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
10 changes: 10 additions & 0 deletions .changeset/20596-plugin-sharing-provenance-anchors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/plugin-sharing': patch
---

Provenance comments in `plugin-sharing` were re-anchored

Comment and docblock lines under `src/` that cited tracker numbers which no
longer resolve on GitHub now cite the commit in this repository's history that
decided the matter, and say in their own words what was decided. Comments
only: no type, schema, export, log or refusal text, or runtime behaviour changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// #14484 — the backfill half of "A: tenant-scoped, writer-repaired, backfilled".
// commit 3f64fe6c6 — the backfill half of "A: tenant-scoped, writer-repaired, backfilled".
//
// These pin the properties the maintainer ruling names, as behaviour rather
// than as prose:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

/**
* backfill-sys-record-share-organizations — the ONE-OFF repair sweep for the
* `sys_record_share` rows the pre-#14484 `SharingService.grant` stranded with
* `sys_record_share` rows that `SharingService.grant`, before commit 3f64fe6c6, stranded with
* no organization.
*
* ## What this repairs, and why it is not optional
*
* #14484 fixed the WRITER: `SharingService.grant` now stamps
* Commit 3f64fe6c6 fixed the WRITER: `SharingService.grant` now stamps
* `organization_id` on every insert and update of `sys_record_share` — a
* rule-materialised grant carries the granting rule's organization, a direct
* grant the shared record's. It wrote nothing to existing rows, and on a WALLED
* deployment that asymmetry is the cliff the card names:
* deployment that asymmetry is the cliff that commit pins:
*
* - the SQL driver's own tenant predicate is NULL-TOLERANT —
* `(organization_id = :tenantId OR organization_id IS NULL)` — so an
Expand All @@ -33,7 +33,7 @@
* The tree's precedents for this shape are
* `plugin-approvals/src/backfill-platform-row-organizations.ts` and
* `service-storage/src/backfill-sys-file-organizations.ts`, and both require a
* MAINTAINER ORDER PER TABLE. The 2026-09-02 ruling on #14484 (decision batch
* MAINTAINER ORDER PER TABLE. The 2026-09-02 ruling commit 3f64fe6c6 applies (decision batch
* #11 item 3, maintainer verbatim 「#13564 转维护者处理;其他同意」 — "其他同意"
* adopts A: tenant-scoped, writer-repaired, existing rows backfilled from the
* record they grant access to) IS that order, and it is the order for
Expand Down Expand Up @@ -121,7 +121,7 @@ import { resolveTenantFieldName } from '@objectstack/objectql';

/**
* The ONE object this sweep repairs. ⛔ Scope-pinned by the 2026-09-02 ruling
* on #14484 — a second table needs its own maintainer order (see the module
* applied by commit 3f64fe6c6 — a second table needs its own maintainer order (see the module
* doc).
*/
export const SYS_RECORD_SHARE_BACKFILL_OBJECT = 'sys_record_share';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* #7136 — compile-time pin for the CONTEXT type this plugin's enforcement
* methods accept.
*
* #6523 converged 36 contract signatures onto the full `ExecutionContext` (the
* #6206 ruling: enforcement adjudicates on the whole `resolveAuthzContext`
* Commit aa4b90d9a converged 36 contract signatures onto the full `ExecutionContext` (the
* full-envelope ruling: enforcement adjudicates on the whole `resolveAuthzContext`
* envelope, never a per-site subset). #7136 is the consumer half — the
* implementations here now annotate their own parameters with that same
* envelope instead of the six-field shape they used to name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,15 @@ export const SysRecordShare = ObjectSchema.create({
//
// ⚠️ ORDERING CONSTRAINT — this id half is `required: true`, and that
// makes it ORDER-DEPENDENT in seeds even though a pointer pair
// contributes no static ordering edge (#11674, measured against the real
// contributes no static ordering edge (commit 1cba33f16, measured against the real
// engine in `packages/objectql/src/engine-seed-required-deferral.test.ts`):
// the seed loader defers an unresolvable reference by DELETING the column
// from the pass-1 insert, required-validation rejects that row, and pass 2
// is then left with no row to back-fill. So the pass-2 healing that makes
// an OPTIONAL id half order-independent (`sys_audit_log`) does not reach
// this one. ⇒ SEED THE TARGET DATASET FIRST. The failure if you do not is
// loud in three places — a write error naming this column, a
// dropped-deferral error, and `success: false` — and since #11674 the
// dropped-deferral error, and `success: false` — and since commit 1cba33f16 the
// loader also WARNS at load time, before the engine rejects the row.
referenceVia: 'object_name',
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,15 @@ export const SysShareLink = ObjectSchema.create({
//
// ⚠️ ORDERING CONSTRAINT — this id half is `required: true`, and that
// makes it ORDER-DEPENDENT in seeds even though a pointer pair
// contributes no static ordering edge (#11674, measured against the real
// contributes no static ordering edge (commit 1cba33f16, measured against the real
// engine in `packages/objectql/src/engine-seed-required-deferral.test.ts`):
// the seed loader defers an unresolvable reference by DELETING the column
// from the pass-1 insert, required-validation rejects that row, and pass 2
// is then left with no row to back-fill. So the pass-2 healing that makes
// an OPTIONAL id half order-independent (`sys_audit_log`) does not reach
// this one. ⇒ SEED THE TARGET DATASET FIRST. The failure if you do not is
// loud in three places — a write error naming this column, a
// dropped-deferral error, and `success: false` — and since #11674 the
// dropped-deferral error, and `success: false` — and since commit 1cba33f16 the
// loader also WARNS at load time, before the engine rejects the row.
referenceVia: 'object_name',
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/plugin-sharing/src/position-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface PositionGraphOptions {
*
* ⚠️ A SECOND implementation of this question lives in `plugin-approvals`
* (`ApprovalService.expandPositionUsers`), and it is deliberately NOT identical
* — do not unify them without reading #8613 / #8710 first. Approval routing is
* — do not unify them without reading #8613 / commit 04d03c3a0 first. Approval routing is
* an ADDRESSING path, so it reads the directory raw and applies no ADR-0091 D2
* window: dropping a lapsed holder there is fail-OPEN (a step routing to
* nobody). Note also that the `sys_position.active` gate for THIS path is not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*
* ## The shape, and which half of it is the security half
*
* After #14484 `sys_record_share` is `tenant-scoped` in the #13491 ledger, so
* After commit 3f64fe6c6 `sys_record_share` is `tenant-scoped` in the #13491 ledger, so
* on a walled install an organization-less system insert on it is refused
* loudly with `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` (#8844).
* `SharingService.grant` resolves the organization on every path that can; a
Expand Down Expand Up @@ -34,7 +34,7 @@
* the deployment posture. A fake engine would have to imitate the very thing
* whose behaviour decides the case. So these cases run a real `SqlDriver` on
* better-sqlite3 `:memory:` behind a real `ObjectQL` on an `isolated` posture,
* the way `record-share-organization-stamp.test.ts` does for the #14484 stamp.
* the way `record-share-organization-stamp.test.ts` does for the commit 3f64fe6c6 stamp.
*
* ## Why the fixture's organization-less record is in the MIDDLE
*
Expand Down Expand Up @@ -347,7 +347,7 @@ describe('[#14754] reconcile: a refused grant is counted and the pass CONTINUES'
const rule = await platformGlobalRule(rules, 'os14754_update_half', 'read');

// A pre-existing rule grant on the organization-less record, carrying no
// organization (which is how it got there before #14484). The pass wants
// organization (which is how it got there before commit 3f64fe6c6). The pass wants
// to raise it to `edit`.
await driver.create('sys_record_share', {
id: 'shr_orgless_old', object_name: OBJECT, record_id: 'rec_orgless', recipient_type: 'user',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* [#14484] Every `sys_record_share` row carries `organization_id` — on a REAL
* [commit 3f64fe6c6] Every `sys_record_share` row carries `organization_id` — on a REAL
* engine over a REAL driver, both write paths, plus the backfill and the
* engine rule the ledger flip switches on.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const DEAL_FIELDS: Record<string, Record<string, unknown>> = {

const SHARE_FIELDS: Record<string, Record<string, unknown>> = {
id: { type: 'text', name: 'id', label: 'Id', primary: true },
// [#14484] The tenant column the registry provisions on every platform
// [commit 3f64fe6c6] The tenant column the registry provisions on every platform
// object (`applySystemFields`); this hand-built table must declare it too,
// now that the writer stamps it — the fixture was never spec-faithful
// without it, the omission just had no reader.
Expand Down
20 changes: 10 additions & 10 deletions packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
* the ablation meaningful: with the new predicate removed, the eligibility
* cases must flip red and these five must stay green.
*
* ## [#13608] The second seam: the same policy, held again at REDEMPTION
* ## [commit fc9ba76a5] The second seam: the same policy, held again at REDEMPTION
*
* Enforcing at mint alone left the adjacent half open, and the state the
* predicate reads is exactly the state an editor changes: publish an article
Expand Down Expand Up @@ -69,7 +69,7 @@ import type { DriverQuery } from '@objectstack/spec/contracts';
import { assertEngineUpdateDispatch, assertEngineFindOnePredicate } from '@objectstack/objectql';
import type { IHttpServer, IHttpRequest, IHttpResponse, RouteHandler } from '@objectstack/spec/contracts';
import { ShareLinkService } from './share-link-service.js';
// [#13608] The PUBLIC seam an anonymous holder actually reaches. The refusal's
// [commit fc9ba76a5] The PUBLIC seam an anonymous holder actually reaches. The refusal's
// shape is a claim about what that caller can observe, so it is measured there
// and not only on the service's return value.
import { registerShareLinkRoutes } from './share-link-routes.js';
Expand Down Expand Up @@ -139,7 +139,7 @@ interface BootOptions {
*/
shapeRow?: (row: any) => any;
/**
* [#13608] Collect the service's server-side log. The redemption refusal is
* [commit fc9ba76a5] Collect the service's server-side log. The redemption refusal is
* deliberately silent on the wire, so this is where the REASON it refused
* becomes assertable — and where the ruling says the reason belongs.
*/
Expand All @@ -165,7 +165,7 @@ async function boot(article: any = ARTICLE, options: BootOptions = {}) {

const schemas: Record<string, any> = { article, sys_share_link: SysShareLink };
/**
* [#13608] Every read the service issues, in order. Two claims are measured
* [commit fc9ba76a5] Every read the service issues, in order. Two claims are measured
* off it: the `id`-only probe is UNCHANGED for an object with no predicate,
* and the eligibility path widens that same read rather than adding a second.
*/
Expand Down Expand Up @@ -595,7 +595,7 @@ describe('[#7861] publicSharing.eligibility is enforced at createLink', () => {


/**
* [#13608] The redemption seam.
* [commit fc9ba76a5] The redemption seam.
*
* `resolveToken` checked `revoked_at`, `expires_at`, the audience gates, the
* password and record EXISTENCE — and served whatever survived, under
Expand Down Expand Up @@ -923,7 +923,7 @@ describe('[#13608] publicSharing.eligibility is enforced again at REDEMPTION', (
* `publicSharing.enabled` governed MINTING only: `getPolicy()` collapsed to an
* empty policy when the block was off, and `resolveToken` read nothing off
* `policy.enabled`. So the platform held this shape — the predicate INSIDE
* the block was re-evaluated at every redemption (#13608, above) while turning
* the block was re-evaluated at every redemption (commit fc9ba76a5, above) while turning
* the ENTIRE block off did not stop a single existing link. Maintainer ruling
* of 2026-09-01 (quoted verbatim in `share-link-service.test.ts`'s reversal
* register): the switch is a standing policy held at every redemption,
Expand Down Expand Up @@ -983,7 +983,7 @@ describe('[#14033] publicSharing.enabled is a standing policy — the switch is
/**
* The HTTP seam, driven end-to-end on the real service through the real
* route, with the route's SECURE default context — every request below is
* anonymous. Same reading as the #13608 pin above, for the same reason: the
* anonymous. Same reading as the commit fc9ba76a5 pin above, for the same reason: the
* switched-off link lands in the generic "invalid / expired / revoked"
* answer, byte-for-byte what a token that NEVER EXISTED gets — not the 410
* bucket, which would confirm the token was real, and not a 422 naming the
Expand Down Expand Up @@ -1268,7 +1268,7 @@ describe('[#14637] the route probe reads the standing policy before it answers f
// Back-dated on the stored row, not minted: `createLink` refuses a past
// `expiresAt` outright (`422 EXPIRY_IN_PAST`), so this is the only way to
// reach an ALREADY-EXPIRED link — and it is exactly what the passage of
// time does to a live one. Same stamp the #13608 pins above use.
// time does to a live one. Same stamp the commit fc9ba76a5 pins above use.
await driver.update('sys_share_link', link.id, {
expires_at: new Date(Date.now() - 60_000).toISOString(),
});
Expand Down Expand Up @@ -1306,7 +1306,7 @@ describe('[#14637] the route probe reads the standing policy before it answers f
});

/**
* [#13608] Mount the real PUBLIC resolve route on the real service.
* [commit fc9ba76a5] Mount the real PUBLIC resolve route on the real service.
*
* Only the verbs `registerShareLinkRoutes` calls are implemented, and the
* SECURE default `contextFromRequest` is deliberately left in place: it reads
Expand All @@ -1318,7 +1318,7 @@ describe('[#14637] the route probe reads the standing policy before it answers f
* the `audience: 'signed_in'` arm from the serving side), and the returned
* driver takes the request query (the only way to reach the `WRONG_PASSWORD`
* arm). Omit both and this is byte-for-byte the anonymous, query-less harness
* the #13608 and #14033 pins above drive.
* the commit fc9ba76a5 and #14033 pins above drive.
*/
function mountResolveRoute(service: ShareLinkService, engine: unknown, signedInUserId?: string) {
const routes = new Map<string, RouteHandler>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* [#6206 / #6430 ruling A] What the share-link routes hand to ENFORCEMENT.
* [commit 8e13ca876 / #6430 ruling A] What the share-link routes hand to ENFORCEMENT.
*
* ## The defect
*
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/plugin-sharing/src/share-link-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export interface ShareLinkRoutesOptions {
* trusted `x-user-id` / `x-tenant-id`, which let a client forge attribution
* and enumerate/revoke other users' links.
*
* [#6206 / #6430] It returns the FULL {@link ExecutionContext} — the whole
* [commit 8e13ca876 / #6430] It returns the FULL {@link ExecutionContext} — the whole
* `resolveAuthzContext` envelope — because this module forwards it unchanged
* into `createLink` / `listLinks` / `revokeLink`, every one of which
* ADJUDICATES access. A resolver that rebuilds a subset here silently changes
Expand All @@ -78,7 +78,7 @@ export interface ShareLinkRoutesOptions {
const defaultContext = (_req: IHttpRequest): ExecutionContext => ({});

/**
* [#6206] The routes' own 401 gate — authenticated vs anonymous, and nothing
* [commit 8e13ca876, full-envelope ruling] The routes' own 401 gate — authenticated vs anonymous, and nothing
* more.
*
* Typed to {@link ShareLinkExecutionContext} deliberately: that is the shape
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ describe('[#13856] declared redactFields survive publicSharing opt-out', () => {
// `publicSharing` block was absent or `enabled !== true`, and nothing in
// `resolveToken()` read `policy.enabled` — the opt-in was checked at MINT only
// (`createLink` → 422 `SHARING_NOT_ENABLED`). So the platform held this shape:
// the predicate INSIDE the block (`eligibility`, #13608) was re-evaluated on
// the predicate INSIDE the block (`eligibility`, commit fc9ba76a5) was re-evaluated on
// every redemption, while turning the WHOLE block off did not stop a single
// link already handed out. An author who wanted anonymous serving to stop had
// to narrow the predicate rather than switch the feature off. Measured before
Expand All @@ -680,7 +680,7 @@ describe('[#13856] declared redactFields survive publicSharing opt-out', () => {
//
// Quoted verbatim in the reversal register above. In one line each: (1) the
// switch is a standing policy, re-read at every redemption; (2) retroactive on
// deploy, as #13608 was; (3) how a link was minted — system context, the
// deploy, as commit fc9ba76a5 was; (3) how a link was minted — system context, the
// `permissive` bypass, ledger row 37 — buys it nothing at redemption; (4)
// switch OFF ⇒ nothing inside the block is evaluated; switch ON ⇒ the sibling
// keys keep their redemption-time behaviour.
Expand All @@ -692,7 +692,7 @@ describe('[#13856] declared redactFields survive publicSharing opt-out', () => {
// read the service issued was the token lookup (no record probe — read off a
// recording engine); and the usage counters did not move. The HTTP-seam
// shape, the server-side log line and the real-driver readings live beside
// the #13608 pins in `share-link-eligibility.test.ts`.
// the commit fc9ba76a5 pins in `share-link-eligibility.test.ts`.
describe('[#14033] publicSharing.enabled is a standing policy — held again at redemption', () => {
/** Every `find` the service issues, so "no record read" is a measurement rather than an assumption. */
function recordingService(engine: any, opts: Record<string, unknown> = {}) {
Expand Down
Loading
Loading