Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/public-sharing-enabled-standing-policy-tsdoc.md
Original file line number Diff line number Diff line change
@@ -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. 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`.
33 changes: 25 additions & 8 deletions packages/spec/src/contracts/share-link-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
*
Expand Down Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion packages/spec/src/data/object.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading