From 0abe841926826670387d12b2c10fb3e2fa3fabf7 Mon Sep 17 00:00:00 2001 From: Boris Mann Date: Fri, 7 Aug 2026 00:41:58 -0700 Subject: [PATCH] Release v0.2.1: account-aware state guard The local state file (cached blob refs + record rkeys) is pinned to the account/PDS that created it. Importing into a different account with old state reused stale blobs, so every record failed to publish with putRecord "Could not find blob". Record the last account DID and, when logged in as a different account, warn and treat the run as a fresh import (re-upload assets, create new records) instead of silently failing. Re-running on the same account is unchanged. Bump to 0.2.1; add release note and update operational docs. --- docs/offprint-feedback-fixes.md | 15 +++++++++------ docs/releases/v0.2.1.md | 27 +++++++++++++++++++++++++++ package.json | 2 +- src/index.ts | 24 ++++++++++++++++++++++++ src/state.ts | 1 + 5 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 docs/releases/v0.2.1.md diff --git a/docs/offprint-feedback-fixes.md b/docs/offprint-feedback-fixes.md index fb3ca9c..d0e8bc8 100644 --- a/docs/offprint-feedback-fixes.md +++ b/docs/offprint-feedback-fixes.md @@ -43,16 +43,19 @@ Tests covering all four: `tests/converter.test.ts` (run via `npm test`). These tripped up the live re-export and are not in the block converter: -- **The account/PDS must match the records.** The GhostOff records live under +- **Account/PDS must match the records.** The GhostOff records live under `atprotocol.dev` → `did:plc:bbj62ki3gmlsjfzsr3dxbkna` on PDS `https://selfhosted.social`, not the default bmann.ca account. Set `ATP_IDENTIFIER=atprotocol.dev`. The `.env` is updated for our testing. -- **`state.json`'s `state.assets` blob cache is per-account/PDS.** Cached blob - refs uploaded under a different PDS are invalid on the target PDS. `putRecord` - fails with `Could not find blob: bafkrei...`. Fix: clear `state.assets = {}` - (preserve `state.posts` so rkeys are reused) to force re-download - (from the local `ghostoff-export/assets` cache, fast) and re-upload. +- **`state.json` is per-account/PDS.** Cached blob refs and record rkeys are + pinned to the account that created them. Importing into a **different** + account reuses old blob refs, so new records reference blobs the new PDS + doesn't have → `putRecord … failed: Could not find blob`. The CLI now detects + this: on login it compares the account DID stored in `state.posts` with the + logged-in account and, on a mismatch, warns and treats the run as a fresh + import (re-uploads assets, creates new records). For a normal idempotent + re-run on the *same* account nothing changes. - **`putRecord` `swapRecord` staleness.** If a record was updated elsewhere (a dashboard/sync), the stored CID is stale and `putRecord` rejects with diff --git a/docs/releases/v0.2.1.md b/docs/releases/v0.2.1.md new file mode 100644 index 0000000..97186c9 --- /dev/null +++ b/docs/releases/v0.2.1.md @@ -0,0 +1,27 @@ +# GhostOff v0.2.1 — account-aware state + +Small follow-up to make re-importing into a different account safe and obvious. + +## What changed + +- The local state file (`ghostoff-state.json`) caches blob references and record + rkeys that are tied to the account/PDS that created them. Importing into a + different account with old state caused every record to fail with + `putRecord … Could not find blob`. +- GhostOff now records the account DID it last ran as (`accountDid`) and, when + you log in as a different account, **warns and treats the run as a fresh + import** (re-uploads assets and creates new records) instead of silently + reusing stale blobs/rkeys. Re-running on the same account (normal + idempotent sync) is unchanged. + +## Install / upgrade + +```bash +npm install -g ghostoff@latest +``` + +## Links + +- npm package: **[npmx.dev/package/ghostoff](https://npmx.dev/package/ghostoff)** +- Live publication: **[ghostoff.offprint.app](https://ghostoff.offprint.app)** +- Source: **[tangled.org/bmann.ca/ghostoff](https://tangled.org/bmann.ca/ghostoff)** \ No newline at end of file diff --git a/package.json b/package.json index 7c2e458..3ef9ef5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostoff", - "version": "0.2.0", + "version": "0.2.1", "description": "Export Ghost CMS posts to Offprint / Standard.site over AT Protocol", "type": "module", "author": "Boris Mann ", diff --git a/src/index.ts b/src/index.ts index 869109b..6553962 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,30 @@ async function main() { state.publicationAtUri = config.publicationAtUri; const session = config.dryRun ? undefined : await getSession(config, log); + + if (session) { + state.accountDid = session.did; + // The state file (cached blob refs and record rkeys) is pinned to a specific + // account/PDS. If it belongs to a different account than the one we logged in + // with, reusing it would make new records reference blobs that don't exist on + // this account's PDS (putRecord "Could not find blob"). Treat it as a fresh + // import instead of silently failing. + const stateDids = new Set(); + for (const p of Object.values(state.posts)) { + const m = p.documentUri?.match(/^at:\/\/(did:[^/]+)\//); + if (m) stateDids.add(m[1]); + } + const hasState = Object.keys(state.posts).length > 0 || (state.assets != null && Object.keys(state.assets).length > 0); + if (hasState && stateDids.size > 0 && !stateDids.has(session.did)) { + log.warn( + `state ${config.stateFile} belongs to account ${[...stateDids].join(', ')}, not ${session.did}; ` + + 'treating this run as a fresh import (re-uploading assets, creating new records).' + ); + state.posts = {}; + state.assets = {}; + } + } + let processed = 0; let failed = 0; let skipped = 0; diff --git a/src/state.ts b/src/state.ts index d23435b..e790254 100644 --- a/src/state.ts +++ b/src/state.ts @@ -12,6 +12,7 @@ export interface PostState { export interface AppState { publicationAtUri?: string; + accountDid?: string; assets?: Record; posts: Record; } -- 2.51.2