Skip to content

Add owner-only archive/unarchive realm endpoints#5341

Draft
lukemelia wants to merge 3 commits into
mainfrom
cs-11662-archiveunarchive-endpoints-owner-only-post-_archive-realm
Draft

Add owner-only archive/unarchive realm endpoints#5341
lukemelia wants to merge 3 commits into
mainfrom
cs-11662-archiveunarchive-endpoints-owner-only-post-_archive-realm

Conversation

@lukemelia

Copy link
Copy Markdown
Contributor

What

Adds two owner-only endpoints to set and clear the realm archive flag:

  • POST /_archive-realm — sets realm_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

  • Authorization: the requester must hold realm-owner on the target realm, otherwise 403.
  • Public/catalog realms are rejected (422): world-readable realms are shared resources and not archivable.
  • Archive sets archived_at = now(); unarchive clears it.
  • Restore enqueues a full reindex for the realm (a full-reindex job 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 in routes.ts alongside publish/unpublish, behind the same jwtMiddleware.

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-owner 403, and the public-realm 422 rejection. 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 to main once it lands.

🤖 Generated with Claude Code

);
return null;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[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);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[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.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Host Test Results

    1 files  ± 0      1 suites  ±0   2h 8m 56s ⏱️ -9s
3 285 tests +38  3 270 ✅ +38  15 💤 ±0  0 ❌ ±0 
3 304 runs  +38  3 289 ✅ +38  15 💤 ±0  0 ❌ ±0 

Results for commit 2af6872. ± Comparison against earlier commit 174b3d4.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   10m 0s ⏱️ +35s
1 757 tests +2  1 757 ✅ +2  0 💤 ±0  0 ❌ ±0 
1 850 runs  +2  1 850 ✅ +2  0 💤 ±0  0 ❌ ±0 

Results for commit 2af6872. ± Comparison against earlier commit 174b3d4.

@lukemelia lukemelia changed the base branch from cs-11657-db-add-archived_at-to-realm_metadata-archive-query-helpers to main June 26, 2026 10:37
lukemelia and others added 3 commits June 26, 2026 06:39
…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>
@lukemelia lukemelia force-pushed the cs-11662-archiveunarchive-endpoints-owner-only-post-_archive-realm branch from 174b3d4 to 2af6872 Compare June 26, 2026 10:46
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.

1 participant