Repository navigation
Commit 9847f87
fix(plugin-sharing): a minted share link carries its organization, never NULL (ADR-0131 D9, C8 stage 8b-0h) (#24461)
Part of #15212
Clause-②: no
C8 stage 8b-0h (claim 6259036738, pointer 6258557956, plan amendment 11
6259028550). `ShareLinkService.createLink` stamps
`sys_share_link.organization_id`, so a minted link is never a NULL row.
#15212 remains open; stage 8b and the rest of C8 are not in this PR.
## What changed
`packages/plugins/plugin-sharing/src/share-link-service.ts`: a new
private `resolveMintOrganization`. It runs after the authority and
eligibility checks and before the insert. In order:
1. **The linked record's organization.** One read under the system
context: `fields: ['id', TENANT_FIELD]`, with the tenant field from
`resolveTenantFieldName`. It takes the system context so that
field-level masking cannot hide the column. The record's organization
wins over the session's.
2. **The acting context's organization** (`tenantId`), when the record
names none: an object with no tenant column, or a row with no
organization.
3. **The ADR-0131 D9 predicate** (`resolveSystemWriteOrganization`, from
`@objectstack/core` through `@objectstack/objectql`'s re-export of the
same binding). Under `single` with exactly one organization, it derives
the Default Organization. Otherwise it refuses with the engine's own
`SystemWriteOrganizationRequiredError`
(`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`, status 500). That covers a
walled posture, and a `single` install with none or several
organizations. Nothing is written.
The stamp rides both the row and the write context (`{ ...SYSTEM_CTX,
tenantId }`), the shape `SharingService.grant` uses for
`sys_record_share`. The mint response is still the contract's
`ShareLink` shape, which declares no organization column. Nothing is
stamped when `sys_share_link` has no tenant column, or when the
composition registers no `sys_organization`; the probe answers `[]`
without a read, like the engine's own probe.
The posture is the **requested** one (`resolveTenancyPosture()`).
`resolveAuthzContext`'s platform-anchor read and the `tenancy` service's
`defaultOrgId()` read the same source in the same fail-stricter
direction: a deployment that asked for a wall stays walled here even
when it runs degraded.
## Why the service decides (not the engine)
`sys_share_link` is `unclassified` in the #13491 ledger, so
`Engine.resolveSystemInsertOrganization` returns early for it
(`isPlatformObjectOutOfTenantAuditScope`, `engine.ts` about `:7183`),
and today the NULL insert goes through. The service raises the engine's
own refusal envelope, so a walled mint gets the same answer before and
after 8b retires that exemption. This PR does not touch the engine or
the ledger.
## Measured (dispatch assumptions)
- **A1 (one writer): holds.** On `538f923cb`, `git grep` for
`insert|create|upsert('sys_share_link'` finds one site:
`share-link-service.ts:665`. `revokeLink` and the usage stamp only
update existing rows.
- **A2 (record read before insert): falsified.** The existing read is
`fields: ['id']` unless an eligibility predicate is declared. It runs
under the caller's context, where field-level masking applies. So the
stage adds the narrowest system read, `['id', TENANT_FIELD]`. A failing
read propagates, so the mint fails and nothing is written. The stage
never stamps the session's organization in place of an unknown record
organization.
- **A3 (cross-organization link): possible under `group`.** Layer 0 is a
membership union there, so a member active in A can mint on a visible
record in B. The `isolated` wall is equality, so both are the same. The
owner and Modify-All alternative is withheld under a wall. A system
caller reads under the system context. In every case the record's
organization is stamped (pin 1).
- **A5 / Clause-②.** No in-repo caller reaches the new refusal. The only
`createLink` callers are the two HTTP doors (`share-link-routes.ts:362`,
`runtime/src/domains/share-links.ts:433`). Both require a `userId`.
Under a wall, an organization-less API key is refused at admission
(`share-link-tenancy-posture-admission.test.ts`). An organization-less
session fails Layer 0 on any tenant-columned record, so the visibility
check refuses first. No in-repo object pairs `publicSharing` with
`tenancy.enabled: false`; the only `publicSharing` object is showcase's
`client-brief`, which is tenant-columned. The refusal is reachable only
by an external system caller that threads no `tenantId`, or by a
third-party organization-less object. So the claim's `Clause-②: no` is
kept as written.
- **A6 (size):** +281 / -1 over 3 files (the changeset, the service and
a new 135-line pin), inside the 0.1–0.3k estimate.
## Tests
- New `share-link-organization-stamp.test.ts`: a real `ObjectQL` and
`SqlDriver` (better-sqlite3 `:memory:`), the real `SysShareLink` object,
and rows read back off the driver. The posture is stubbed through
`OS_TENANCY_POSTURE`.
- (0) The registered `sys_share_link` carries the tenant column.
- (1) Walled: the row gets the record's organization, even when the
caller acts in another.
- (2) Walled: a record with no organization gets the caller's.
- (3) `single`: the Default Organization, and the response has no
`organization_id`.
- (4) Walled with neither: the refusal envelope (`code` and `status`),
and zero rows.
- **Ablations**, through `scripts/ablation-replace.mjs`, on commit
`36706e5e`. Every anchor hit 1 → 0, and every restore showed blob ==
HEAD with an empty `git diff HEAD`. The test imports
`./share-link-service.js` from `src`, so no build or dist step is
involved.
- Drop the record-organization return: only pin 1 goes red (1 failed / 4
passed).
- Drop the session fallback: only pin 2 goes red.
- Drop the `derived` return: only pin 3 goes red.
- Turn the refusal into `return undefined`: only pin 4 goes red.
- `pnpm --filter @objectstack/plugin-sharing test`: 49 files, 1144 tests
pass. `typecheck`: exit 0.
- `pnpm --filter @objectstack/runtime exec vitest run
src/domains/share-links`: 6 files, 202 tests pass. These suites
construct `ShareLinkService`.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 67 commands, plus the 53
artifact-roster families printed on STDERR (118 unique). All of them
were run, and the readings are in the dev report on #15212. The first
run found two real problems, both fixed in `a117f81c`:
- `check-tenant-audit-census`: a conditional insert context counted as
"elevation undecidable". It is now `{ ...SYSTEM_CTX, tenantId }`.
- `check:query-options-erasure`: 5 → 7 because of two `as any` casts on
find options. Both casts are dropped.
The rest exit non-zero only because they cannot measure locally. Four
need PR context (`closing-target-claim`, `partof-closing-keyword`,
`single-claim-paths`, `governed-queue-guard`). One needs an unshallowed
clone (`check-plugin-teardown-shape --self-test`). Three need a full
workspace build (`check:i18n`, `check:dual-build-cjs-loads`,
`check:published-readme-exports`). All of these are NOT MEASURED locally
and are left to CI.
## Acceptance notes
- The `sys_share_link` row in `platform-object-tenancy.ts` is still
`unclassified`. With the writer now stamping, it is ready for 8b, which
owns the engine-side change; that is not done here.
- The `sys_share_link` row in `organization-ownership-inventory.ts`
(`fate: 'attribution'`, subject anchor `object_name` / `record_id`)
already matches this stamp. No change is needed.
- `sharing-plugin.ts` is not touched. The stage needs no new wiring,
because the posture is the requested one and the Default Organization
comes from the same D9 probe the engine uses.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DhtBNRSSLKsbimJ37KcfZy)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ff48407 commit 9847f87
3 files changed
Lines changed: 281 additions & 1 deletion
File tree
- .changeset
- packages/plugins/plugin-sharing/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
47 | 58 | | |
48 | 59 | | |
49 | 60 | | |
| |||
60 | 71 | | |
61 | 72 | | |
62 | 73 | | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
63 | 77 | | |
64 | 78 | | |
65 | 79 | | |
| |||
662 | 676 | | |
663 | 677 | | |
664 | 678 | | |
665 | | - | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
666 | 688 | | |
667 | 689 | | |
668 | 690 | | |
669 | 691 | | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
670 | 803 | | |
671 | 804 | | |
672 | 805 | | |
| |||
0 commit comments