Skip to content

feat: implement deactivated account pattern for migration#33

Merged
ascorbic merged 27 commits into
mainfrom
claude/deactivate-account-pattern-EDLhH
Jan 1, 2026
Merged

feat: implement deactivated account pattern for migration#33
ascorbic merged 27 commits into
mainfrom
claude/deactivate-account-pattern-EDLhH

Conversation

@ascorbic
Copy link
Copy Markdown
Owner

Implements the deactivated account pattern to support account migration:

Account State Management:

  • Add active column to repo_state table to track activation state
  • Add INITIAL_ACTIVE env var to control initial account state
  • Add RPC methods: rpcGetActive(), rpcActivateAccount(), rpcDeactivateAccount()

Write Operation Guards:

  • Add ensureActive() check before all write operations
  • Block createRecord, putRecord, deleteRecord, applyWrites when deactivated
  • Return "AccountDeactivated" error with helpful message

New Endpoints:

  • POST /xrpc/com.atproto.server.activateAccount - Enable writes and firehose
  • POST /xrpc/com.atproto.server.deactivateAccount - Disable writes
  • Enhanced getAccountStatus to return actual activation state

Init Script Migration Flow:

  • Ask if migrating existing account vs creating new account
  • For migration: prompt for current handle, resolve to DID, deploy deactivated
  • For new account: generate DID, set up identity, deploy active
  • Add handle resolver utility using DNS and HTTPS well-known methods
  • Show migration steps (export, import, PLC update, activate)
  • Show identity setup instructions for new accounts

Testing:

  • All 169 tests pass
  • Existing functionality preserved
  • Deactivated accounts can read and import but not write

Implements the deactivated account pattern to support account migration:

**Account State Management:**
- Add `active` column to `repo_state` table to track activation state
- Add `INITIAL_ACTIVE` env var to control initial account state
- Add RPC methods: `rpcGetActive()`, `rpcActivateAccount()`, `rpcDeactivateAccount()`

**Write Operation Guards:**
- Add `ensureActive()` check before all write operations
- Block `createRecord`, `putRecord`, `deleteRecord`, `applyWrites` when deactivated
- Return "AccountDeactivated" error with helpful message

**New Endpoints:**
- `POST /xrpc/com.atproto.server.activateAccount` - Enable writes and firehose
- `POST /xrpc/com.atproto.server.deactivateAccount` - Disable writes
- Enhanced `getAccountStatus` to return actual activation state

**Init Script Migration Flow:**
- Ask if migrating existing account vs creating new account
- For migration: prompt for current handle, resolve to DID, deploy deactivated
- For new account: generate DID, set up identity, deploy active
- Add handle resolver utility using DNS and HTTPS well-known methods
- Show migration steps (export, import, PLC update, activate)
- Show identity setup instructions for new accounts

**Testing:**
- All 169 tests pass
- Existing functionality preserved
- Deactivated accounts can read and import but not write
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 31, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
atproto-pds 074e979 Jan 01 2026, 08:49 AM

ascorbic and others added 7 commits December 31, 2025 07:16
- Use @atproto-labs/handle-resolver with Cloudflare DoH for reliable
  handle-to-DID resolution instead of custom implementation
- Fix retry flow when handle resolution fails: users can now choose
  to try a different handle or enter DID manually
- Default handle to hostname for new accounts (simpler setup)
- Show different identity setup notes based on whether handle matches
  hostname - when they match, explain that verification is automatic
- Add link to official AT Protocol migration docs in migration steps
- Add INITIAL_ACTIVE to VarName type for TypeScript compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add endpoints and infrastructure for reliable account migration:

- listMissingBlobs: returns blobs referenced but not yet uploaded
- getBlocks: returns CAR file with specific blocks by CID
- Enhanced getAccountStatus: returns full migration metrics
  (repoBlocks, indexedRecords, expectedBlobs, importedBlobs)

Infrastructure:
- record_blob table tracks blob refs in records (populated on import)
- imported_blobs table tracks successful uploads
- extractBlobCids() helper for recursive blob detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add `pds migrate` command for migrating accounts from source PDS
  - Defaults to production, use --dev for local testing
  - Progress bar for blob transfers
  - Bright styling for notes using picocolors

- Improve init workflow
  - Ask for worker name (slugified handle as default)
  - Offer to push secrets to Cloudflare at end
  - Updated next steps with local testing instructions

- Fix CID serialization in record methods for imported repos
- Fix blob extraction to handle CID objects directly
- Add `pds-client.ts` for XRPC communication with PDS
- Change create-pds to ask for "Folder name" instead of "Project name"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a deactivated account pattern to enable seamless account migration from existing Bluesky infrastructure to self-hosted PDSs. The implementation provides a safe, resumable migration workflow where accounts start deactivated, users import their data, and then activate once migration is complete.

Key changes:

  • Account activation state management with write operation guards
  • New activation/deactivation XRPC endpoints
  • Enhanced migration CLI with handle resolution and progress tracking
  • Blob tracking infrastructure for monitoring migration completeness

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/pds/src/storage.ts Adds activation state column and blob tracking tables for migration progress
packages/pds/src/account-do.ts Implements activation checks, RPC methods, and blob extraction logic
packages/pds/src/xrpc/server.ts Adds activate/deactivate/resetMigration endpoints with metrics
packages/pds/src/xrpc/repo.ts Adds listMissingBlobs endpoint for migration progress
packages/pds/src/xrpc/sync.ts Adds getBlocks endpoint for partial sync
packages/pds/src/cli/commands/init.ts Adds migration mode with handle resolution and custom domain validation
packages/pds/src/cli/commands/migrate.ts Implements data transfer workflow with resumability
packages/pds/src/cli/utils/* Adds handle resolver, PDS client, and improved secret prompts
packages/pds/test/migration.test.ts Adds tests for new migration endpoints
pnpm-lock.yaml Adds handle-resolver and picocolors dependencies
plans/todo/*.md Updates documentation to reflect completed migration features
packages/oauth-provider/README.md Adds comprehensive OAuth provider documentation
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/pds/src/cli/commands/init.ts Outdated
Comment thread packages/pds/src/xrpc/server.ts
Comment thread packages/pds/src/cli/commands/migrate.ts
Comment thread packages/pds/src/cli/commands/migrate.ts
Comment thread packages/pds/src/cli/commands/migrate.ts
Comment thread packages/pds/src/cli/commands/migrate.ts Outdated
Comment thread packages/pds/src/account-do.ts Outdated
Comment thread packages/pds/test/migration.test.ts Outdated
Address all review comments from PR #33:

1. Fix duplicate success message in init.ts (line 213-215)
   - Consolidated spinner.stop() and log.success() into single output

2. Fix hardcoded localhost:5173 in migrate.ts (line 445)
   - Changed to use ${targetDomain} variable

3. Fix variable shadowing in migrate.ts (totalBlobs declared twice)
   - Renamed outer variable to expectedBlobs to avoid shadowing

4. Implement missing 'pds activate' command
   - Created packages/pds/src/cli/commands/activate.ts
   - Added client methods activateAccount()/deactivateAccount()
   - Registered commands in CLI index

5. Implement missing 'pds deactivate' command
   - Created packages/pds/src/cli/commands/deactivate.ts
   - Both commands handle dev/production modes
   - Proper error handling and confirmation prompts

6. Add tests for activate/deactivate endpoints
   - Added 4 new tests in migration.test.ts
   - Tests cover auth requirements and state transitions
   - Verify writes are blocked when deactivated

7. Remove unused status variable in migration.test.ts (line 327)
   - Removed unused variable assignment

8. Fix test environment
   - Added INITIAL_ACTIVE: "true" to vitest.config.ts
   - Ensures tests start with active accounts by default
- Add afterEach hook to reactivate account after each test
  This prevents deactivated state from leaking between tests

- Fix deactivate test assertion
  Changed from parsing JSON error to checking status code
  The error response is plain text, not JSON

- Improve importRepo validation logic
  Only reject imports if account is ACTIVE and repo exists
  Allow imports on deactivated accounts (for migration)
  This enables safe re-import workflow with --clean flag

All 181 tests now passing ✅
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 12 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +210 to +211
} catch {
// Ignore errors, use fallback domains
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration command fetches hosted domains from the source PDS but silently ignores any errors during this fetch. While the fallback domains work, users might benefit from knowing if domain detection failed. Consider adding a debug log or warning when the domain fetch fails to help diagnose potential issues.

Suggested change
} catch {
// Ignore errors, use fallback domains
} catch (err) {
// Ignore errors, use fallback domains
p.log.debug?.(
`Failed to detect hosted domains from source PDS; using fallback domains instead. Error: ${String(
err,
)}`,
);

Copilot uses AI. Check for mistakes.
Comment thread packages/pds/src/account-do.ts
Comment on lines +407 to +411
? `SELECT rb.blobCid, rb.recordUri FROM record_blob rb
LEFT JOIN imported_blobs ib ON rb.blobCid = ib.cid
WHERE ib.cid IS NULL AND rb.blobCid > ?
ORDER BY rb.blobCid
LIMIT ?`
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SQL query uses rb.blobCid > ? for cursor-based pagination, which assumes CIDs are sortable strings. While this works, it could lead to non-deterministic ordering if CIDs with the same string prefix are compared. Consider adding an explicit ORDER BY clause or documenting this assumption about CID string ordering.

Copilot uses AI. Check for mistakes.
Comment thread packages/pds/src/cli/utils/secrets.ts Outdated
spinner.message(`Transferring images ${progressBar(synced, totalBlobs)}`);
} catch (err) {
synced++;
failedBlobs.push(blob.cid);
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error response for failed blob transfers is silently swallowed by incrementing the synced counter and adding to failedBlobs array. However, the specific error information is lost, making it difficult to debug why certain blobs failed. Consider logging the error message or CID to help users understand what went wrong during migration.

Suggested change
failedBlobs.push(blob.cid);
failedBlobs.push(blob.cid);
const errorMessage = err instanceof Error ? err.message : String(err);
p.log.warn(`Failed to transfer blob ${blob.cid}: ${errorMessage}`);

Copilot uses AI. Check for mistakes.
Comment thread packages/pds/src/index.ts
Comment on lines +233 to +235
app.post("/xrpc/gg.mk.experimental.resetMigration", requireAuth, (c) =>
server.resetMigration(c, getAccountDO(c.env)),
);
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new experimental endpoint gg.mk.experimental.resetMigration is not covered by tests. This endpoint performs destructive operations (clearing blocks and blob tracking) and has important safety checks (only works on deactivated accounts). Add test coverage to verify: 1) it requires authentication, 2) it fails on active accounts with proper error, 3) it successfully clears data on deactivated accounts, 4) it returns accurate counts of deleted items.

Copilot uses AI. Check for mistakes.
Comment thread packages/pds/src/xrpc/server.ts Outdated
Comment thread packages/pds/src/cli/commands/init.ts Outdated
Comment thread packages/pds/src/cli/utils/handle-resolver.ts
Comment thread packages/pds/src/cli/commands/deactivate.ts Outdated
Critical fixes:
- Add retry limits to prevent infinite loops
  - Handle resolution: max 3 attempts with helpful error message
  - Password prompt: max 3 attempts before exit

- Extract duplicate helper functions to shared module
  - Created cli-helpers.ts with getTargetUrl() and getDomain()
  - Removed duplication from activate.ts, deactivate.ts, migrate.ts
  - Follows DRY principles

Minor improvements:
- Document that activated/active fields are intentional
  - Both used by different parts of the codebase
  - activated: used in migrate.ts and tests
  - active: more descriptive, returned by getActive()

Note: Other Copilot suggestions deferred:
- SQL ordering assumption is correct (CIDs are deterministic)
- 10s timeout is reasonable for DNS-over-HTTPS
- Error logging already exists for failed blob transfers
- Magic number "10" already removed in previous commit
…resetMigration tests

- Remove duplicate 'activated' field from getAccountStatus endpoint, keep only 'active'
- Update migrate.ts to use 'active' field consistently
- Update all test assertions to use 'active' instead of 'activated'
- Add comprehensive test coverage for gg.mk.experimental.resetMigration endpoint:
  - Test authentication requirement
  - Test failure on active accounts (AccountActive error)
  - Test success on deactivated accounts with deletion count verification

All 24 migration tests passing.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 28 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/pds/src/cli/commands/init.ts Outdated
Comment on lines +230 to +239
if (attempts >= MAX_ATTEMPTS) {
p.log.error("Unable to resolve handle after 3 attempts.");
p.log.info("");
p.log.info("You can:");
p.log.info(" 1. Double-check your handle spelling");
p.log.info(" 2. Provide your DID directly if you know it");
p.log.info(" 3. Run 'pds init' again when ready");
p.outro("Initialization cancelled.");
process.exit(1);
}
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MAX_ATTEMPTS check on line 230 is incorrectly placed inside the "else" block (when DID is successfully resolved). This means the check will never be reached when it should be - after failed resolution attempts. The check should be moved outside the "else" block, after the retry/manual DID flow completes, so it can properly exit the loop after 3 failed attempts.

Copilot uses AI. Check for mistakes.
Comment on lines +405 to +414
const body = (await response.json()) as {
activated: boolean;
validDid: boolean;
repoCommit: string;
repoRev: string;
repoBlocks: number;
indexedRecords: number;
expectedBlobs: number;
importedBlobs: number;
};
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type definition includes 'activated: boolean;' but the actual assertions use 'body.active'. This is inconsistent with the type annotation. The type should be updated to use 'active: boolean;' instead of 'activated: boolean;' to match the actual API response field name.

Copilot uses AI. Check for mistakes.
},
},
async run({ args }) {
const pm = detectPackageManager();
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function detectPackageManager is used on line 54 but is not imported or defined in this file. This will cause a runtime error. The function should either be imported from the create-pds package or implemented locally in this file.

Copilot uses AI. Check for mistakes.
process.exit(1);
}

const sourcePdsUrl = getPdsEndpoint(didDoc);
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function getPdsEndpoint is used on line 137 but is not imported or defined in this file. This will cause a runtime error. The function needs to be imported or implemented to extract the PDS endpoint from a DID document.

Copilot uses AI. Check for mistakes.
/**
* Migration command - transfers account data from source PDS to local PDS
*/
import { existsSync } from "node:fs";
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement on line 4 imports existsSync from "node:fs" but this function is never used in the file. This unused import should be removed to keep the code clean.

Suggested change
import { existsSync } from "node:fs";

Copilot uses AI. Check for mistakes.
Comment thread packages/pds/src/account-do.ts Outdated
@ascorbic ascorbic force-pushed the claude/deactivate-account-pattern-EDLhH branch from b3d6282 to 6b83f43 Compare December 31, 2025 17:50
Address PR review comment: Type definition at line 406 was inconsistent
with actual API response field. Changed from 'activated: boolean' to
'active: boolean' to match the getAccountStatus endpoint response.

All 184 tests passing.
@ascorbic ascorbic force-pushed the claude/deactivate-account-pattern-EDLhH branch from 6b83f43 to 8e3ed6b Compare December 31, 2025 17:54
claude and others added 5 commits December 31, 2025 18:31
Move gg.mk.experimental.resetMigration describe block inside Account Migration
describe so it benefits from the afterEach hook that reactivates the account.
This prevents deactivated state from leaking to subsequent test files.

All 184 tests passing.
When resetMigration() clears the repo, it must also reset the
repoInitialized flag. Otherwise, subsequent getRepo() calls skip
initialization and return null, causing test failures in firehose
tests that run after migration tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* wip

* fix: complete e2e test suite setup

- Switch from programmatic Vite to subprocess for better isolation
- Use test.local as handle (requires 2+ parts)
- Fix BlobRef CID access (use .ref.toString() for CID object)
- Skip getLatestCommit tests (endpoint not implemented)
- Update deleteRecord test to expect error

All 32 tests pass (2 skipped for unimplemented endpoint)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use static fixture for e2e tests

- Create e2e/fixture/ directory with static template files
- Copy fixture to temp directory instead of programmatic file creation
- Use npm run dev instead of npx vite
- Replace {{PDS_PACKAGE_PATH}} placeholder with actual path

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: add e2e fixture .dev.vars (test credentials only)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* choire: run all tests

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Runs e2e tests against live Vite dev server on PRs and main pushes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Attempted fixes for InspectorProxyController.updateConnection error:
- Set miniflare.inspector: false in vite.config.ts
- Set dev.inspector_port: 0 in wrangler.jsonc
- Set NO_INSPECTOR=1 environment variable

However, the error persists. The InspectorProxyController is still attempting
to make fetch calls that fail with 'TypeError: fetch failed'. This appears
to be an environment-specific issue with the Cloudflare Vite plugin's
inspector proxy trying to connect in CI/test environments.

Issue: https://github.com/ascorbic/atproto-worker/actions/runs/20628674621
claude and others added 9 commits January 1, 2026 07:45
The InspectorProxyController.updateConnection fetch error is caused by this
sandbox's proxy environment, not an actual issue with the code or CI.

The proxy intercepts localhost connections that the inspector needs, causing
fetch to fail. This doesn't occur on Mac (no proxy) or in GitHub CI.

Reverting inspector disable configurations as they don't address any real
issue and add unnecessary complexity.
Add comprehensive logging to help diagnose CI failures:
- Environment information (Node version, platform, temp dir)
- Timing information for each step
- npm install stdout/stderr output (even on success)
- Incremental Vite server output as it arrives
- Package.json path replacement details
- Total setup time tracking

This will help identify where the e2e setup is failing in GitHub CI.
The previous regex pattern didn't match because Vite's output includes
ANSI escape codes (colors, bold, unicode arrows) that break pattern matching.

Changed from: /Local:\s+http:\/\/localhost:(\d+)/
Changed to:   /localhost:(\d+)/

This simpler pattern will match regardless of ANSI formatting or unicode
characters in the output.
Vite's output includes ANSI escape codes for formatting (bold, colors)
even within the port number itself. Strip these codes before matching
to ensure the regex can find the port number.
Remove verbose logging now that e2e tests are working:
- Removed detailed environment info, timing, and incremental output
- Keep essential messages: dependency install and server start
- Preserve full output in error messages for debugging

Maintains ANSI escape code stripping for reliable port detection.
- Ignore e2e/fixture directory (used at runtime by e2e tests)
- Add vite and @cloudflare/vite-plugin to ignoreDependencies (used in e2e fixture)
- Remove unused TEST_AUTH_TOKEN export
- Remove ws/@types/ws from ignoreDependencies (no longer flagged)
…action

Replace custom type-checking logic with official @atproto/lex-data helpers:
- Use `asCid()` instead of duck-typing CID detection in serializeRecord
- Use `isBlobRef()` instead of manual $type checking in extractBlobCids

This makes the code more maintainable and relies on official implementations.
Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 1, 2026

@ascorbic I've opened a new pull request, #35, to work on those changes. Once the pull request is ready, I'll request review from you.

@ascorbic ascorbic merged commit 4f5b50c into main Jan 1, 2026
6 checks passed
@ascorbic ascorbic deleted the claude/deactivate-account-pattern-EDLhH branch January 1, 2026 09:06
@mixie-bot mixie-bot Bot mentioned this pull request Jan 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants