Add owner-only archive/unarchive realm endpoints#5341
Conversation
| ); | ||
| return null; | ||
| } | ||
|
|
There was a problem hiding this comment.
[Codex] Please run the body id through normalizeRealmURL here instead of ensureTrailingSlash + new URL. ensureTrailingSlash is just string concatenation, so an id like https://host/realm?token=abc becomes https://host/realm?token=abc/ and will miss the canonical permission/metadata rows; an invalid URL also throws before the handler try, producing a server error instead of the expected 400. utils/realm-url.ts calls out that every realm-URL keying path needs the shared normalizer, and the delete/upsert-permission handlers already use it.
There was a problem hiding this comment.
[Claude Code 🤖] Fixed — the resolver now runs the body id through normalizeRealmURL, so query strings/fragments are stripped to the canonical realm root and an invalid URL returns 400 instead of throwing a system error before the handler try. 2af6872.
| return null; | ||
| } | ||
|
|
||
| let realmURL = ensureTrailingSlash(realmId); |
There was a problem hiding this comment.
[Codex] This currently treats a realm_user_permissions row as proof that the target realm exists. That lets a stale/manual realm-owner permission row archive an arbitrary URL, and /_unarchive-realm would then enqueue a full reindex for a realm with no registry/disk backing. The other lifecycle handlers now use realm_registry as the source of truth for realm existence; can this shared resolver first require a source realm_registry row for realmURL (404 if missing, 422 if it is a published/bootstrap row) and add a test that permissions alone are not enough?
There was a problem hiding this comment.
[Claude Code 🤖] Good catch — the resolver now requires a realm_registry row as the source of truth for existence: 404 when there's no row (a stale/manual realm-owner permission alone is no longer enough to archive an arbitrary URL or trigger a restore reindex for a realm with no disk backing), and 422 when the row isn't kind = 'source' (published/bootstrap). Added tests that permissions-only yields 404 and that a published realm is rejected. 2af6872.
Host Test Results 1 files ± 0 1 suites ±0 2h 8m 56s ⏱️ -9s Results for commit 2af6872. ± Comparison against earlier commit 174b3d4. Realm Server Test Results 1 files ±0 1 suites ±0 10m 0s ⏱️ +35s Results for commit 2af6872. ± Comparison against earlier commit 174b3d4. |
…itle Add a secondary sort on url to fetchArchivedRealmsForOwner so the result order stays stable when several realms share an archived_at second (SQLite's CURRENT_TIMESTAMP is 1-second resolution). Replace a curly apostrophe in a test title with an ASCII one so module filters and copy/paste stay portable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /_archive-realm and POST /_unarchive-realm set and clear the
realm_metadata.archived_at flag for the realm named in the JSON:API body
({ data: { type: 'realm', id: realmURL } }). Both require the requester
to be a realm-owner of the target (else 403) and reject public/catalog
realms, which are world-readable and not archivable (else 422). Restore
enqueues a full-reindex job for the realm to rebuild its index from disk;
the indexer owns how a restored realm re-enters the sweep.
Shared authorize/validate logic lives in archive-realm-utils. Integration
tests cover the owner happy paths, the non-owner 403, and the public-realm
rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the body id through normalizeRealmURL so query strings/fragments are stripped and an invalid URL returns 400 instead of throwing a system error. Require a source realm_registry row to exist (404 if missing, 422 for published/bootstrap realms) rather than treating a realm_user_permissions row as proof of existence — a stale or manual realm-owner grant must not let an arbitrary URL be archived, nor have its restore enqueue a reindex for a realm with no disk backing. Add tests that permissions alone yield 404 and that a published realm is rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
174b3d4 to
2af6872
Compare
What
Adds two owner-only endpoints to set and clear the realm archive flag:
POST /_archive-realm— setsrealm_metadata.archived_at.POST /_unarchive-realm— clears it and enqueues a full reindex.Both take a JSON:API body
{ data: { type: "realm", id: <realmURL> } }and authenticate via the realm-server JWT.Behavior
realm-owneron the target realm, otherwise403.422): world-readable realms are shared resources and not archivable.archived_at = now(); unarchive clears it.full-reindexjob scoped to that realm URL) so its index is rebuilt from disk. How a restored realm re-enters the index sweep is owned by the indexer work; this endpoint just fires the hook and returns promptly rather than blocking on the reindex.Structure
Shared parse/authorize/validate logic lives in
archive-realm-utils.ts, used by both handlers. Routes are registered inroutes.tsalongside publish/unpublish, behind the samejwtMiddleware.Testing
Integration tests (
server-endpoints/archive-realm-test.ts) cover the owner archive and unarchive happy paths (including asserting the full-reindex job is enqueued), the non-owner403, and the public-realm422rejection. All pass.Stacking
Stacked on the archive storage + query helpers branch; built on
archiveRealm/unarchiveRealm/fetchRealmPermissions. Review/merge that PR first — this base will retarget tomainonce it lands.🤖 Generated with Claude Code