From 1f294f94ae8cdd443088a5a760db95ce16352b77 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 19:22:33 +0000 Subject: [PATCH 1/3] refactor(spec,plugin-sharing,runtime): one canonical `publicSharing.enabled` predicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `isPublicSharingEnabled` is now exported from `@objectstack/spec/data`, declared beside the `publicSharing` block in `src/data/object.zod.ts` (the same shape as the neighbouring `isTenancyDisabled`). `plugin-sharing`'s service and route probe consume it, and `packages/runtime`'s documented private mirror in `src/domains/share-links.ts` is deleted. The mirror's stated justification — importing the plugin would invert the dependency direction, it being a dev dependency of the runtime — held only for that home: both packages already depend on `@objectstack/spec`, so the shared home needed no new edge. Behaviour unchanged, fail-closed included: an absent block, an absent schema and an engine that cannot answer `getSchema` remain one answer, `false`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- ...lic-sharing-enabled-canonical-predicate.md | 13 ++++ .../plugin-sharing/src/share-link-routes.ts | 10 ++- .../plugin-sharing/src/share-link-service.ts | 30 +++----- packages/runtime/src/domains/share-links.ts | 39 +++++----- packages/spec/api-surface/data.json | 1 + packages/spec/export-origins/data.json | 1 + packages/spec/src/data/object.test.ts | 71 ++++++++++++++++++- packages/spec/src/data/object.zod.ts | 33 +++++++++ 8 files changed, 151 insertions(+), 47 deletions(-) create mode 100644 .changeset/public-sharing-enabled-canonical-predicate.md diff --git a/.changeset/public-sharing-enabled-canonical-predicate.md b/.changeset/public-sharing-enabled-canonical-predicate.md new file mode 100644 index 0000000000..17b3856580 --- /dev/null +++ b/.changeset/public-sharing-enabled-canonical-predicate.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": minor +"@objectstack/plugin-sharing": patch +"@objectstack/runtime": patch +--- + +`publicSharing.enabled` now has one canonical predicate, exported from the package that declares the key. + +`isPublicSharingEnabled(schema)` is a new export of `@objectstack/spec/data`, declared in `src/data/object.zod.ts` beside the `publicSharing` block itself — the same shape as the neighbouring `isTenancyDisabled`. It is additive: nothing was removed or narrowed from the spec's public API. + +Until now the same policy read existed in two spellings. `@objectstack/plugin-sharing` defined it (for the share-link service's redemption gate and the route probe above it), and `@objectstack/runtime` carried a documented private mirror for its `/share-links` dispatcher domain — copied rather than imported because the plugin is only a **dev** dependency of the runtime. That reasoning was true of that one home and not of the question: both packages already depend on `@objectstack/spec`, so a shared home existed all along and the de-duplication adds no dependency edge. Both surfaces now consume the exported predicate and the runtime copy is deleted. + +Behaviour is unchanged, fail-closed included: an absent `publicSharing` block, an absent schema, and an engine that cannot answer `getSchema` at all remain **one** answer, `false`, and only the boolean `true` enables. The two pins that held the copies equal — `share-link-eligibility.test.ts` in the plugin and `share-links-enforcement-context.test.ts` in the runtime, which assert the same observable answer on both surfaces rather than trusting the copy — are unchanged and still green; they are what proves the merge did not move behaviour. The predicate's own contract, which those tests can only observe indirectly, is now pinned directly in `packages/spec/src/data/object.test.ts`. diff --git a/packages/plugins/plugin-sharing/src/share-link-routes.ts b/packages/plugins/plugin-sharing/src/share-link-routes.ts index 03632ed773..7110833ad6 100644 --- a/packages/plugins/plugin-sharing/src/share-link-routes.ts +++ b/packages/plugins/plugin-sharing/src/share-link-routes.ts @@ -35,11 +35,15 @@ import type { IHttpServer, IHttpRequest, RouteHandler } from '@objectstack/spec/ import { sendOk, sendError } from '@objectstack/types'; import type { ShareLinkExecutionContext } from '@objectstack/spec/contracts'; import type { ExecutionContext } from '@objectstack/spec/kernel'; -// [#14637] `isPublicSharingEnabled` is the service's OWN reading of the +// [#14637 -> #14935] `isPublicSharingEnabled` is the CANONICAL reading of the // standing switch, imported rather than restated here. A second spelling of // `publicSharing.enabled` at this layer is how the probe below came to -// contradict the gate inside `resolveToken` in the first place. -import { isPublicSharingEnabled, type ShareLinkService } from './share-link-service.js'; +// contradict the gate inside `resolveToken` in the first place. It now comes +// from the package that DECLARES the key, which is the same predicate +// `share-link-service.ts` gates redemption with — one definition, not a +// service-local one this layer re-exports. +import { isPublicSharingEnabled } from '@objectstack/spec/data'; +import { type ShareLinkService } from './share-link-service.js'; import type { SharingEngine } from './sharing-service.js'; const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const; diff --git a/packages/plugins/plugin-sharing/src/share-link-service.ts b/packages/plugins/plugin-sharing/src/share-link-service.ts index 6fbe885c6f..ef7ca45a69 100644 --- a/packages/plugins/plugin-sharing/src/share-link-service.ts +++ b/packages/plugins/plugin-sharing/src/share-link-service.ts @@ -32,6 +32,16 @@ import { ExpressionEngine } from '@objectstack/formula'; // keep a copy. `declared-fields.ts`'s doc comment is the canonical statement of // the rule; this seam defers to it instead of restating it. import { materializeDeclaredFields } from '@objectstack/objectql/core'; +// [#14935] The ONE reading of `publicSharing.enabled`, imported from the +// package that DECLARES the key rather than spelled out again here. This file +// exported its own copy (#14637) and `@objectstack/runtime` kept a documented +// mirror of it, because `@objectstack/plugin-sharing` is only a DEV dependency +// of that package — but both packages already depend on `@objectstack/spec`, +// so the shared home the copy was justified by existed all along. The +// definition is unchanged, fail-closed included: an absent block, an absent +// schema and an engine that cannot answer `getSchema` are one answer, `false` +// — the same definition `getPolicy` below has always used. +import { isPublicSharingEnabled } from '@objectstack/spec/data'; import type { SharingEngine } from './sharing-service.js'; import { deleteRowsForDeletedRecords, @@ -85,26 +95,6 @@ function generateToken(length: number = TOKEN_LENGTH): string { return out; } -/** - * [#14637] Is `publicSharing` switched ON for this object schema? - * - * The ONE reading of the standing switch, exported so the HTTP probe that sits - * ABOVE `resolveToken` asks the same question the gate INSIDE it asks. It was - * a private expression here while the route layer answered from the token row - * with no knowledge of the object's block, which re-opened the existence - * oracle this service's redemption gate closes (maintainer ruling 2026-09-03, - * decision batch #17 item 1, verbatim 「同意」 — option A). - * - * An absent block, an absent schema, and an engine that cannot answer - * `getSchema` at all are one answer: `false`. `enabled` defaults to off, so a - * caller that cannot read the policy must refuse rather than answer from the - * row — the same definition {@link getPolicy} has always used. - */ -export function isPublicSharingEnabled(schema: unknown): boolean { - return (schema as { publicSharing?: { enabled?: unknown } } | null | undefined) - ?.publicSharing?.enabled === true; -} - /** Internal helper — extract publicSharing policy from an object schema. */ function getPolicy(schema: any): { enabled: boolean; diff --git a/packages/runtime/src/domains/share-links.ts b/packages/runtime/src/domains/share-links.ts index 6b14a2e197..b94e7a28d3 100644 --- a/packages/runtime/src/domains/share-links.ts +++ b/packages/runtime/src/domains/share-links.ts @@ -36,33 +36,26 @@ */ import { SHARE_LINK_SERVICE } from '@objectstack/spec/contracts'; +// [#14637 -> #14935] The standing `publicSharing.enabled` switch, read through +// the ONE predicate the package that DECLARES the key exports. This file used +// to carry a documented MIRROR of `isPublicSharingEnabled` from +// `plugin-sharing/src/share-link-service.ts`, copied rather than imported +// because `@objectstack/plugin-sharing` is a **dev** dependency here and +// importing it would invert the dependency direction. That reasoning held only +// for that home: `@objectstack/spec` is a runtime dependency of this package +// AND of the plugin, so moving the predicate beside the schema removes the copy +// without adding an edge. Behaviour is unchanged, fail-closed included — an +// absent block, an absent schema, and an engine that cannot answer `getSchema` +// remain one answer, `false` — and the pins that held the two spellings equal +// (`share-links-enforcement-context.test.ts` here, +// `share-link-eligibility.test.ts` on the other side) are unchanged too: they +// assert the same observable answer on both surfaces, which is what proves the +// de-duplication did not move the behaviour. +import { isPublicSharingEnabled } from '@objectstack/spec/data'; import type { HttpProtocolContext, HttpDispatcherResult } from '../http-dispatcher.js'; import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry.js'; -/** - * [#14637] Is `publicSharing` switched ON for this object schema? - * - * A deliberate MIRROR of `isPublicSharingEnabled` in - * `plugin-sharing/src/share-link-service.ts`, which is the canonical - * definition and the one `resolveToken`'s own gate reads. It is copied rather - * than imported because `@objectstack/plugin-sharing` is a **dev** dependency - * of this package: importing it here would invert the dependency direction to - * make one boolean read shared. The two spellings are held equal by the pins - * in `share-links-enforcement-context.test.ts` on this side and - * `share-link-eligibility.test.ts` on the other, which assert the SAME - * observable answer on both surfaces rather than trusting the copy. - * - * An absent block, an absent schema, and an engine that cannot answer - * `getSchema` at all are one answer: `false`. `enabled` defaults to off, so a - * surface that cannot read the policy must refuse rather than answer from the - * token row. - */ -function isPublicSharingEnabled(schema: unknown): boolean { - return (schema as { publicSharing?: { enabled?: unknown } } | null | undefined) - ?.publicSharing?.enabled === true; -} - export function createShareLinksDomain(deps: DomainHandlerDeps): DomainRoute { return { prefix: '/share-links', diff --git a/packages/spec/api-surface/data.json b/packages/spec/api-surface/data.json index 639140b0e0..b817cdf3e3 100644 --- a/packages/spec/api-surface/data.json +++ b/packages/spec/api-surface/data.json @@ -740,6 +740,7 @@ "isNowDefaultToken (function)", "isOrganizationUnique (function)", "isPlainRecord (function)", + "isPublicSharingEnabled (function)", "isRuntimeDefaultToken (function)", "isTenancyDisabled (function)", "isTextFilterOperator (function)", diff --git a/packages/spec/export-origins/data.json b/packages/spec/export-origins/data.json index 51fa0bb664..624ffba6fa 100644 --- a/packages/spec/export-origins/data.json +++ b/packages/spec/export-origins/data.json @@ -740,6 +740,7 @@ "isNowDefaultToken": "src/data/default-value-tokens.ts#isNowDefaultToken (function)", "isOrganizationUnique": "src/data/field.zod.ts#isOrganizationUnique (function)", "isPlainRecord": "src/data/authoring-key-lint.ts#isPlainRecord (function)", + "isPublicSharingEnabled": "src/data/object.zod.ts#isPublicSharingEnabled (function)", "isRuntimeDefaultToken": "src/data/default-value-tokens.ts#isRuntimeDefaultToken (function)", "isTenancyDisabled": "src/data/object.zod.ts#isTenancyDisabled (function)", "isTextFilterOperator": "src/data/filter-text-operator-declared-type.ts#isTextFilterOperator (function)", diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index 334debc015..82cf719a93 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; // `activities`, … and the annotation stops being a contract check at all. This // only became visible when tsconfig.test.json put these files in front of tsc // (#5286). -import { ObjectSchema, ObjectCapabilities, IndexSchema, ObjectFieldGroupSchema, ObjectExternalBindingSchema, ObjectAccessConfigSchema, LifecycleSchema, TenancyConfigSchema, isTenancyDisabled, resolveCrudAffordances, type ServiceObject } from './object.zod'; +import { ObjectSchema, ObjectCapabilities, IndexSchema, ObjectFieldGroupSchema, ObjectExternalBindingSchema, ObjectAccessConfigSchema, LifecycleSchema, TenancyConfigSchema, isTenancyDisabled, isPublicSharingEnabled, resolveCrudAffordances, type ServiceObject } from './object.zod'; import { resolveInjectedSystemColumns } from './injected-system-columns'; import { Field } from './field.zod'; import type { StateMachineValidation } from './validation.zod'; @@ -2101,6 +2101,75 @@ describe('isTenancyDisabled — platform-global posture predicate (#3249, ADR-00 }); }); +/** + * [#14935] `isPublicSharingEnabled` — the canonical read of the standing + * share-link switch, exported beside the `publicSharing` declaration. + * + * It replaces two spellings: this predicate was private to + * `plugin-sharing/src/share-link-service.ts` (#14637) and `@objectstack/runtime` + * carried a documented MIRROR of it for its `/share-links` dispatcher domain. + * Those two surfaces keep their own behavioural pins — `share-link-eligibility` + * and `share-links-enforcement-context`, which assert the same observable + * answer on both surfaces. What is pinned HERE is the predicate's own contract, + * which those tests can only observe indirectly: fail-CLOSED, with the three + * unreadable cases collapsing to ONE answer. + */ +describe('isPublicSharingEnabled — standing share-link policy predicate (#14935, #14637)', () => { + it('is true only for an explicit publicSharing.enabled === true', () => { + expect(isPublicSharingEnabled({ name: 'article', publicSharing: { enabled: true } })).toBe(true); + expect(isPublicSharingEnabled({ name: 'article', publicSharing: { enabled: false } })).toBe(false); + }); + + it('is false when the block, or the key, is absent — `enabled` defaults to OFF', () => { + expect(isPublicSharingEnabled({ name: 'article', fields: { title: { type: 'text' } } })).toBe(false); + expect(isPublicSharingEnabled({ name: 'article', publicSharing: {} })).toBe(false); + expect(isPublicSharingEnabled({ name: 'article', publicSharing: { allowedAudiences: ['link_only'] } })).toBe(false); + }); + + it('collapses the three unreadable cases to ONE answer, false', () => { + // An absent block, an absent schema, and an engine that cannot answer + // `getSchema` at all (`engine.getSchema?.(name)` -> undefined). A surface + // that cannot read the policy must refuse rather than answer from the + // share-link row: a distinguishable "sharing is off for this object" is an + // existence oracle for a caller holding nothing but a token. + const unreadable = [{ name: 'article' }, undefined, null]; + for (const schema of unreadable) expect(isPublicSharingEnabled(schema)).toBe(false); + expect(new Set(unreadable.map(isPublicSharingEnabled)).size).toBe(1); + }); + + it('refuses a truthy non-boolean — only the boolean true publishes', () => { + // Nothing that reaches this predicate is guaranteed to have been through + // `ObjectSchema`: the runtime probe reads whatever the engine's schema + // registry holds. `=== true` is what keeps a stored `'true'` from + // publishing records. + for (const enabled of ['true', 1, {}, [], 'yes'] as unknown[]) { + expect(isPublicSharingEnabled({ name: 'article', publicSharing: { enabled } })).toBe(false); + } + }); + + it('tolerates null/undefined/non-object schemas', () => { + expect(isPublicSharingEnabled(undefined)).toBe(false); + expect(isPublicSharingEnabled(null)).toBe(false); + expect(isPublicSharingEnabled('article')).toBe(false); + expect(isPublicSharingEnabled(42)).toBe(false); + }); + + it('agrees with the schema it reads — the parsed default is OFF', () => { + const parsed = ObjectSchema.parse({ + name: 'article', + fields: { title: { type: 'text' } }, + publicSharing: { allowedAudiences: ['link_only'] }, + }); + expect(parsed.publicSharing?.enabled).toBe(false); + expect(isPublicSharingEnabled(parsed)).toBe(false); + expect(isPublicSharingEnabled(ObjectSchema.parse({ + name: 'article', + fields: { title: { type: 'text' } }, + publicSharing: { enabled: true }, + }))).toBe(true); + }); +}); + describe('userActions row predicates + resolveCrudAffordances (objectui#2614)', () => { it('accepts the plain boolean form unchanged (back-compat)', () => { const obj = ObjectSchema.parse({ diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 2d83dd0f5c..b13580052b 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -2204,6 +2204,8 @@ const ObjectSchemaBase = strictObject( * provided, the plugin allows `link_only` audience + `view` permission * (the safest combination — caller still needs the URL to access). * + * @see {@link isPublicSharingEnabled} — the ONE reading of `enabled`, + * exported below beside this declaration. * @see packages/plugins/plugin-sharing/src/share-link-service.ts */ publicSharing: strictObject({ @@ -2323,6 +2325,37 @@ const ObjectSchemaBase = strictObject( ...MetadataProtectionFields, }); +/** + * [#14935] Is `publicSharing` switched ON for this object schema? + * + * The ONE reading of the standing switch declared in the `publicSharing` block + * above, exported here beside the declaration so that every surface gating on + * it asks the same question. Two packages read it today — the share-link + * service and the route probe above it (`@objectstack/plugin-sharing`), and the + * `/share-links` dispatcher domain (`@objectstack/runtime`) — and the second + * carried a documented copy of this expression, because the plugin is only a + * DEV dependency of the runtime. That copy was never structurally forced: both + * packages already depend on THIS one, so the shared home existed all along. + * One policy read spelled twice, held equal by a comment and by two pins, is a + * contract defect even while the two spellings agree. + * + * Fail-CLOSED, and the three unreadable cases are ONE answer, `false`: an absent + * `publicSharing` block, an absent schema, and an engine that cannot answer + * `getSchema` at all. `enabled` defaults to off, so a surface that cannot read + * the policy must refuse rather than answer from the share-link row — a + * distinguishable "sharing is off for this object" is an existence oracle for a + * caller holding nothing but a token. Only the boolean `true` enables: the + * strict comparison is deliberate, so a truthy `'true'` or `1` that never went + * through this schema does not publish records. + * + * The same shape as {@link isTenancyDisabled} — an object posture the spec owns + * precisely because more than one package must not re-derive it independently. + */ +export function isPublicSharingEnabled(schema: unknown): boolean { + return (schema as { publicSharing?: { enabled?: unknown } } | null | undefined) + ?.publicSharing?.enabled === true; +} + /** * Converts a snake_case name to a human-readable Title Case label. * @example snakeCaseToLabel('project_task') → 'Project Task' From 4620deb7d9b29045107102fe3ff41857333c9e93 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 20:30:27 +0000 Subject: [PATCH 2/3] docs(permissions): re-anchor row 37's five citations after the -10 line shift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check-system-context-census` red on `1f294f94a`: this PR's edit to `plugin-sharing/src/share-link-service.ts` is +10 above line 45 and -20 above line 108, so every line below shifts by exactly -10, and row 37 of `content/docs/permissions/system-context.mdx` cites five of them by number. All five are PURE LINE ROT, checked per anchor with the file open rather than assumed: the text now at 459 / 513 / 517 / 590 / 620 is byte-identical to the text that was at 469 / 523 / 527 / 600 / 630 at the merge base, and the diff's two hunks both end above line 108 — no line at or below the first anchor was touched at all. Row 37's claim ("share-link policy `enabled` check bypassed; system callers re-enter under a system context") is unchanged and still described by the same five `context.isSystem` reads, so no ledger row with a needle is owed here. Repaired with `node scripts/check-system-context-census.mjs --fix`, which rewrote exactly those five numbers and no prose. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- content/docs/permissions/system-context.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/permissions/system-context.mdx b/content/docs/permissions/system-context.mdx index f2a9a94f18..309fd898db 100644 --- a/content/docs/permissions/system-context.mdx +++ b/content/docs/permissions/system-context.mdx @@ -135,7 +135,7 @@ The largest single consumer — **17 of the 105 sites**. | 34 | `revoke()` deletes directly, **before** the non-manual-source guard | Get: the evaluator can revoke its own grants. Lose: the `CONFLICT` guard that warns a rule-materialised grant will be silently re-granted on the next reconcile | `plugin-sharing/src/sharing-service.ts:1476` (guard at `:1501`) | | 35 | `listShares()` skips the management gate | Get: full enumeration of who can see a record | `plugin-sharing/src/sharing-service.ts:1528` | | 36 | `sys_record_share` reads are **not** self-scoped | Get: tenant-wide share listing without `manage_sharing` | `sharing-plugin.ts:1088` | -| 37 | Share-link policy `enabled` check bypassed; system callers re-enter under a system context | Get: link **creation** while the policy is off — resolution is **not** bypassed since #14033 (`publicSharing.enabled` is a standing policy held at every redemption): a link minted this way does not resolve until the block is enabled | `plugin-sharing/src/share-link-service.ts:469`, `:523`, `:527`, `:600`, `:630` | +| 37 | Share-link policy `enabled` check bypassed; system callers re-enter under a system context | Get: link **creation** while the policy is off — resolution is **not** bypassed since #14033 (`publicSharing.enabled` is a standing policy held at every redemption): a link minted this way does not resolve until the block is enabled | `plugin-sharing/src/share-link-service.ts:459`, `:513`, `:517`, `:590`, `:620` | | 38 | Sharing-rule provenance stamp skipped | Lose: the row is not marked as an admin customization — seeder / `defineRule` / boot reconcilers are "the package door" | `sharing-rule-provenance.ts:47` | | 39 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface, and the platform-global-rule delete guard | `sharing-rule-service.ts:202`, `:427` | From 05fc7cfff31f82616e672a03e7321160e97e1848 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 22:15:39 +0000 Subject: [PATCH 3/3] chore: regenerate the system-context census from the merged tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `content/docs/permissions/system-context.mdx` is an os-regen artifact and both sides moved it since `f7db8f4fd`: this branch re-anchored row 37 after its own -10 line shift, and `origin/main` carried the census rows that #15996's share-link admission seam moved. The driver merges such a path with exit 0 while silently keeping ONE side, so it is regenerated from the merged tree rather than resolved by picking a side or by hand. `pnpm gen:system-context-census` re-derives BOTH sides at once, which is visible in the result: row 37 keeps this branch's anchors (`share-link-service.ts:459`, `:513`, `:517`, `:590`, `:620`) while main's shifts land beside them (`sharing-plugin.ts:1088` becomes `:1189`, `rest-server.ts:1553`/`:1582`/`:1585` become `:1565`/`:1594`/`:1597`, `domains/packages.ts:422` becomes `:535`, and row 50's five `rest-server.ts` anchors move). No prose changed — only anchors. Landed through `scripts/pm/os-regen-merge.sh`: merge first, then regenerate. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- content/docs/permissions/system-context.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/content/docs/permissions/system-context.mdx b/content/docs/permissions/system-context.mdx index 309fd898db..25dae0637f 100644 --- a/content/docs/permissions/system-context.mdx +++ b/content/docs/permissions/system-context.mdx @@ -64,7 +64,7 @@ not on any flag. ## How the flag is set `isSystem` is **server-constructed and never client-supplied**. Inbound HTTP -cannot set it (`packages/rest/src/rest-server.ts:1553`, `:1582`), and neither +cannot set it (`packages/rest/src/rest-server.ts:1565`, `:1594`), and neither can an action body (`packages/runtime/src/domains/actions.ts:414`). It is written by internal callers only, as an option on the engine call: @@ -103,7 +103,7 @@ that silently does not happen. | 14 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `stdio-data-bridge.ts:250` | | 15 | **Read-audit rows are not written** | plugin-audit | Lose: the "a person opened this record" trail. `sudo()` keeps the caller's `userId`, so this flag is the only thing separating a human read from a platform one | `read-audit.ts:556` | | 16 | Approval snapshot payload redaction skipped | plugin-approvals | Get: the whole snapshot on `find` / `findOne` — the audit/replay channel. Lose: field-visibility redaction over approval payloads | `payload-redaction-middleware.ts:115` | -| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1585` | +| 17 | REST anonymous-deny seam satisfied | rest | Get: `enforceAuth` passes with no `userId`. Not reachable from the wire — `isSystem` is never set on an inbound request | `rest-server.ts:1597` | ### 2. Write pipeline and data integrity @@ -134,7 +134,7 @@ The largest single consumer — **17 of the 105 sites**. | 33 | `grant()` skips the enforcement + manage-shares assertions | Get: the rule evaluator can materialise through the public API. Note it is **not** a bare skip: the system branch asserts the grant is not *inert* instead (a grant on an object no verdict can consult is refused) | `plugin-sharing/src/sharing-service.ts:1238` | | 34 | `revoke()` deletes directly, **before** the non-manual-source guard | Get: the evaluator can revoke its own grants. Lose: the `CONFLICT` guard that warns a rule-materialised grant will be silently re-granted on the next reconcile | `plugin-sharing/src/sharing-service.ts:1476` (guard at `:1501`) | | 35 | `listShares()` skips the management gate | Get: full enumeration of who can see a record | `plugin-sharing/src/sharing-service.ts:1528` | -| 36 | `sys_record_share` reads are **not** self-scoped | Get: tenant-wide share listing without `manage_sharing` | `sharing-plugin.ts:1088` | +| 36 | `sys_record_share` reads are **not** self-scoped | Get: tenant-wide share listing without `manage_sharing` | `sharing-plugin.ts:1189` | | 37 | Share-link policy `enabled` check bypassed; system callers re-enter under a system context | Get: link **creation** while the policy is off — resolution is **not** bypassed since #14033 (`publicSharing.enabled` is a standing policy held at every redemption): a link minted this way does not resolve until the block is enabled | `plugin-sharing/src/share-link-service.ts:459`, `:513`, `:517`, `:590`, `:620` | | 38 | Sharing-rule provenance stamp skipped | Lose: the row is not marked as an admin customization — seeder / `defineRule` / boot reconcilers are "the package door" | `sharing-rule-provenance.ts:47` | | 39 | Sharing-rule service write + delete paths return early | Lose: the manage-rules gate on the service surface, and the platform-global-rule delete guard | `sharing-rule-service.ts:202`, `:427` | @@ -158,9 +158,9 @@ The largest single consumer — **17 of the 105 sites**. |:--|:---|:---|:---|:---| | 48 | Object API-exposure gate bypassed (`apiEnabled` / `apiMethods`) | runtime | Get: internal self-writes ignore exposure declarations — these govern **external** exposure, not engine self-writes | `action-execution.ts:138` | | 49 | Action `requiredPermissions` bypassed | runtime | Get: engine self-invocation runs any action | `action-execution.ts:401` | -| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:5101`, `:6527`, `:6775`, `:7206`, `:7399` | +| 50 | `manage_metadata` bypassed on metadata writes | runtime, rest | Get: schema writes without the capability | `domains/meta.ts:471`, `:874`, `rest-server.ts:5145`, `:6571`, `:6819`, `:7250`, `:7443` | | 51 | The shared metadata-write verdict itself returns `allowed` | metadata-core | Get: the one function all of row 50's doors consult answers yes before any capability is examined | `meta-write-capability.ts:134` | -| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:422`, `external-datasource-routes.ts:302`, `package-routes.ts:97` | +| 52 | Anonymous-deny seam satisfied on the domain dispatchers and the package/federation routes | runtime, rest | Get: passes with no `userId` | `domains/actions.ts:421`, `domains/ai.ts:60`, `domains/automation.ts:989`, `domains/meta.ts:232`, `domains/security.ts:78`, `domains/packages.ts:535`, `external-datasource-routes.ts:302`, `package-routes.ts:97` | | 53 | MCP principal check satisfied | runtime | Get: MCP surface reachable with no user | `domains/mcp.ts:61` | | 54 | Package REST route capability gate bypassed | rest | Get: package read/write over REST without `manage_metadata` / `studio.access` / `setup.access` | `package-routes.ts:102` | | 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `domains/packages.ts:241`, `:274` | @@ -199,7 +199,7 @@ assuming `isSystem` covers it is a documented source of bugs. | "It preserves a supplied `updated_at` / `updated_by`" | **No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only | `field.zod.ts:1581` (#3493 / #6640) | | "It stamps `created_by`" | **No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error | `runtime-identity.ts:280`–`281` | | "It bypasses every guard" | **No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one | `last-admin-guard.ts:299` | -| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1553`, `:1582`; `domains/actions.ts:414` | +| "A client can request it" | **No.** Never settable from inbound HTTP or from an action body | `rest-server.ts:1565`, `:1594`; `domains/actions.ts:414` | ---