|
| 1 | +--- |
| 2 | +"@objectstack/plugin-sharing": minor |
| 3 | +--- |
| 4 | + |
| 5 | +fix(plugin-sharing): hold `publicSharing.enabled` at redemption, not only at mint (#14033) |
| 6 | + |
| 7 | +**BREAKING** runtime behaviour change on a published package: share links that |
| 8 | +were legitimately minted can now stop resolving without anyone revoking them — |
| 9 | +every link on an object whose `publicSharing.enabled` is not `true`. Shipped as |
| 10 | +`minor` under the repo's launch-window convention (a breaking change does not |
| 11 | +burn a major while the stack is in lockstep). No export is added, removed or |
| 12 | +re-shaped; the level carries the breaking banner, not a surface change. |
| 13 | + |
| 14 | +`ShareLinkService.createLink()` refused to mint on an object whose |
| 15 | +`publicSharing` block was absent or had `enabled !== true` (422 |
| 16 | +`SHARING_NOT_ENABLED`), and nothing checked the switch again. `resolveToken()` |
| 17 | +checked `revoked_at`, `expires_at`, the audience gates, the password, record |
| 18 | +existence and — since #13608 — the block's `eligibility` predicate, then served |
| 19 | +whatever survived, under the system context, to a caller with no principal at |
| 20 | +all. So the platform held the block's CHILD predicate as a standing policy while |
| 21 | +its PARENT switch governed minting only: an author who turned the whole feature |
| 22 | +off stopped new links and not one existing link, and would have had to narrow |
| 23 | +the predicate to stop anonymous serving — the opposite of what the surface |
| 24 | +reads like. Measured before it was changed: a token minted while the block was |
| 25 | +on kept serving the record in full after the block was turned off. |
| 26 | + |
| 27 | +**What changed.** `resolveToken()` reads the object's CURRENT `publicSharing` |
| 28 | +block on every redemption and refuses when `enabled` is not `true` — before the |
| 29 | +record is read, before the usage stamp, before any sibling key inside the block |
| 30 | +is evaluated. Re-enabling the block restores the same tokens: this is a standing |
| 31 | +policy, not a revocation, and no `sys_share_link` row is touched. How a link was |
| 32 | +minted buys it nothing at redemption — a link minted under a system context or |
| 33 | +the service's `permissive` bypass (the system-context ledger's row 37 path) on a |
| 34 | +switched-off object refuses exactly like one orphaned by an author turning the |
| 35 | +block off, and an object with no `publicSharing` block at all is the same switch |
| 36 | +at its default and refuses too. With the block on, `eligibility` (#13608) and |
| 37 | +the declared `redactFields` (#13856) keep their existing redemption-time |
| 38 | +behaviour; nothing new is evaluated. An object the engine cannot return a |
| 39 | +schema for — no `getSchema` on the engine, or an object not registered at the |
| 40 | +moment of redemption — is `enabled: false` by `getPolicy`'s definition and is |
| 41 | +refused at redemption: fail-closed, the same definition `createLink` uses. The |
| 42 | +#13856 entry's "an opted-out object's links keep resolving with the declared |
| 43 | +redactions" state is superseded: with the block off they do not resolve at all. |
| 44 | + |
| 45 | +**The refusal is deliberately indistinguishable.** It is the same answer a |
| 46 | +revoked, expired, unknown or no-longer-eligible token already gets: the |
| 47 | +undifferentiated `null` — no new error code, no new response branch, and no |
| 48 | +usage stamp. Over HTTP a switched-off link is answered with the generic |
| 49 | +`404 INVALID_OR_EXPIRED`, byte-for-byte what a token that never existed |
| 50 | +receives. The readable reason (`SHARING_NOT_ENABLED`, with the link, object and |
| 51 | +record ids) is written to the server-side log at `warn`, where the eligibility |
| 52 | +refusal already writes its own. |
| 53 | + |
| 54 | +**Operator impact — retroactive, on deploy.** Every live link on an object whose |
| 55 | +`publicSharing` block is currently switched off — or that never declared one — |
| 56 | +stops resolving the moment this version is deployed, with no revocation event |
| 57 | +and no grace period. That is the intent: the alternative is a declared switch |
| 58 | +the platform does not hold. Measure before rollout: the objects to read are |
| 59 | +those whose `publicSharing.enabled` is not `true`, and the links at risk are the |
| 60 | +`sys_share_link` rows naming them (`object_name`). To keep such links working, |
| 61 | +enable the block — and narrow it with `eligibility` / `redactFields` if the |
| 62 | +feature was off for a reason; there is no per-link opt-out, deliberately. |
| 63 | +Minting is unchanged: `createLink` still refuses `SHARING_NOT_ENABLED` for an |
| 64 | +ordinary caller, and the system / `permissive` bypass still mints — what it |
| 65 | +mints simply does not serve until the block is on. The refusal logs one `warn` |
| 66 | +line per refused hit and is not latched, so a retroactive deploy with many live |
| 67 | +links on switched-off objects will burst the log once. |
| 68 | + |
| 69 | +<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-shaped: `publicSharing.enabled` keeps its name, its type, its default and its accept-set, and the change is WHEN the platform holds it. There is therefore no tombstone for `objectstack migrate meta` to carry and no mechanical rewrite it could perform — a deployment whose links stop resolving must decide whether the block should be on at all, which is an authoring decision no ledger entry can make on its behalf. --> |
0 commit comments