From 0b9aac129dbb6bd0a6681a3ff6330a181c026d44 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 04:53:27 +0000 Subject: [PATCH 1/2] docs(spec): publicSharing.enabled is a standing policy held at redemption; resolveToken names the switched-off block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TSDoc above `publicSharing.enabled` described only the mint half ("no share links can be issued"). The switch is a standing policy re-read on every redemption, so with it off no existing link resolves either — links minted while it was on, and links minted through the system-context / `permissive` mint bypass alike. The comment now states that, in the shape the sibling `eligibility` predicate's prose uses. `IShareLinkService.resolveToken`'s `null`-cause list gains the switched-off block in the position the gates run; the contract's design notes gain a matching entry beside the eligibility one, and the `isSystem` mint bypass is marked mint-only. Documentation only: no schema, shape or behaviour change; the `.describe()` string feeding the generated reference is untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno --- ...c-sharing-enabled-standing-policy-tsdoc.md | 11 +++++++ .../spec/src/contracts/share-link-service.ts | 33 ++++++++++++++----- packages/spec/src/data/object.zod.ts | 17 +++++++++- 3 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 .changeset/public-sharing-enabled-standing-policy-tsdoc.md diff --git a/.changeset/public-sharing-enabled-standing-policy-tsdoc.md b/.changeset/public-sharing-enabled-standing-policy-tsdoc.md new file mode 100644 index 0000000000..a282e7ce90 --- /dev/null +++ b/.changeset/public-sharing-enabled-standing-policy-tsdoc.md @@ -0,0 +1,11 @@ +--- +"@objectstack/spec": patch +--- + +Document `publicSharing.enabled` as the standing policy it is, and name the switched-off block among `resolveToken`'s `null` causes. + +The TSDoc above `publicSharing.enabled` read "when false, no share links can be issued for this object" — true, but only the mint half. Since the switch became a standing policy held at every redemption, a block that is off also stops every existing link on it from resolving: links minted while it was on, and links minted through the system-context / `permissive` mint bypass alike. Re-enabling the block serves them again; no row moves. The comment now says so, in the shape the sibling `eligibility` predicate's prose already uses. + +`IShareLinkService.resolveToken` enumerated the causes of its undifferentiated `null` — unknown, revoked, expired, audience, password, record gone, ineligible — without the switched-off block, so an implementer reading the list to enumerate refusal causes got an incomplete set. The list now carries it, in the position the gates run; the contract's design notes gain a matching entry beside the eligibility one, and the `isSystem` mint bypass is marked as mint-only. + +Documentation only: no schema, shape or behaviour change, and the `.describe()` string that feeds the generated reference is untouched. `@objectstack/spec` ships the corrected text in its published type declarations and, for `object.zod.ts`, in the source file it publishes directly. diff --git a/packages/spec/src/contracts/share-link-service.ts b/packages/spec/src/contracts/share-link-service.ts index d2bc3a85b7..cae16801a1 100644 --- a/packages/spec/src/contracts/share-link-service.ts +++ b/packages/spec/src/contracts/share-link-service.ts @@ -46,6 +46,20 @@ * predicate that cannot be evaluated refuses. The redemption refusal is * the undifferentiated `null` documented on {@link * IShareLinkService.resolveToken}. + * + * 7. **`publicSharing.enabled` is a STANDING policy too, not a mint-time + * switch (#14033).** Note 2 is the mint half only. Implementations + * re-read the object's CURRENT block on every `resolveToken`, and a + * block that is off refuses every token on it — those minted while it + * was on, and those minted under the system-context / `permissive` mint + * bypass alike: redemption is an anonymous act, and how the row got + * there buys it nothing. Retroactive on deploy, as note 6 was. Not a + * revocation: no row moves, and re-enabling the block serves them again. + * Off ⇒ nothing inside the block is evaluated (the predicate is not run, + * the redaction set is not computed); on ⇒ the sibling keys keep the + * redemption-time behaviour of note 6. The refusal is the same + * undifferentiated `null` documented on {@link + * IShareLinkService.resolveToken}. */ import type { ExecutionContext } from '../kernel/execution-context.zod.js'; @@ -183,7 +197,8 @@ export interface ShareLinkExecutionContext { * * Implementations MUST treat `context.isSystem === true` as a bypass * (skip the per-object opt-in check) so platform bootstrappers can seed - * demo links. + * demo links. That bypass is MINT-only: a link seeded this way is governed + * at redemption like any other (design note 7, #14033). * * ## The context every method takes (#6206 ruling, #6430) * @@ -231,16 +246,18 @@ export interface IShareLinkService { * `last_used_at` as a side effect of a SUCCESSFUL resolution only. * * Returns `null` when the token does not exist, is revoked, is expired, - * fails the audience or password gate, names a record that no longer exists - * (#5190), or names a record that no longer satisfies the object's - * `publicSharing.eligibility` predicate (#13608). + * fails the audience or password gate, names an object whose + * `publicSharing.enabled` switch is off — the block absent or disabled, + * however the link was minted (#14033, design note 7) — names a record that + * no longer exists (#5190), or names a record that no longer satisfies the + * object's `publicSharing.eligibility` predicate (#13608). * * ⛔ That single `null` is the contract, not an implementation detail. The * caller of this method may hold nothing but a token, and distinguishing - * "does not exist" from "revoked" from "no longer eligible" for such a - * caller is an existence oracle. Implementations MUST NOT return a - * distinguishable answer per reason, and MUST NOT throw one either; the - * readable reason belongs in the server-side log. + * "does not exist" from "revoked" from "switched off" from "no longer + * eligible" for such a caller is an existence oracle. Implementations MUST + * NOT return a distinguishable answer per reason, and MUST NOT throw one + * either; the readable reason belongs in the server-side log. * * @param token raw token from the URL / cookie * @param probe contextual gates the caller has already evaluated diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 230fdd7a4d..d55e218b4f 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -2275,7 +2275,22 @@ const ObjectSchemaBase = strictObject( 'external access is that key, one level up.', }, }, { - /** Master switch. When false (default), no share links can be issued for this object. */ + /** + * Master switch — a STANDING policy held at every redemption, not a + * mint-time check (#14033; the same shape as the `eligibility` predicate + * below, #13608). + * + * When false (default), no share links can be issued for this object AND + * no share link on it resolves: `resolveToken` re-reads this switch on + * every redemption, so links minted while it was on stop serving the + * moment it is turned off — links minted through the system-context / + * `permissive` mint bypass included (redemption is an anonymous act; how + * the row got there buys it nothing). Not a revocation: no row moves, and + * re-enabling the block serves them again. Off ⇒ nothing inside this block + * is evaluated; on ⇒ the sibling keys apply at redemption. The refusal is + * the undifferentiated `null` documented on `IShareLinkService.resolveToken` + * (`contracts/share-link-service.ts`). + */ enabled: z.boolean().default(false).describe('Allow records of this object to be published via share link'), /** * Audiences the platform will accept when issuing a link. From a1e1610acab2d7a85e31db64d26c95eec4fa95b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 05:22:25 +0000 Subject: [PATCH 2/2] docs(spec): changeset states where the corrected TSDoc reaches consumers, as measured on the built package The contract file's new lines all survive into dist/contracts/index.d.ts; the object.zod.ts property comment reaches no .d.ts and ships through the directly published source file and the data sourcemap. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno --- .changeset/public-sharing-enabled-standing-policy-tsdoc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/public-sharing-enabled-standing-policy-tsdoc.md b/.changeset/public-sharing-enabled-standing-policy-tsdoc.md index a282e7ce90..1aaf837f68 100644 --- a/.changeset/public-sharing-enabled-standing-policy-tsdoc.md +++ b/.changeset/public-sharing-enabled-standing-policy-tsdoc.md @@ -8,4 +8,4 @@ The TSDoc above `publicSharing.enabled` read "when false, no share links can be `IShareLinkService.resolveToken` enumerated the causes of its undifferentiated `null` — unknown, revoked, expired, audience, password, record gone, ineligible — without the switched-off block, so an implementer reading the list to enumerate refusal causes got an incomplete set. The list now carries it, in the position the gates run; the contract's design notes gain a matching entry beside the eligibility one, and the `isSystem` mint bypass is marked as mint-only. -Documentation only: no schema, shape or behaviour change, and the `.describe()` string that feeds the generated reference is untouched. `@objectstack/spec` ships the corrected text in its published type declarations and, for `object.zod.ts`, in the source file it publishes directly. +Documentation only: no schema, shape or behaviour change, and the `.describe()` string that feeds the generated reference is untouched. Where the corrected text reaches consumers, measured on the built package: every new line in `share-link-service.ts` ships in the published `dist/contracts/index.d.ts` (the interface-member docs and the module design notes both survive the declaration bundle); the `object.zod.ts` property comment reaches no `.d.ts` (the schema's declaration is an inferred type) and ships through the source file `@objectstack/spec` publishes directly (`src/**/*.zod.ts`) and through `dist/data/index.js.map`.