Skip to content

Commit 050e8ed

Browse files
committed
wip(spec,plugin-sharing): retire the record-share guest recipient (ADR-0138 D8)
Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ Co-authored-by: Claude <noreply@anthropic.com>
1 parent c056979 commit 050e8ed

3 files changed

Lines changed: 45 additions & 5 deletions

File tree

‎packages/plugins/plugin-sharing/src/objects/sys-record-share.object.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,12 @@ export const SysRecordShare = ObjectSchema.create({
172172

173173
// ── Recipient (who receives access) ──────────────────────────
174174
recipient_type: Field.select(
175-
['user', 'group', 'position', 'unit_and_subordinates', 'guest'],
175+
// `guest` was removed (ADR-0138 D8): a share to the anonymous principal
176+
// was declared here and honoured nowhere — `grant()` refused it and no
177+
// gate reads such a row — since guest access flows through the guest
178+
// position and share links only (ADR-0090 D11). Held equal to
179+
// `RecordShareRecipientType` in `@objectstack/spec/contracts`.
180+
['user', 'group', 'position', 'unit_and_subordinates'],
176181
{
177182
label: 'Recipient Type',
178183
required: true,

‎packages/plugins/plugin-sharing/src/sharing-service.ts‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,27 @@ function makeShareId(): string {
8080
/** System-elevated context for the plugin's own queries / mutations. */
8181
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;
8282

83+
/**
84+
* [ADR-0138 D8] Recipient values `RecordShareRecipientType` no longer declares,
85+
* each with the refusal {@link SharingService.grant} answers when one still
86+
* arrives at runtime — a REST body, an untyped caller. A retired value is told
87+
* where its access actually lives; every other non-`user` value keeps the
88+
* generic refusal in `grant()`. Keyed by the value exactly as a caller sends it.
89+
*/
90+
const RETIRED_RECIPIENT_TYPE_REFUSALS: Readonly<Record<string, string>> = {
91+
guest:
92+
"recipientType 'guest' was removed (ADR-0138 D8) — a record share to the anonymous principal "
93+
+ 'was never honoured, because guest access flows through the guest position and share links '
94+
+ '(ADR-0090 D11). Expose the record through a public form or a share link instead.',
95+
};
96+
97+
/** The retired-value refusal for a `recipientType` a caller sent, or `undefined`. */
98+
function retiredRecipientTypeRefusal(value: unknown): string | undefined {
99+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(RETIRED_RECIPIENT_TYPE_REFUSALS, value)
100+
? RETIRED_RECIPIENT_TYPE_REFUSALS[value]
101+
: undefined;
102+
}
103+
83104
/**
84105
* Owner field convention. Hard-coded to `owner_id` for MVP — the
85106
* sharing model in Salesforce / ServiceNow / Dynamics all assume a
@@ -1346,9 +1367,15 @@ export class SharingService implements ISharingService {
13461367
// (ADR-0078). Group / business-unit principals are delivered via sharing
13471368
// rules, whose evaluator expands them into per-user rows.
13481369
if (recipientType !== 'user') {
1370+
// [ADR-0138 D8] A retired value no longer typechecks, yet still reaches
1371+
// here untyped. It is told where guest access lives, never that a sharing
1372+
// rule delivers it: the rule vocabulary retired the same recipient.
1373+
const retired = retiredRecipientTypeRefusal(recipientType);
13491374
throw new Error(
1350-
`VALIDATION_FAILED: recipientType must be 'user' (received ${JSON.stringify(recipientType)}) — `
1351-
+ `group/position recipients are delivered via sharing rules (ADR-0111 D7)`,
1375+
retired !== undefined
1376+
? `VALIDATION_FAILED: ${retired}`
1377+
: `VALIDATION_FAILED: recipientType must be 'user' (received ${JSON.stringify(recipientType)}) — `
1378+
+ `group/position recipients are delivered via sharing rules (ADR-0111 D7)`,
13521379
);
13531380
}
13541381
// Validate BEFORE any write. Previously anything at all was persisted

‎packages/spec/src/contracts/sharing-service.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ import type { ExecutionContext } from '../kernel/execution-context.zod.js';
8787
* as expanded `user` rows. The remaining members are persisted-for-forward-
8888
* compatibility vocabulary, kept in lockstep with the select.
8989
*
90+
* `guest` is NOT a member: it was removed (ADR-0138 D8). A record share to the
91+
* anonymous principal was declared and never honoured — the grant refused it,
92+
* and no gate reads such a row — because guest access flows through the guest
93+
* position and share links only (ADR-0090 D11). A record a guest must reach is
94+
* exposed through a public form or a share link. {@link ISharingService.grant}
95+
* refuses a `'guest'` that still arrives at runtime with that prescription, and
96+
* the select refuses it on a row write. The sharing-RULE vocabulary retired the
97+
* same recipient earlier (the ADR-0087 entry `sharing-rule-recipient-reconcile`).
98+
*
9099
* NOT the sharing-RULE recipient vocabulary — that is
91100
* {@link SharingRuleRecipientType} here, whose authorable subset is
92101
* `ShareRecipientType` in `spec/security` (a different concept that shares
@@ -96,8 +105,7 @@ export type RecordShareRecipientType =
96105
| 'user'
97106
| 'group'
98107
| 'position'
99-
| 'unit_and_subordinates'
100-
| 'guest';
108+
| 'unit_and_subordinates';
101109

102110
/**
103111
* Access level on a single record — the authorable subset, mirroring

0 commit comments

Comments
 (0)