@cotal-ai/core 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/acls.d.ts +31 -7
  2. package/dist/acls.d.ts.map +1 -1
  3. package/dist/acls.js +105 -17
  4. package/dist/acls.js.map +1 -1
  5. package/dist/agent-file.js +1 -1
  6. package/dist/agent-file.js.map +1 -1
  7. package/dist/auth-provider.d.ts +8 -0
  8. package/dist/auth-provider.d.ts.map +1 -1
  9. package/dist/auth-provider.js.map +1 -1
  10. package/dist/backup.d.ts.map +1 -1
  11. package/dist/backup.js +6 -4
  12. package/dist/backup.js.map +1 -1
  13. package/dist/broker-floor.d.ts +34 -0
  14. package/dist/broker-floor.d.ts.map +1 -0
  15. package/dist/broker-floor.js +39 -0
  16. package/dist/broker-floor.js.map +1 -0
  17. package/dist/canonical.d.ts +24 -0
  18. package/dist/canonical.d.ts.map +1 -0
  19. package/dist/canonical.js +159 -0
  20. package/dist/canonical.js.map +1 -0
  21. package/dist/connector.d.ts +4 -0
  22. package/dist/connector.d.ts.map +1 -1
  23. package/dist/endpoint-action.d.ts +424 -0
  24. package/dist/endpoint-action.d.ts.map +1 -0
  25. package/dist/endpoint-action.js +910 -0
  26. package/dist/endpoint-action.js.map +1 -0
  27. package/dist/endpoint-binding.d.ts +344 -0
  28. package/dist/endpoint-binding.d.ts.map +1 -0
  29. package/dist/endpoint-binding.js +753 -0
  30. package/dist/endpoint-binding.js.map +1 -0
  31. package/dist/endpoint-checkpoint.d.ts +292 -0
  32. package/dist/endpoint-checkpoint.d.ts.map +1 -0
  33. package/dist/endpoint-checkpoint.js +818 -0
  34. package/dist/endpoint-checkpoint.js.map +1 -0
  35. package/dist/endpoint-cluster.d.ts +95 -0
  36. package/dist/endpoint-cluster.d.ts.map +1 -0
  37. package/dist/endpoint-cluster.js +194 -0
  38. package/dist/endpoint-cluster.js.map +1 -0
  39. package/dist/endpoint-contract-store.d.ts +90 -0
  40. package/dist/endpoint-contract-store.d.ts.map +1 -0
  41. package/dist/endpoint-contract-store.js +348 -0
  42. package/dist/endpoint-contract-store.js.map +1 -0
  43. package/dist/endpoint-envelope.d.ts +165 -0
  44. package/dist/endpoint-envelope.d.ts.map +1 -0
  45. package/dist/endpoint-envelope.js +346 -0
  46. package/dist/endpoint-envelope.js.map +1 -0
  47. package/dist/endpoint-grants.d.ts +123 -0
  48. package/dist/endpoint-grants.d.ts.map +1 -0
  49. package/dist/endpoint-grants.js +215 -0
  50. package/dist/endpoint-grants.js.map +1 -0
  51. package/dist/endpoint-guard.d.ts +222 -0
  52. package/dist/endpoint-guard.d.ts.map +1 -0
  53. package/dist/endpoint-guard.js +468 -0
  54. package/dist/endpoint-guard.js.map +1 -0
  55. package/dist/endpoint-handle.d.ts +176 -0
  56. package/dist/endpoint-handle.d.ts.map +1 -0
  57. package/dist/endpoint-handle.js +608 -0
  58. package/dist/endpoint-handle.js.map +1 -0
  59. package/dist/endpoint-journal.d.ts +225 -0
  60. package/dist/endpoint-journal.d.ts.map +1 -0
  61. package/dist/endpoint-journal.js +431 -0
  62. package/dist/endpoint-journal.js.map +1 -0
  63. package/dist/endpoint-receipt.d.ts +225 -0
  64. package/dist/endpoint-receipt.d.ts.map +1 -0
  65. package/dist/endpoint-receipt.js +417 -0
  66. package/dist/endpoint-receipt.js.map +1 -0
  67. package/dist/endpoint-records.d.ts +243 -0
  68. package/dist/endpoint-records.d.ts.map +1 -0
  69. package/dist/endpoint-records.js +591 -0
  70. package/dist/endpoint-records.js.map +1 -0
  71. package/dist/endpoint-serve.d.ts +176 -0
  72. package/dist/endpoint-serve.d.ts.map +1 -0
  73. package/dist/endpoint-serve.js +446 -0
  74. package/dist/endpoint-serve.js.map +1 -0
  75. package/dist/endpoint-service.d.ts +518 -0
  76. package/dist/endpoint-service.d.ts.map +1 -0
  77. package/dist/endpoint-service.js +973 -0
  78. package/dist/endpoint-service.js.map +1 -0
  79. package/dist/endpoint-session.d.ts +398 -0
  80. package/dist/endpoint-session.d.ts.map +1 -0
  81. package/dist/endpoint-session.js +834 -0
  82. package/dist/endpoint-session.js.map +1 -0
  83. package/dist/endpoint-signing.d.ts +105 -0
  84. package/dist/endpoint-signing.d.ts.map +1 -0
  85. package/dist/endpoint-signing.js +190 -0
  86. package/dist/endpoint-signing.js.map +1 -0
  87. package/dist/endpoint-subjects.d.ts +222 -0
  88. package/dist/endpoint-subjects.d.ts.map +1 -0
  89. package/dist/endpoint-subjects.js +381 -0
  90. package/dist/endpoint-subjects.js.map +1 -0
  91. package/dist/endpoint-supervisor.d.ts +19 -0
  92. package/dist/endpoint-supervisor.d.ts.map +1 -0
  93. package/dist/endpoint-supervisor.js +12 -0
  94. package/dist/endpoint-supervisor.js.map +1 -0
  95. package/dist/endpoint-traits.d.ts +225 -0
  96. package/dist/endpoint-traits.d.ts.map +1 -0
  97. package/dist/endpoint-traits.js +469 -0
  98. package/dist/endpoint-traits.js.map +1 -0
  99. package/dist/endpoint-verbs.d.ts +277 -0
  100. package/dist/endpoint-verbs.d.ts.map +1 -0
  101. package/dist/endpoint-verbs.js +576 -0
  102. package/dist/endpoint-verbs.js.map +1 -0
  103. package/dist/endpoint-virtual.d.ts +308 -0
  104. package/dist/endpoint-virtual.d.ts.map +1 -0
  105. package/dist/endpoint-virtual.js +440 -0
  106. package/dist/endpoint-virtual.js.map +1 -0
  107. package/dist/endpoint-work.d.ts +304 -0
  108. package/dist/endpoint-work.d.ts.map +1 -0
  109. package/dist/endpoint-work.js +728 -0
  110. package/dist/endpoint-work.js.map +1 -0
  111. package/dist/endpoint.d.ts +66 -22
  112. package/dist/endpoint.d.ts.map +1 -1
  113. package/dist/endpoint.js +289 -89
  114. package/dist/endpoint.js.map +1 -1
  115. package/dist/evict.d.ts +95 -32
  116. package/dist/evict.d.ts.map +1 -1
  117. package/dist/evict.js +193 -3
  118. package/dist/evict.js.map +1 -1
  119. package/dist/index.d.ts +25 -0
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +25 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/lease.d.ts +4 -0
  124. package/dist/lease.d.ts.map +1 -1
  125. package/dist/lease.js.map +1 -1
  126. package/dist/members.d.ts +4 -4
  127. package/dist/members.d.ts.map +1 -1
  128. package/dist/members.js +12 -12
  129. package/dist/members.js.map +1 -1
  130. package/dist/provision.d.ts +70 -18
  131. package/dist/provision.d.ts.map +1 -1
  132. package/dist/provision.js +194 -33
  133. package/dist/provision.js.map +1 -1
  134. package/dist/safe-pattern.d.ts +33 -0
  135. package/dist/safe-pattern.d.ts.map +1 -0
  136. package/dist/safe-pattern.js +486 -0
  137. package/dist/safe-pattern.js.map +1 -0
  138. package/dist/schema-profile.d.ts +93 -0
  139. package/dist/schema-profile.d.ts.map +1 -0
  140. package/dist/schema-profile.js +284 -0
  141. package/dist/schema-profile.js.map +1 -0
  142. package/dist/streams.d.ts +13 -8
  143. package/dist/streams.d.ts.map +1 -1
  144. package/dist/streams.js +35 -18
  145. package/dist/streams.js.map +1 -1
  146. package/dist/subjects.d.ts +100 -40
  147. package/dist/subjects.d.ts.map +1 -1
  148. package/dist/subjects.js +136 -60
  149. package/dist/subjects.js.map +1 -1
  150. package/dist/types.d.ts +9 -0
  151. package/dist/types.d.ts.map +1 -1
  152. package/package.json +3 -1
@@ -0,0 +1,176 @@
1
+ /**
2
+ * v0.4 serve/describe machinery (SPEC §13.5 verbs, §13.7 "Descriptor and describe", §13.2
3
+ * rails) — the request-boundary dispatch an endpoint instance serves its registered commands
4
+ * through: queue-grouped class serving, the scatter and stable-instance rails, contract-
5
+ * digest-bound invoke with MANDATORY runtime schema validation, structural reply derivation,
6
+ * and the reserved authorization-scoped `describe`.
7
+ *
8
+ * Only EPHEMERAL commands are rail-served: journal work rides `epj` submissions into the
9
+ * canonicalizer and executes off the effects/pool durables (§13.4/§13.5), so a journal-class
10
+ * command def REFUSES at construction, and a request DECLARING `class: journal` on a rail is
11
+ * refused at the boundary. Incarnation fencing is not a subscription shape (§13.9: the epoch
12
+ * is deliberately absent from serve subscriptions): the §13.1 takeover barrier fences a
13
+ * superseded subscriber, every reply carries the responder's epoch in its SUBJECT (attributably
14
+ * stale when superseded), and commits are epoch-fenced at the record seam
15
+ * ({@link writeServiceStatus}).
16
+ */
17
+ import type { NatsConnection } from "@nats-io/transport-node";
18
+ import { type EpCaller, type ParsedEpRequest } from "./endpoint-subjects.js";
19
+ import { type EndpointRequest } from "./endpoint-envelope.js";
20
+ import { type CompiledContract } from "./schema-profile.js";
21
+ import { type EpServeGrant } from "./endpoint-service.js";
22
+ import type { DescribeDescriptor } from "./endpoint-cluster.js";
23
+ import { type EpTraitEnforcement } from "./endpoint-traits.js";
24
+ import type { GuardObligation } from "./endpoint-guard.js";
25
+ /** The serving instance's identity: its stable logical instance id and fenced process epoch
26
+ * (§13.1). Both ride every reply SUBJECT (attribution is structural, §13.2). */
27
+ export interface EpServeIdentity {
28
+ endpoint: string;
29
+ instanceId: string;
30
+ epoch: number;
31
+ }
32
+ /** What a handler sees: the broker-authenticated SUBJECT shape (route, caller, target) beside
33
+ * the validated body — provenance never comes from the body (§13.2/§13.3). `obligations` is
34
+ * present exactly when a guard allowed WITH signed attenuations (§13.6), and every entry is
35
+ * VERIFIED by the gate (D28 signature, anchor role/scope, window, space + request binding)
36
+ * before it reaches a handler: the endpoint MUST apply them (monotonic); the applying policy
37
+ * engine is an extension behind the seam. */
38
+ export interface EpServeContext {
39
+ identity: EpServeIdentity;
40
+ subject: ParsedEpRequest;
41
+ request: EndpointRequest;
42
+ obligations?: readonly GuardObligation[];
43
+ }
44
+ /** One served command: its handler plus the COMPILED §13.7 contracts (schema-profile
45
+ * {@link compileContract} — provenance-branded, so a fabricated `{validate, closureDigest}`
46
+ * pair refuses at construction). Everything AUTHORITY-shaped about the command — its class,
47
+ * whether it is targeted and which modes it admits, its schema closure digests — comes from
48
+ * the serve artifact's digest-VERIFIED registered declaration, never from this def: the def
49
+ * only supplies the code, and its compiled contracts must EQUAL the registered digests.
50
+ * `input.validate` gates args before any effect (`bad-request`), `output.validate` gates
51
+ * before the success publish (`internal` — an invalid reply is a server bug, §13.3/§13.7).
52
+ * Runtime validation at the serving boundary is not optional. */
53
+ export interface EpCommandDef {
54
+ command: string;
55
+ contract: {
56
+ input: CompiledContract;
57
+ output: CompiledContract;
58
+ };
59
+ handler: (ctx: EpServeContext) => Promise<unknown> | unknown;
60
+ }
61
+ /** The FRESH target-resolver seam (§13.3/§13.9: targets resolve by `(alias, lifecycleUid)`
62
+ * against the CURRENT mapping immediately before effect; static subject/body agreement is not
63
+ * currency). Returns the alias's current mapping, or `undefined` when the alias has none. The
64
+ * production reader is the D13 lifecycle registry's leader-served mapping read. */
65
+ export type EpTargetResolver = (target: {
66
+ owner: string;
67
+ actor: string;
68
+ }) => Promise<{
69
+ lifecycleUid: string;
70
+ mappingRevision: number;
71
+ } | undefined> | {
72
+ lifecycleUid: string;
73
+ mappingRevision: number;
74
+ } | undefined;
75
+ /** The `child`-mode fresh-authorization seam (§13.2): TRUE iff the DURABLE spawner record of
76
+ * `target` names `caller` as its spawner — read fresh at dispatch, never inferred from the
77
+ * caller's grant alone (the grant pins the owner domain; the spawner relation is per-entity
78
+ * state). The production reader is the D13 lifecycle registry's spawner record. */
79
+ export type EpChildAuthority = (args: {
80
+ caller: EpCaller;
81
+ target: {
82
+ owner: string;
83
+ actor: string;
84
+ lifecycleUid: string;
85
+ };
86
+ }) => Promise<boolean> | boolean;
87
+ /** The `ledger`-mode fresh-authorization seam (§13.2): TRUE iff a FRESH read of the
88
+ * authorization ledger grants `caller` this op on `target`. Fail-closed by construction: no
89
+ * seam, a seam failure, and a false answer all refuse — a ledger row is never cached into a
90
+ * dispatch decision. */
91
+ export type EpLedgerAuthority = (args: {
92
+ caller: EpCaller;
93
+ target: {
94
+ owner: string;
95
+ actor: string;
96
+ lifecycleUid: string;
97
+ };
98
+ op: {
99
+ endpoint: string;
100
+ command: string;
101
+ };
102
+ }) => Promise<boolean> | boolean;
103
+ export interface EpServeHandle {
104
+ /** Drain every serve subscription, then await every in-flight handler: after `stop()`
105
+ * resolves this incarnation performs no further effects and publishes no further replies. */
106
+ stop(): Promise<void>;
107
+ }
108
+ /**
109
+ * Serve an authorized instance's granted commands on the three §13.2 rails, exactly the
110
+ * per-command forms the serve credential grants (§13.9 {@link epServeSubscribeRows}): the class
111
+ * rail queue-qualified under the canonical queue group (`one` = queue-group anycast), the
112
+ * scatter rail plain, and this instance's own `inst` rail.
113
+ *
114
+ * `serve` is the registry-authorized ARTIFACT {@link authorizeServeGrant} returned — the same
115
+ * value the credential minted from. Construction refuses anything else (brand check), refuses
116
+ * a foreign space, and binds every def to the artifact's digest-VERIFIED registered
117
+ * declaration: the def must be a GRANTED command, its provenance-branded compiled contracts
118
+ * must equal the registered schema digests, its class/targeted/modes come from the verified
119
+ * declaration (a journal-class registered command never rail-serves, so it takes no def), and
120
+ * every granted EPHEMERAL command must have a def (a rail nobody serves is a construction bug);
121
+ * journal commands stay in the credential/descriptor surface but ride epj, so a journal-only or
122
+ * mixed endpoint still constructs and serves describe. The reserved `describe` (§13.7: every endpoint MUST serve it) is built
123
+ * HERE over the artifact's DERIVED deep-frozen descriptor — a `describe` def refuses at
124
+ * construction, so the authorization seam cannot be replaced, and no hand-authored or
125
+ * later-mutated descriptor can reach the wire.
126
+ *
127
+ * Boundary discipline per message: subject parse (a non-request subject has no sender and is
128
+ * never handled), body validation with the exact §13.3 catalog codes, body-subject agreement,
129
+ * class match against the REGISTERED class, digest binding, registered admission (targeted
130
+ * commands refuse the untargeted form and vice versa), args schema validation, fresh target
131
+ * currency, the per-mode fresh authorization (`child`/`ledger` seams), then — because those
132
+ * seams await — target currency AGAIN immediately before dispatch (§13.2/§13.3: a mapping
133
+ * rotated during the authority read must fail, never ride a pre-rotation read into the
134
+ * effect), the §13.7 governed pre-effect gate for a command whose REGISTERED declaration
135
+ * carries a governed trait (guard-then-priced, {@link assertGovernedPreEffect}; construction
136
+ * already refused a governed surface/hook gap, so a bypass is structurally impossible), and
137
+ * budgeted output schema validation before the success publish. A call's reply
138
+ * (success OR structured error) is published on the DERIVED reply subject (§13.2: never a
139
+ * body-supplied target); a cast is never replied to, even on error (§13.5: at-most-once, the
140
+ * caller never reads the rail). A request whose body cannot be parsed carries no trustworthy
141
+ * verb; it is answered (the derived subject is nonce-scoped to this caller, and a cast caller
142
+ * simply holds no subscription there). A reply that does not serialize is replaced by a
143
+ * structured `internal` error reply, never dropped.
144
+ */
145
+ export declare function serveEndpoint(nc: NatsConnection, space: string, serve: EpServeGrant, commands: EpCommandDef[], describe: DescribeAuthorization, opts?: {
146
+ resolveTarget?: EpTargetResolver;
147
+ childAuthority?: EpChildAuthority;
148
+ ledgerAuthority?: EpLedgerAuthority;
149
+ /** The §13.9 trait seam: REQUIRED (with the matching hooks) when any granted command's
150
+ * registered declaration carries a governed trait — construction refuses a governed
151
+ * command it cannot enforce, and refuses an extraneous enforcement bundle on an
152
+ * ungoverned surface (fail loud both ways, never a silent no-op). */
153
+ traits?: EpTraitEnforcement;
154
+ }): EpServeHandle;
155
+ /** A caller's authority view from the TRUSTED source (§13.7): the command set this caller may
156
+ * see. `undefined` = no fresh view (stale beyond its bound, or the source has no answer) —
157
+ * describe then fails CLOSED, never answers from a weaker source. */
158
+ export interface DescribeView {
159
+ commands: string[];
160
+ }
161
+ /** The describe authorization seam: either the deployment declared this descriptor PUBLIC (no
162
+ * view is consulted and the answer says so), or a trusted view provider keyed by the
163
+ * broker-authenticated caller identity (§13.7: payload/slot-asserted scope is ignored — it is
164
+ * not even a parameter here). The provider owns its own freshness bound. */
165
+ export type DescribeAuthorization = {
166
+ public: true;
167
+ } | {
168
+ public?: false;
169
+ view: (caller: EpCaller) => Promise<DescribeView | undefined> | DescribeView | undefined;
170
+ };
171
+ /** The describe answer: `public` says which path produced it (§13.7: the answer says so). */
172
+ export interface DescribeAnswer {
173
+ public: boolean;
174
+ descriptor: DescribeDescriptor;
175
+ }
176
+ //# sourceMappingURL=endpoint-serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-serve.d.ts","sourceRoot":"","sources":["../src/endpoint-serve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,446 @@
1
+ import { spacePrefix } from "./subjects.js";
2
+ import { endpointToken, assertCommandToken, assertLifecycleToken, epClassQueueGroup, deriveReplySubject, parseEpSubject, } from "./endpoint-subjects.js";
3
+ import { EpEnvelopeError, parseEndpointRequest, checkRequestSubjectAgreement, assertClassMatches, assertArgsValid, assertOutputValid, } from "./endpoint-envelope.js";
4
+ import { compileContract, assertCompiledContract, VOID_SCHEMA } from "./schema-profile.js";
5
+ import { assertServeGrantAuthorized } from "./endpoint-service.js";
6
+ import { GOVERNED_TRAIT_URNS, TRAIT_GUARDED, TRAIT_PRICED, assertGovernedSurfaceFor, assertGovernedPreEffect, } from "./endpoint-traits.js";
7
+ /** §13.3 target currency at the pre-effect seam, for EVERY body-targeted request (call or
8
+ * cast; a cast has effects too). Fail-closed: no resolver seam means targeted modes are
9
+ * REFUSED (`unavailable`), never dispatched unchecked; a resolver failure is `unavailable`;
10
+ * a missing/superseded mapping, a UID mismatch, or a pinned `mappingRevision` mismatch is
11
+ * `expired` (§13.3). */
12
+ async function assertTargetCurrent(env, resolve) {
13
+ const t = env.target;
14
+ if (t === undefined)
15
+ return;
16
+ if (resolve === undefined)
17
+ throw new EpEnvelopeError("unavailable", "this instance has no trusted target resolver; a targeted request cannot be validated against the current mapping and is refused, never dispatched unchecked (SPEC 13.3/13.9)");
18
+ let mapping;
19
+ try {
20
+ mapping = await resolve({ owner: t.owner, actor: t.actor });
21
+ }
22
+ catch (err) {
23
+ throw new EpEnvelopeError("unavailable", `the trusted target resolver failed; refusing the targeted request (SPEC 13.9): ${err?.message ?? String(err)}`);
24
+ }
25
+ if (mapping === undefined)
26
+ throw new EpEnvelopeError("expired", `target ${t.owner}.${t.actor} has no current lifecycle mapping (SPEC 13.3)`);
27
+ if (mapping.lifecycleUid !== t.lifecycleUid)
28
+ throw new EpEnvelopeError("expired", `target ${t.owner}.${t.actor} expected lifecycle ${t.lifecycleUid} but the current mapping is ${mapping.lifecycleUid} (SPEC 13.3: expired on mapping mismatch)`);
29
+ if (t.mappingRevision !== undefined && mapping.mappingRevision !== t.mappingRevision)
30
+ throw new EpEnvelopeError("expired", `target ${t.owner}.${t.actor} pinned mappingRevision ${t.mappingRevision} but the current mapping is at ${mapping.mappingRevision} (SPEC 13.3: the pin is exact)`);
31
+ }
32
+ /** §13.7 invocation binding at the pre-effect seam. `parseEndpointRequest` already enforced
33
+ * digest PRESENCE for every non-describe command; here the pinned values must EQUAL the served
34
+ * contract. `describe` has no contract to pin (`digests` absent exactly there), so a digest
35
+ * carried on it cannot be honored — `contract-mismatch`, never silently ignored. */
36
+ function bindContract(env, def) {
37
+ if (def.digests === undefined) {
38
+ if (env.op.inputDigest !== undefined || env.op.outputDigest !== undefined)
39
+ throw new EpEnvelopeError("contract-mismatch", "describe pins no contract; a digest carried on it cannot be honored (SPEC 13.7)");
40
+ return;
41
+ }
42
+ if (env.op.inputDigest !== def.digests.input || env.op.outputDigest !== def.digests.output)
43
+ throw new EpEnvelopeError("contract-mismatch", `pinned digests ${env.op.inputDigest}/${env.op.outputDigest} do not match the served contract ${def.digests.input}/${def.digests.output}; a member that cannot honor a pinned digest rejects, never coerces (SPEC 13.7)`);
44
+ }
45
+ /** §13.2/§13.7 registered admission at the pre-effect seam: a command serves ONLY the form its
46
+ * verified registered declaration admits. The subject's mode token is broker-authenticated
47
+ * (the caller's credential pinned it, §13.9), but the GRANT proves what the caller may claim,
48
+ * not what this command supports — default-deny BOTH ways: a targeted command refuses the
49
+ * untargeted form (it would bypass the per-mode fresh authorization entirely), an untargeted
50
+ * command refuses every targeted form, and a targeted request whose mode is not declared is
51
+ * `permission-denied` — all before args validation and before any target resolution. */
52
+ function assertModeAdmitted(parsed, def) {
53
+ if (parsed.target === null) {
54
+ if (def.targeted)
55
+ throw new EpEnvelopeError("permission-denied", `command "${def.command}" is registered TARGETED; the untargeted form is not its surface and would bypass the per-mode fresh authorization (SPEC 13.2/13.7)`);
56
+ return;
57
+ }
58
+ if (!def.targeted)
59
+ throw new EpEnvelopeError("permission-denied", `command "${def.command}" is registered untargeted; a targeted form is not its surface (SPEC 13.2/13.7)`);
60
+ if (!def.targetModes.has(parsed.target.mode))
61
+ throw new EpEnvelopeError("permission-denied", `command "${def.command}" does not admit the "${parsed.target.mode}" authorization mode; a command serves only its registered target modes (SPEC 13.2)`);
62
+ }
63
+ /** §13.2 per-mode FRESH authorization, after target currency: `child` must find the caller in
64
+ * the target's durable spawner record, `ledger` must find a live authorization-ledger grant —
65
+ * both read fresh at dispatch through their seams, both fail CLOSED (`unavailable`) when the
66
+ * seam is absent or fails, `permission-denied` on a false answer. The other modes carry their
67
+ * whole authorization in the minted grant + subject agreement (`self`/`owner`/`handle`) or
68
+ * grant policy alone (`any`) and need no dispatch-time record read. */
69
+ async function assertTargetModeAuthorized(env, parsed, opts) {
70
+ const mode = parsed.target?.mode;
71
+ if (mode !== "child" && mode !== "ledger")
72
+ return;
73
+ const t = env.target; // targeted non-self forms carry a body target (subject agreement, §13.3)
74
+ const target = { owner: t.owner, actor: t.actor, lifecycleUid: t.lifecycleUid };
75
+ const seam = mode === "child" ? opts.childAuthority : opts.ledgerAuthority;
76
+ if (seam === undefined)
77
+ throw new EpEnvelopeError("unavailable", `this instance has no trusted "${mode}"-mode authority seam; a "${mode}"-targeted request cannot be freshly authorized and is refused, never dispatched on the grant alone (SPEC 13.2)`);
78
+ let authorized;
79
+ try {
80
+ authorized = mode === "child"
81
+ ? await opts.childAuthority({ caller: parsed.caller, target })
82
+ : await opts.ledgerAuthority({ caller: parsed.caller, target, op: { endpoint: env.op.endpoint, command: env.op.command } });
83
+ }
84
+ catch (err) {
85
+ throw new EpEnvelopeError("unavailable", `the trusted "${mode}"-mode authority seam failed; refusing the targeted request (SPEC 13.2): ${err?.message ?? String(err)}`);
86
+ }
87
+ if (!authorized)
88
+ throw new EpEnvelopeError("permission-denied", mode === "child"
89
+ ? `the durable spawner record does not name the caller as ${t.owner}.${t.actor}'s spawner (SPEC 13.2: child mode is a fresh spawner check, never the grant alone)`
90
+ : `the authorization ledger holds no live grant for this caller on ${t.owner}.${t.actor} (SPEC 13.2: ledger mode is a fresh ledger read, fail closed)`);
91
+ }
92
+ /**
93
+ * Serve an authorized instance's granted commands on the three §13.2 rails, exactly the
94
+ * per-command forms the serve credential grants (§13.9 {@link epServeSubscribeRows}): the class
95
+ * rail queue-qualified under the canonical queue group (`one` = queue-group anycast), the
96
+ * scatter rail plain, and this instance's own `inst` rail.
97
+ *
98
+ * `serve` is the registry-authorized ARTIFACT {@link authorizeServeGrant} returned — the same
99
+ * value the credential minted from. Construction refuses anything else (brand check), refuses
100
+ * a foreign space, and binds every def to the artifact's digest-VERIFIED registered
101
+ * declaration: the def must be a GRANTED command, its provenance-branded compiled contracts
102
+ * must equal the registered schema digests, its class/targeted/modes come from the verified
103
+ * declaration (a journal-class registered command never rail-serves, so it takes no def), and
104
+ * every granted EPHEMERAL command must have a def (a rail nobody serves is a construction bug);
105
+ * journal commands stay in the credential/descriptor surface but ride epj, so a journal-only or
106
+ * mixed endpoint still constructs and serves describe. The reserved `describe` (§13.7: every endpoint MUST serve it) is built
107
+ * HERE over the artifact's DERIVED deep-frozen descriptor — a `describe` def refuses at
108
+ * construction, so the authorization seam cannot be replaced, and no hand-authored or
109
+ * later-mutated descriptor can reach the wire.
110
+ *
111
+ * Boundary discipline per message: subject parse (a non-request subject has no sender and is
112
+ * never handled), body validation with the exact §13.3 catalog codes, body-subject agreement,
113
+ * class match against the REGISTERED class, digest binding, registered admission (targeted
114
+ * commands refuse the untargeted form and vice versa), args schema validation, fresh target
115
+ * currency, the per-mode fresh authorization (`child`/`ledger` seams), then — because those
116
+ * seams await — target currency AGAIN immediately before dispatch (§13.2/§13.3: a mapping
117
+ * rotated during the authority read must fail, never ride a pre-rotation read into the
118
+ * effect), the §13.7 governed pre-effect gate for a command whose REGISTERED declaration
119
+ * carries a governed trait (guard-then-priced, {@link assertGovernedPreEffect}; construction
120
+ * already refused a governed surface/hook gap, so a bypass is structurally impossible), and
121
+ * budgeted output schema validation before the success publish. A call's reply
122
+ * (success OR structured error) is published on the DERIVED reply subject (§13.2: never a
123
+ * body-supplied target); a cast is never replied to, even on error (§13.5: at-most-once, the
124
+ * caller never reads the rail). A request whose body cannot be parsed carries no trustworthy
125
+ * verb; it is answered (the derived subject is nonce-scoped to this caller, and a cast caller
126
+ * simply holds no subscription there). A reply that does not serialize is replaced by a
127
+ * structured `internal` error reply, never dropped.
128
+ */
129
+ export function serveEndpoint(nc, space, serve, commands, describe, opts = {}) {
130
+ assertServeGrantAuthorized(serve); // §13.9: the serve table consumes ONLY registry-authorized serve authority
131
+ if (serve.space !== space)
132
+ throw new Error(`the serve artifact was authorized for space "${serve.space}", not "${space}" (SPEC 13.9)`);
133
+ const identity = { endpoint: serve.endpoint, instanceId: serve.instanceId, epoch: serve.epoch };
134
+ const e = endpointToken(identity.endpoint);
135
+ const iId = assertLifecycleToken(identity.instanceId, "instanceId");
136
+ const seen = new Set();
137
+ const defs = [];
138
+ for (const def of commands) {
139
+ assertCommandToken(def.command);
140
+ if (def.command === "describe")
141
+ throw new Error("describe is reserved and built from the serve artifact; a custom describe def would bypass the authorization seam (SPEC 13.7)");
142
+ if (seen.has(def.command))
143
+ throw new Error(`command "${def.command}" is served twice`);
144
+ seen.add(def.command);
145
+ const decl = serve.surface[def.command];
146
+ if (decl === undefined)
147
+ throw new Error(`command "${def.command}" is not granted by the serve artifact; the serve table is exactly the granted registered surface (SPEC 13.9)`);
148
+ if (decl.class !== "ephemeral")
149
+ throw new Error(`command "${def.command}" is registered class "${decl.class}": only ephemeral commands are rail-served, so a journal command takes NO rail def; journal work rides epj submissions (SPEC 13.4/13.5)`);
150
+ // §13.7 digest-bound validators: the def carries provenance-BRANDED compiled contracts
151
+ // (a structural {validate, closureDigest} pair refuses — an arbitrary validator cannot
152
+ // wear a registered digest), and their closure digests must EQUAL the verified registered
153
+ // declaration's, so the schema the caller pinned at describe time is the schema this
154
+ // boundary enforces.
155
+ const input = assertCompiledContract(def.contract?.input, `command "${def.command}" input contract`);
156
+ const output = assertCompiledContract(def.contract?.output, `command "${def.command}" output contract`);
157
+ if (input.closureDigest !== decl.inputDigest || output.closureDigest !== decl.outputDigest)
158
+ throw new Error(`command "${def.command}" compiled contracts ${input.closureDigest}/${output.closureDigest} do not equal the registered declaration ${decl.inputDigest}/${decl.outputDigest} (SPEC 13.7: the registered cluster document is the schema authority)`);
159
+ defs.push({
160
+ command: def.command,
161
+ class: decl.class,
162
+ digests: { input: decl.inputDigest, output: decl.outputDigest },
163
+ validate: { args: input.validate, output: output.validate },
164
+ targeted: decl.targeted,
165
+ targetModes: new Set(decl.modes),
166
+ governed: decl.traits.some((t) => GOVERNED_TRAIT_URNS.includes(t)),
167
+ handler: def.handler,
168
+ });
169
+ }
170
+ // Exact coverage applies to the EPHEMERAL (rail-served) subset only: every ephemeral command
171
+ // needs a def (a rail nobody serves is a construction bug), while journal commands stay in the
172
+ // credential/descriptor surface but ride epj submissions, never a rail def — so a journal-only
173
+ // or mixed endpoint constructs and serves its mandatory `describe` (SPEC 13.7), rather than
174
+ // being impossible because the full-surface rule and the journal-rejection rule contradict.
175
+ for (const cmd of serve.commands) {
176
+ const governedUrns = serve.surface[cmd].traits.filter((t) => GOVERNED_TRAIT_URNS.includes(t));
177
+ if (serve.surface[cmd].class !== "ephemeral") {
178
+ // Journal command: no rail def, by design — but a GOVERNED journal command has no
179
+ // enforcement point in this slice (its pre-effect seam is the epj acceptance→effect
180
+ // path), so refusing to construct beats serving governance unenforced (fail closed).
181
+ if (governedUrns.length > 0)
182
+ throw new Error(`journal-class command "${cmd}" declares governed trait(s) ${governedUrns.join(", ")}; the journal-side pre-effect gate is not built in this slice, and a governed command is never served unenforced (SPEC 13.7: fail closed)`);
183
+ continue;
184
+ }
185
+ if (!seen.has(cmd))
186
+ throw new Error(`ephemeral command "${cmd}" has no def in this serve table; the credential subscribes a rail nobody would serve (SPEC 13.9)`);
187
+ }
188
+ // §13.7/§13.9 governed wiring, decided at CONSTRUCTION (a bypass must be structurally
189
+ // impossible, never a first-request surprise): a governed command demands the verified
190
+ // governed surface (branded, bound to exactly THIS grant) plus each trait's hook; an
191
+ // enforcement bundle on an ungoverned surface is a misconfiguration and refuses loudly.
192
+ const governedDefs = defs.filter((d) => d.governed);
193
+ // Capture a CONSTRUCTION-LOCAL enforcement snapshot the dispatch closure binds, so a caller
194
+ // that mutates or swaps `opts.traits` (or its `.governed`) AFTER construction cannot change
195
+ // what the gate enforces — the construction-time validation below is then both necessary AND
196
+ // sufficient (the surface itself is deep-frozen, this closes the whole-object-swap vector).
197
+ let enforcement;
198
+ if (opts.traits === undefined) {
199
+ if (governedDefs.length > 0)
200
+ throw new Error(`command(s) ${governedDefs.map((d) => `"${d.command}"`).join(", ")} declare governed traits and no trait enforcement is wired (opts.traits); missing or unverifiable governed attachments refuse before effect, so construction refuses (SPEC 13.7: fail closed)`);
201
+ }
202
+ else {
203
+ if (governedDefs.length === 0)
204
+ throw new Error("opts.traits is wired but no granted command declares a governed trait; an extraneous enforcement bundle is a misconfiguration, refused loudly rather than silently ignored (SPEC 13.7)");
205
+ // The guard WIRING is snapshot per-field at construction (engineer MEDIUM: the bundle was
206
+ // copied by reference, so a caller swapping `guard.call` after construction would change
207
+ // what the gate enforces at dispatch): single-read the three fields, validate them as
208
+ // functions HERE (never at first request), and freeze a detached bundle. Invoking the
209
+ // captured `now` per request is intentional - only the function REFERENCES are fixed.
210
+ const guardIn = opts.traits.guard;
211
+ let guard;
212
+ if (guardIn !== undefined) {
213
+ const call = guardIn.call, resolveAnchor = guardIn.resolveAnchor, now = guardIn.now;
214
+ if (typeof call !== "function" || typeof resolveAnchor !== "function" || (now !== undefined && typeof now !== "function"))
215
+ throw new Error("opts.traits.guard must wire `call` and `resolveAnchor` functions (plus an optional `now` clock); a garbled guard seam refuses at construction, never as a first-request surprise (SPEC 13.6)");
216
+ guard = Object.freeze({ call, resolveAnchor, ...(now !== undefined ? { now } : {}) });
217
+ }
218
+ enforcement = { governed: opts.traits.governed, guard, verifyPaymentProof: opts.traits.verifyPaymentProof };
219
+ assertGovernedSurfaceFor(enforcement.governed, serve);
220
+ for (const d of governedDefs) {
221
+ const per = enforcement.governed.commands[d.command];
222
+ if (per?.[TRAIT_GUARDED] !== undefined && enforcement.guard === undefined)
223
+ throw new Error(`command "${d.command}" is guarded and no guard seam is wired; an unreachable guard is deny, so construction refuses rather than denying every request (SPEC 13.6)`);
224
+ // priced ⇒ journal-class (§13.10): a priced effect MUST leave a receipt, and a receipt
225
+ // derives from the journaled acceptance fact — the ephemeral rail records no acceptance,
226
+ // so an ephemeral priced command structurally CANNOT satisfy MUST-emit. Every def in this
227
+ // table is rail-served (ephemeral), hence any priced def here refuses at construction.
228
+ if (per?.[TRAIT_PRICED] !== undefined)
229
+ throw new Error(`command "${d.command}" is priced and declared ephemeral; a priced effect MUST leave a receipt, and a receipt derives from the journaled acceptance fact, so priced implies journal-class (SPEC 13.10) - declare the command class journal`);
230
+ }
231
+ }
232
+ defs.push({
233
+ command: "describe",
234
+ class: "ephemeral",
235
+ // describe pins no digests (§13.7) but validates like every command: canonical void args
236
+ // (`bad-request` on any payload, BEFORE the authorization-view lookup) and the declared
237
+ // DescribeAnswer output shape.
238
+ validate: describeValidators(),
239
+ targeted: false, // §13.7: describe is reserved UNTARGETED; every targeted form refuses
240
+ targetModes: new Set(),
241
+ governed: false, // §13.7: describe is the discovery bootstrap; it carries no traits
242
+ handler: describeHandler(serve.descriptor, describe),
243
+ });
244
+ const p = spacePrefix(space);
245
+ const subs = [];
246
+ const pending = new Set();
247
+ const enc = new TextEncoder();
248
+ const dec = new TextDecoder();
249
+ const handle = async (def, msg) => {
250
+ const parsed = parseEpSubject(msg.subject);
251
+ if (!parsed || parsed.plane !== "request")
252
+ return; // no sender: MUST NOT be handled (§13.2)
253
+ let env;
254
+ let reply;
255
+ try {
256
+ env = parseEndpointRequest(JSON.parse(dec.decode(msg.data)));
257
+ checkRequestSubjectAgreement(env, parsed);
258
+ assertClassMatches(env, def.class);
259
+ bindContract(env, def);
260
+ // §13.2: the command admits only its REGISTERED target modes — before args validation,
261
+ // so an unadmitted mode learns nothing about the input contract.
262
+ assertModeAdmitted(parsed, def);
263
+ // §13.7: args validate against the input schema BEFORE any effect (bad-request).
264
+ assertArgsValid(def.validate.args, env.args);
265
+ // §13.3/§13.9: target currency resolves against the FRESH mapping immediately before
266
+ // effect — static agreement is not currency; casts have effects too.
267
+ await assertTargetCurrent(env, opts.resolveTarget);
268
+ // §13.2: per-mode fresh authorization (child spawner check / ledger read), fail closed.
269
+ await assertTargetModeAuthorized(env, parsed, opts);
270
+ // §13.2/§13.3: the child/ledger seams AWAIT — the mapping can rotate (delete/recreate,
271
+ // revision advance) during that read, so currency is re-resolved AFTER dynamic
272
+ // authorization, immediately before the effect; a pre-rotation read never rides into
273
+ // the handler.
274
+ if (parsed.target?.mode === "child" || parsed.target?.mode === "ledger")
275
+ await assertTargetCurrent(env, opts.resolveTarget);
276
+ // §13.7/§13.9 governed pre-effect gate, for calls AND casts (casts have effects too):
277
+ // guard-then-priced, every anomalous answer refuses, both seams bounded. Construction
278
+ // refused a governed def without enforcement, so the snapshot is present and total here.
279
+ let obligations;
280
+ if (def.governed) {
281
+ ({ obligations } = await assertGovernedPreEffect({
282
+ enforcement: enforcement,
283
+ endpoint: identity.endpoint,
284
+ command: def.command,
285
+ caller: parsed.caller,
286
+ requestId: env.id,
287
+ space,
288
+ ...(env.auth !== undefined ? { auth: env.auth } : {}),
289
+ ...(env.deadlineMs !== undefined ? { deadlineMs: env.deadlineMs } : {}),
290
+ }));
291
+ // §13.2/§13.3 TOCTOU: the gate AWAITED guard/proof, so a target mapping or child/ledger
292
+ // grant can have rotated since the last currency read — re-resolve currency, re-run the
293
+ // dynamic authorization, and then (because the child/ledger authorization is ITSELF an
294
+ // await the mapping can rotate during) re-resolve currency a FINAL time immediately
295
+ // before the effect. This is the SAME currency → auth → currency discipline the
296
+ // pre-gate path above follows; currency → auth alone leaves the last await unfenced.
297
+ // A one-use priced proof MAY be consumed before this refusal fires: fail-closed, no
298
+ // effect occurs, and the caller retries with a fresh request id and proof.
299
+ await assertTargetCurrent(env, opts.resolveTarget);
300
+ await assertTargetModeAuthorized(env, parsed, opts);
301
+ if (parsed.target?.mode === "child" || parsed.target?.mode === "ledger")
302
+ await assertTargetCurrent(env, opts.resolveTarget);
303
+ }
304
+ const ctx = { identity, subject: parsed, request: env, ...(obligations !== undefined ? { obligations } : {}) };
305
+ if (!env.replyExpected) {
306
+ await def.handler(ctx);
307
+ return; // cast: the responder MUST NOT reply (§13.5)
308
+ }
309
+ const data = await def.handler(ctx);
310
+ // §13.7: the reply validates against the output schema BEFORE it is published, under the
311
+ // same fixed budget as args — an invalid reply is a server bug and fails loud, never
312
+ // reaches the caller as success.
313
+ assertOutputValid(def.validate.output, data);
314
+ reply = { v: 1, id: env.id, ok: true, ...(data !== undefined ? { data } : {}) };
315
+ }
316
+ catch (err) {
317
+ if (env && !env.replyExpected)
318
+ return; // a failed cast stays silent (§13.5 at-most-once)
319
+ const error = err instanceof EpEnvelopeError
320
+ ? err.toEpError()
321
+ : { code: "internal", message: err?.message ?? String(err) };
322
+ // The id echoes the request where one parsed; the reply subject is already nonce-scoped
323
+ // to exactly this request's caller, so attribution never rides the echo.
324
+ reply = { v: 1, id: env?.id ?? "invalid", ok: false, error };
325
+ }
326
+ let bytes;
327
+ try {
328
+ bytes = enc.encode(JSON.stringify(reply));
329
+ }
330
+ catch (err) {
331
+ // A non-serializable success payload (cycle, BigInt) must not silently drop the reply.
332
+ const fallback = { v: 1, id: reply.id, ok: false, error: { code: "internal", message: `the reply does not serialize: ${err.message}` } };
333
+ bytes = enc.encode(JSON.stringify(fallback));
334
+ }
335
+ nc.publish(deriveReplySubject(space, parsed, identity), bytes);
336
+ };
337
+ for (const def of defs) {
338
+ const cmd = def.command;
339
+ const cb = (_err, msg) => {
340
+ const run = handle(def, msg).catch(() => { });
341
+ pending.add(run);
342
+ void run.finally(() => pending.delete(run));
343
+ };
344
+ subs.push(nc.subscribe(`${p}.ep.one.${e}.${cmd}.>`, { queue: epClassQueueGroup(identity.endpoint), callback: cb }));
345
+ subs.push(nc.subscribe(`${p}.ep.all.${e}.${cmd}.>`, { callback: cb }));
346
+ subs.push(nc.subscribe(`${p}.ep.inst.${e}.${iId}.${cmd}.>`, { callback: cb }));
347
+ }
348
+ return {
349
+ async stop() {
350
+ await Promise.all(subs.map((s) => s.drain())); // no new deliveries
351
+ await Promise.allSettled([...pending]); // in-flight handlers finish before "stopped"
352
+ },
353
+ };
354
+ }
355
+ /** The declared {@link DescribeAnswer} output schema. The answer's own two fields are closed;
356
+ * the descriptor level is deliberately OPEN to additive evolution (§13.7: protocol.v stays 1
357
+ * across additive changes), with the identity, protocol pin, digest grammar, and non-empty
358
+ * per-cluster command lists enforced (an all-filtered cluster leaves the answer, so an empty
359
+ * `commands` never appears; an empty `clusters` array is the valid authorized-but-empty
360
+ * intersection). */
361
+ const DESCRIBE_ANSWER_SCHEMA = {
362
+ type: "object",
363
+ required: ["public", "descriptor"],
364
+ additionalProperties: false,
365
+ properties: {
366
+ public: { type: "boolean" },
367
+ descriptor: {
368
+ type: "object",
369
+ required: ["endpoint", "owner", "protocol", "clusters"],
370
+ properties: {
371
+ endpoint: { type: "string", minLength: 1 },
372
+ owner: { type: "string", minLength: 1 },
373
+ endpointType: { type: "string" },
374
+ protocol: { type: "object", required: ["v"], properties: { v: { const: 1 } } },
375
+ clusters: {
376
+ type: "array",
377
+ items: {
378
+ type: "object",
379
+ required: ["digest", "commands"],
380
+ properties: {
381
+ digest: { type: "string", pattern: "^sha256:[0-9a-f]{64}$" },
382
+ commands: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
383
+ document: { type: "object" },
384
+ },
385
+ },
386
+ },
387
+ },
388
+ },
389
+ },
390
+ };
391
+ /** The reserved describe's compiled validators (§13.7): the canonical VOID input (a describe
392
+ * carrying any args is `bad-request` at the same pre-effect seam as every command, BEFORE the
393
+ * authorization-view lookup) and the declared {@link DescribeAnswer} output. Compiled once,
394
+ * lazily, through the same profile compiler every registered contract rides. */
395
+ let describeValidate;
396
+ function describeValidators() {
397
+ describeValidate ??= {
398
+ args: compileContract({ root: VOID_SCHEMA }).validate,
399
+ output: compileContract({ root: DESCRIBE_ANSWER_SCHEMA }).validate,
400
+ };
401
+ return describeValidate;
402
+ }
403
+ /** The reserved describe handler (internal: {@link serveEndpoint} is the only constructor —
404
+ * §13.7 makes describe mandatory and this seam non-replaceable). The authorization policy is
405
+ * SNAPSHOTTED at construction (the discriminant + the view function), so a later mutation of
406
+ * the caller-supplied `authz` object — flipping `{view}` to `{public:true}` — can never change
407
+ * what a running describe answers; the descriptor itself is deep-frozen at authorization. The
408
+ * scoped answer intersects the descriptor against a FRESH trusted view of the caller's
409
+ * authority; an unavailable or answerless view is `unavailable` (fail closed). An
410
+ * authorized-but-empty intersection is a valid (empty) answer: describe is the default-granted
411
+ * bootstrap (§13.9), and descriptor visibility is never inferred from its reachability alone. */
412
+ function describeHandler(descriptor, authz) {
413
+ const isPublic = authz.public === true;
414
+ // Snapshot the view function once; when not public the union guarantees it is present.
415
+ const viewFn = isPublic ? undefined : authz.view;
416
+ return async (ctx) => {
417
+ if (endpointToken(descriptor.endpoint) !== endpointToken(ctx.identity.endpoint))
418
+ throw new EpEnvelopeError("internal", "describe descriptor does not name the serving endpoint");
419
+ if (isPublic)
420
+ return { public: true, descriptor };
421
+ if (viewFn === undefined)
422
+ throw new EpEnvelopeError("internal", "describe has no snapshotted authorization view but is not public");
423
+ let view;
424
+ try {
425
+ view = await viewFn(ctx.subject.caller);
426
+ }
427
+ catch (err) {
428
+ throw new EpEnvelopeError("unavailable", `the trusted authorization view failed; describe fails closed, never answers from a weaker source (SPEC 13.7): ${err?.message ?? String(err)}`);
429
+ }
430
+ if (view === undefined)
431
+ throw new EpEnvelopeError("unavailable", "no fresh trusted authorization view for this caller; describe fails closed (SPEC 13.7)");
432
+ const allowed = new Set(view.commands);
433
+ const clusters = descriptor.clusters
434
+ .map((c) => {
435
+ const commands = c.commands.filter((cmd) => allowed.has(cmd));
436
+ // The inline document is an OPAQUE cluster artifact core cannot project command-wise:
437
+ // a partial intersection answers by digest only, so a denied command can never leak
438
+ // through the inline copy; the full document rides only a full-cluster authorization.
439
+ const full = commands.length === c.commands.length;
440
+ return { digest: c.digest, commands, ...(full && c.document !== undefined ? { document: c.document } : {}) };
441
+ })
442
+ .filter((c) => c.commands.length > 0);
443
+ return { public: false, descriptor: { ...descriptor, clusters } };
444
+ };
445
+ }
446
+ //# sourceMappingURL=endpoint-serve.js.map