@kynesyslabs/demosdk 4.0.10 → 4.0.12

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 (87) hide show
  1. package/build/abstraction/Identities.d.ts +70 -0
  2. package/build/abstraction/Identities.js +170 -2
  3. package/build/abstraction/Identities.js.map +1 -1
  4. package/build/identity/cci/claim.d.ts +47 -0
  5. package/build/identity/cci/claim.js +86 -0
  6. package/build/identity/cci/claim.js.map +1 -0
  7. package/build/identity/cci/index.d.ts +11 -0
  8. package/build/identity/cci/index.js +11 -0
  9. package/build/identity/cci/index.js.map +1 -0
  10. package/build/identity/cci/signing.d.ts +44 -0
  11. package/build/identity/cci/signing.js +83 -0
  12. package/build/identity/cci/signing.js.map +1 -0
  13. package/build/identity/cci/types.d.ts +14 -0
  14. package/build/identity/cci/types.js +10 -0
  15. package/build/identity/cci/types.js.map +1 -0
  16. package/build/identity/index.d.ts +2 -0
  17. package/build/identity/index.js +3 -0
  18. package/build/identity/index.js.map +1 -0
  19. package/build/l2ps/anchor/anchor.d.ts +92 -0
  20. package/build/l2ps/anchor/anchor.js +186 -0
  21. package/build/l2ps/anchor/anchor.js.map +1 -0
  22. package/build/l2ps/anchor/index.d.ts +10 -0
  23. package/build/l2ps/anchor/index.js +10 -0
  24. package/build/l2ps/anchor/index.js.map +1 -0
  25. package/build/l2ps/anchor/types.d.ts +36 -0
  26. package/build/l2ps/anchor/types.js +2 -0
  27. package/build/l2ps/anchor/types.js.map +1 -0
  28. package/build/l2ps/binding/binding.d.ts +72 -0
  29. package/build/l2ps/binding/binding.js +160 -0
  30. package/build/l2ps/binding/binding.js.map +1 -0
  31. package/build/l2ps/binding/canonical.d.ts +19 -0
  32. package/build/l2ps/binding/canonical.js +30 -0
  33. package/build/l2ps/binding/canonical.js.map +1 -0
  34. package/build/l2ps/binding/index.d.ts +10 -0
  35. package/build/l2ps/binding/index.js +10 -0
  36. package/build/l2ps/binding/index.js.map +1 -0
  37. package/build/l2ps/binding/types.d.ts +20 -0
  38. package/build/l2ps/binding/types.js +2 -0
  39. package/build/l2ps/binding/types.js.map +1 -0
  40. package/build/l2ps/channel/canonical.d.ts +24 -0
  41. package/build/l2ps/channel/canonical.js +44 -0
  42. package/build/l2ps/channel/canonical.js.map +1 -0
  43. package/build/l2ps/channel/channelIdRegistry.d.ts +19 -0
  44. package/build/l2ps/channel/channelIdRegistry.js +16 -0
  45. package/build/l2ps/channel/channelIdRegistry.js.map +1 -0
  46. package/build/l2ps/channel/envelope.d.ts +26 -0
  47. package/build/l2ps/channel/envelope.js +62 -0
  48. package/build/l2ps/channel/envelope.js.map +1 -0
  49. package/build/l2ps/channel/finalize.d.ts +63 -0
  50. package/build/l2ps/channel/finalize.js +85 -0
  51. package/build/l2ps/channel/finalize.js.map +1 -0
  52. package/build/l2ps/channel/index.d.ts +17 -0
  53. package/build/l2ps/channel/index.js +17 -0
  54. package/build/l2ps/channel/index.js.map +1 -0
  55. package/build/l2ps/channel/negotiate.d.ts +106 -0
  56. package/build/l2ps/channel/negotiate.js +206 -0
  57. package/build/l2ps/channel/negotiate.js.map +1 -0
  58. package/build/l2ps/channel/session.d.ts +66 -0
  59. package/build/l2ps/channel/session.js +101 -0
  60. package/build/l2ps/channel/session.js.map +1 -0
  61. package/build/l2ps/channel/transcript.d.ts +48 -0
  62. package/build/l2ps/channel/transcript.js +120 -0
  63. package/build/l2ps/channel/transcript.js.map +1 -0
  64. package/build/l2ps/channel/transport.d.ts +117 -0
  65. package/build/l2ps/channel/transport.js +178 -0
  66. package/build/l2ps/channel/transport.js.map +1 -0
  67. package/build/l2ps/channel/types.d.ts +55 -0
  68. package/build/l2ps/channel/types.js +2 -0
  69. package/build/l2ps/channel/types.js.map +1 -0
  70. package/build/l2ps/index.d.ts +5 -2
  71. package/build/l2ps/index.js +3 -0
  72. package/build/l2ps/index.js.map +1 -1
  73. package/build/l2ps/l2ps.d.ts +32 -0
  74. package/build/l2ps/l2ps.js +60 -0
  75. package/build/l2ps/l2ps.js.map +1 -1
  76. package/build/l2ps/utils/hex.d.ts +26 -0
  77. package/build/l2ps/utils/hex.js +45 -0
  78. package/build/l2ps/utils/hex.js.map +1 -0
  79. package/build/types/abstraction/index.d.ts +17 -2
  80. package/build/websdk/BroadcastFailedError.js.map +1 -1
  81. package/build/websdk/TransportError.js.map +1 -1
  82. package/build/websdk/Web2Calls.js +2 -0
  83. package/build/websdk/Web2Calls.js.map +1 -1
  84. package/build/websdk/demosclass.d.ts +15 -0
  85. package/build/websdk/demosclass.js +14 -0
  86. package/build/websdk/demosclass.js.map +1 -1
  87. package/package.json +4 -2
@@ -0,0 +1,160 @@
1
+ import { demosAddressFromClaim, isDemosClaim, normalizeDemosAddress, signWithPrimaryClaim, verifyPrimaryClaimSignature, } from "../../identity/cci/index.js";
2
+ import { DemosTransactions } from "../../websdk/DemosTransactions.js";
3
+ import { StorageProgram } from "../../storage/StorageProgram.js";
4
+ import { bindingSigningBytes, signatureFromHex, signatureToHex, stripBindingSignature, } from "./canonical.js";
5
+ /**
6
+ * Deterministic Storage Program name for `(channelId, subnetMemberId)`.
7
+ * Public so `resolveMember` can re-derive it; included verbatim in tests.
8
+ *
9
+ * Components are percent-encoded so the layout stays one-to-one. Without
10
+ * this guard, the pairs `("a:b", "c")` and `("a", "b:c")` would collide on
11
+ * the same SP name and `resolveMember` could be tricked into returning a
12
+ * binding from one channel/member context for a lookup of another.
13
+ */
14
+ export function bindingProgramName(channelId, subnetMemberId) {
15
+ return `l2ps-binding:${encodeURIComponent(channelId)}:${encodeURIComponent(subnetMemberId)}`;
16
+ }
17
+ /**
18
+ * Brief calls subnetMemberId "the member's L2PS/RSA identity within the
19
+ * subnet". L2PS already publishes a stable fingerprint of the RSA key —
20
+ * we reuse that so the value is derivable on both sides without a separate
21
+ * registry.
22
+ */
23
+ export async function subnetMemberIdFromL2PS(l2ps) {
24
+ return await l2ps.getKeyFingerprint();
25
+ }
26
+ /**
27
+ * Build and sign a binding. The signature is produced by the key
28
+ * controlling `claim`. Throws if `claim`'s scheme is not `demos:` or if
29
+ * the controlling address does not match the connected wallet — the
30
+ * invariant that ties channel signatures to on-chain identity.
31
+ */
32
+ export async function createMembershipBinding(opts) {
33
+ const { channelId, subnetMemberId, claim, demos } = opts;
34
+ if (!channelId)
35
+ throw new Error("createMembershipBinding: channelId required");
36
+ if (!subnetMemberId)
37
+ throw new Error("createMembershipBinding: subnetMemberId required");
38
+ if (!isDemosClaim(claim))
39
+ throw new Error(`createMembershipBinding: claim must be a "demos:..." ClaimReference, got "${claim}"`);
40
+ const boundAt = opts.boundAt ?? Date.now();
41
+ if (!Number.isSafeInteger(boundAt) || boundAt < 0)
42
+ throw new Error("createMembershipBinding: boundAt must be a non-negative safe-integer unix-ms timestamp");
43
+ const unsigned = {
44
+ bindingVersion: "1",
45
+ channelId,
46
+ subnetMemberId,
47
+ cciPrimaryClaim: claim,
48
+ boundAt,
49
+ };
50
+ const payload = bindingSigningBytes(unsigned);
51
+ const sig = await signWithPrimaryClaim(claim, payload, demos);
52
+ return { ...unsigned, signature: signatureToHex(sig) };
53
+ }
54
+ /**
55
+ * Pure signature check — no chain access. Verifies the embedded signature
56
+ * against the Demos public key encoded in `cciPrimaryClaim`.
57
+ *
58
+ * Returns `false` (not throws) on any structural problem so callers can
59
+ * use it as a filter when iterating SP candidates in `resolveMember`.
60
+ */
61
+ export function verifyMembershipBinding(binding) {
62
+ if (binding?.bindingVersion !== "1")
63
+ return false;
64
+ if (!binding.channelId || !binding.subnetMemberId)
65
+ return false;
66
+ if (!isDemosClaim(binding.cciPrimaryClaim))
67
+ return false;
68
+ if (!Number.isSafeInteger(binding.boundAt) ||
69
+ binding.boundAt < 0)
70
+ return false;
71
+ if (typeof binding.signature !== "string" || !binding.signature)
72
+ return false;
73
+ try {
74
+ const payload = bindingSigningBytes(stripBindingSignature(binding));
75
+ const sig = signatureFromHex(binding.signature);
76
+ return verifyPrimaryClaimSignature(binding.cciPrimaryClaim, payload, sig);
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ /**
83
+ * Anchor a signed binding to chain via SR-2 (Storage Program). The SP is
84
+ * deployed under `bindingProgramName(...)` so `resolveMember` can find it
85
+ * by name. The deployer (= connected Demos wallet) becomes the SP owner;
86
+ * that owner check is what makes the resolver safe against impostor SPs
87
+ * published under the same name.
88
+ *
89
+ * Returns once the broadcast succeeds. Callers that need on-chain finality
90
+ * should poll the returned tx hash before opening the channel.
91
+ */
92
+ export async function anchorMembershipBinding(binding, demos) {
93
+ if (!verifyMembershipBinding(binding))
94
+ throw new Error("anchorMembershipBinding: refusing to anchor an unverifiable binding");
95
+ const claimAddress = demosAddressFromClaim(binding.cciPrimaryClaim);
96
+ const connected = normalizeDemosAddress(await demos.getEd25519Address());
97
+ if (claimAddress !== connected)
98
+ throw new Error(`anchorMembershipBinding: claim "${binding.cciPrimaryClaim}" does not match connected wallet ${connected}`);
99
+ const nonce = (await demos.getAddressNonce(connected)) + 1;
100
+ const payload = StorageProgram.createStorageProgram(connected, bindingProgramName(binding.channelId, binding.subnetMemberId), binding, "json", { mode: "public" }, { nonce });
101
+ const tx = DemosTransactions.empty();
102
+ tx.content.to = payload.storageAddress;
103
+ tx.content.nonce = nonce;
104
+ tx.content.amount = 0;
105
+ tx.content.type = "storageProgram";
106
+ tx.content.timestamp = Date.now();
107
+ tx.content.data = ["storageProgram", payload];
108
+ const signed = await demos.sign(tx);
109
+ const validity = await demos.confirm(signed);
110
+ await demos.broadcast(validity);
111
+ return { storageAddress: payload.storageAddress, txHash: signed.hash };
112
+ }
113
+ /**
114
+ * Find the bound `ClaimReference` for `(channelId, subnetMemberId)`.
115
+ *
116
+ * Two-stage check on every candidate Storage Program:
117
+ * 1. Embedded signature verifies under the embedded claim's key.
118
+ * 2. SP owner's Demos address matches that claim's address — so only
119
+ * the actual key-holder could have deployed this SP under this name.
120
+ *
121
+ * Both checks must pass. Returns `null` when no candidate qualifies.
122
+ * Membership is fixed for channel lifetime (CH-1) so callers SHOULD cache
123
+ * the result for the session.
124
+ */
125
+ export async function resolveMember(channelId, subnetMemberId, rpcUrl) {
126
+ const name = bindingProgramName(channelId, subnetMemberId);
127
+ const list = await StorageProgram.searchByName(rpcUrl, name, {
128
+ exactMatch: true,
129
+ });
130
+ for (const item of list) {
131
+ const sp = await StorageProgram.getByAddress(rpcUrl, item.storageAddress);
132
+ if (sp?.encoding !== "json" || !sp.data)
133
+ continue;
134
+ if (typeof sp.data !== "object")
135
+ continue;
136
+ const binding = sp.data;
137
+ if (binding.channelId !== channelId)
138
+ continue;
139
+ if (binding.subnetMemberId !== subnetMemberId)
140
+ continue;
141
+ if (!verifyMembershipBinding(binding))
142
+ continue;
143
+ // Both parses are inside the same try so a single adversarially-
144
+ // crafted candidate (malformed claim ref OR malformed sp.owner)
145
+ // gets skipped instead of aborting the whole resolution loop —
146
+ // otherwise a squatter publishing a poisoned binding under our
147
+ // deterministic name could deny-of-service every consumer.
148
+ try {
149
+ const claimAddress = demosAddressFromClaim(binding.cciPrimaryClaim);
150
+ if (normalizeDemosAddress(sp.owner) !== claimAddress)
151
+ continue;
152
+ }
153
+ catch {
154
+ continue;
155
+ }
156
+ return binding.cciPrimaryClaim;
157
+ }
158
+ return null;
159
+ }
160
+ //# sourceMappingURL=binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../../../../src/l2ps/binding/binding.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { signatureFromHex, signatureToHex } from "../utils/hex";
2
+ import type { L2PSMembershipBinding, UnsignedL2PSMembershipBinding } from "./types";
3
+ /**
4
+ * Domain prefix for binding signatures. The brief specifies prefixes for
5
+ * channelmsg and transcript but not for the binding itself; per §5 of the
6
+ * brief, all signatures in this family must be domain-separated, so we
7
+ * align with the established `dacs-*:v1:` pattern.
8
+ */
9
+ export declare const BINDING_DOMAIN_PREFIX = "dacs-binding:v1:";
10
+ /**
11
+ * Bytes that the binding signature covers:
12
+ * `dacs-binding:v1:` || sha256(JCS(binding_without_signature))
13
+ *
14
+ * Domain separation prevents this signature from being lifted into a
15
+ * channelmsg or transcript context (or vice versa).
16
+ */
17
+ export declare function bindingSigningBytes(unsigned: UnsignedL2PSMembershipBinding): Uint8Array;
18
+ export declare function stripBindingSignature(b: L2PSMembershipBinding): UnsignedL2PSMembershipBinding;
19
+ export { signatureFromHex, signatureToHex };
@@ -0,0 +1,30 @@
1
+ import { sha256 } from "@noble/hashes/sha2";
2
+ import { canonicalJSONStringify } from "../../websdk/utils/canonicalJson.js";
3
+ import { bytesToHex, signatureFromHex, signatureToHex } from "../utils/hex.js";
4
+ /**
5
+ * Domain prefix for binding signatures. The brief specifies prefixes for
6
+ * channelmsg and transcript but not for the binding itself; per §5 of the
7
+ * brief, all signatures in this family must be domain-separated, so we
8
+ * align with the established `dacs-*:v1:` pattern.
9
+ */
10
+ export const BINDING_DOMAIN_PREFIX = "dacs-binding:v1:";
11
+ /**
12
+ * Bytes that the binding signature covers:
13
+ * `dacs-binding:v1:` || sha256(JCS(binding_without_signature))
14
+ *
15
+ * Domain separation prevents this signature from being lifted into a
16
+ * channelmsg or transcript context (or vice versa).
17
+ */
18
+ export function bindingSigningBytes(unsigned) {
19
+ const canonical = canonicalJSONStringify(unsigned);
20
+ const digestHex = bytesToHex(sha256(new TextEncoder().encode(canonical)));
21
+ return new TextEncoder().encode(BINDING_DOMAIN_PREFIX + digestHex);
22
+ }
23
+ export function stripBindingSignature(b) {
24
+ const { signature: _signature, ...rest } = b;
25
+ return rest;
26
+ }
27
+ // Re-exported from the shared utility so existing imports of
28
+ // `signatureFromHex` / `signatureToHex` from this module keep working.
29
+ export { signatureFromHex, signatureToHex };
30
+ //# sourceMappingURL=canonical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.js","sourceRoot":"","sources":["../../../../src/l2ps/binding/canonical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AACrE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAM3E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAAkB,CAAA;AAEvD;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAC/B,QAAuC;IAEvC,MAAM,SAAS,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IAClD,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;IACzE,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,qBAAqB,GAAG,SAAS,CAAC,CAAA;AACtE,CAAC;AAED,MAAM,UAAU,qBAAqB,CACjC,CAAwB;IAExB,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAA;IAC5C,OAAO,IAAI,CAAA;AACf,CAAC;AAED,6DAA6D;AAC7D,uEAAuE;AACvE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * L2PS membership binding — DACS-3 §8.3.2 (SR-4 WI-1).
3
+ *
4
+ * Anchors `(channelId, subnetMemberId) -> CCI ClaimReference` proofs to
5
+ * chain via SR-2 Storage Programs so channel signatures can be verified
6
+ * against the same identity used in DACS-2.
7
+ */
8
+ export { BINDING_DOMAIN_PREFIX, bindingSigningBytes, signatureFromHex, signatureToHex, stripBindingSignature, } from "./canonical";
9
+ export type { L2PSMembershipBinding, UnsignedL2PSMembershipBinding, } from "./types";
10
+ export { anchorMembershipBinding, bindingProgramName, createMembershipBinding, resolveMember, subnetMemberIdFromL2PS, verifyMembershipBinding, type AnchorMembershipBindingResult, type CreateMembershipBindingOpts, } from "./binding";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * L2PS membership binding — DACS-3 §8.3.2 (SR-4 WI-1).
3
+ *
4
+ * Anchors `(channelId, subnetMemberId) -> CCI ClaimReference` proofs to
5
+ * chain via SR-2 Storage Programs so channel signatures can be verified
6
+ * against the same identity used in DACS-2.
7
+ */
8
+ export { BINDING_DOMAIN_PREFIX, bindingSigningBytes, signatureFromHex, signatureToHex, stripBindingSignature, } from "./canonical.js";
9
+ export { anchorMembershipBinding, bindingProgramName, createMembershipBinding, resolveMember, subnetMemberIdFromL2PS, verifyMembershipBinding, } from "./binding.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/l2ps/binding/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACH,qBAAqB,EACrB,mBAAmB,EACnB,gBAAgB,EAChB,cAAc,EACd,qBAAqB,GACxB,MAAM,aAAa,CAAA;AAOpB,OAAO,EACH,uBAAuB,EACvB,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACb,sBAAsB,EACtB,uBAAuB,GAG1B,MAAM,WAAW,CAAA"}
@@ -0,0 +1,20 @@
1
+ import type { ClaimReference } from "../../identity/cci";
2
+ /**
3
+ * DACS-3 §8.3.2 interim membership binding.
4
+ *
5
+ * Until L2PS is natively CCI-keyed, each subnet member publishes one of
6
+ * these so channel signatures can be tied back to the same identity used
7
+ * in DACS-2. The signature MUST be produced by the key controlling
8
+ * `cciPrimaryClaim` — on Demos, the Demos Ed25519 key (NOT the RSA subnet
9
+ * key the L2PS class is configured with).
10
+ */
11
+ export interface L2PSMembershipBinding {
12
+ bindingVersion: "1";
13
+ channelId: string;
14
+ subnetMemberId: string;
15
+ cciPrimaryClaim: ClaimReference;
16
+ boundAt: number;
17
+ /** Hex (0x-prefixed) Ed25519 signature over `bindingSigningBytes`. */
18
+ signature: string;
19
+ }
20
+ export type UnsignedL2PSMembershipBinding = Omit<L2PSMembershipBinding, "signature">;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/l2ps/binding/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import { signatureFromHex, signatureToHex } from "../utils/hex";
2
+ import type { ChannelMessage, ChannelTranscript, UnsignedChannelMessage, UnsignedChannelTranscript } from "./types";
3
+ /**
4
+ * Domain separation prefixes — verbatim from the brief (§2 WI-2, WI-3) and
5
+ * the spec references at §B.7. A signature lifted from one domain (e.g.
6
+ * channelmsg) fails verification in any other (e.g. transcript / binding).
7
+ */
8
+ export declare const CHANNEL_MESSAGE_DOMAIN_PREFIX = "dacs-channelmsg:v1:";
9
+ export declare const TRANSCRIPT_DOMAIN_PREFIX = "dacs-transcript:v1:";
10
+ /**
11
+ * Hex digest of `JCS(envelope_without_signature)`. Exposed as a primitive
12
+ * so auditors can re-derive the hash independently from the wire envelope.
13
+ */
14
+ export declare function envelopeHashHex(unsigned: UnsignedChannelMessage): string;
15
+ /**
16
+ * Bytes the channel-message signature covers:
17
+ * `dacs-channelmsg:v1:` || envelope_hash_hex
18
+ */
19
+ export declare function channelMessageSigningBytes(unsigned: UnsignedChannelMessage): Uint8Array;
20
+ export declare function stripChannelMessageSignature(msg: ChannelMessage): UnsignedChannelMessage;
21
+ export declare function transcriptHashHex(unsigned: UnsignedChannelTranscript): string;
22
+ export declare function transcriptSigningBytes(unsigned: UnsignedChannelTranscript): Uint8Array;
23
+ export declare function stripTranscriptSignatures(t: ChannelTranscript): UnsignedChannelTranscript;
24
+ export { signatureFromHex, signatureToHex };
@@ -0,0 +1,44 @@
1
+ import { sha256 } from "@noble/hashes/sha2";
2
+ import { canonicalJSONStringify } from "../../websdk/utils/canonicalJson.js";
3
+ import { bytesToHex, signatureFromHex, signatureToHex } from "../utils/hex.js";
4
+ /**
5
+ * Domain separation prefixes — verbatim from the brief (§2 WI-2, WI-3) and
6
+ * the spec references at §B.7. A signature lifted from one domain (e.g.
7
+ * channelmsg) fails verification in any other (e.g. transcript / binding).
8
+ */
9
+ export const CHANNEL_MESSAGE_DOMAIN_PREFIX = "dacs-channelmsg:v1:";
10
+ export const TRANSCRIPT_DOMAIN_PREFIX = "dacs-transcript:v1:";
11
+ /**
12
+ * Hex digest of `JCS(envelope_without_signature)`. Exposed as a primitive
13
+ * so auditors can re-derive the hash independently from the wire envelope.
14
+ */
15
+ export function envelopeHashHex(unsigned) {
16
+ const canonical = canonicalJSONStringify(unsigned);
17
+ return bytesToHex(sha256(new TextEncoder().encode(canonical)));
18
+ }
19
+ /**
20
+ * Bytes the channel-message signature covers:
21
+ * `dacs-channelmsg:v1:` || envelope_hash_hex
22
+ */
23
+ export function channelMessageSigningBytes(unsigned) {
24
+ return new TextEncoder().encode(CHANNEL_MESSAGE_DOMAIN_PREFIX + envelopeHashHex(unsigned));
25
+ }
26
+ export function stripChannelMessageSignature(msg) {
27
+ const { signature: _signature, ...rest } = msg;
28
+ return rest;
29
+ }
30
+ export function transcriptHashHex(unsigned) {
31
+ const canonical = canonicalJSONStringify(unsigned);
32
+ return bytesToHex(sha256(new TextEncoder().encode(canonical)));
33
+ }
34
+ export function transcriptSigningBytes(unsigned) {
35
+ return new TextEncoder().encode(TRANSCRIPT_DOMAIN_PREFIX + transcriptHashHex(unsigned));
36
+ }
37
+ export function stripTranscriptSignatures(t) {
38
+ const { signatures: _signatures, ...rest } = t;
39
+ return rest;
40
+ }
41
+ // Re-exported from the shared utility so existing imports of
42
+ // `signatureFromHex` / `signatureToHex` from this module keep working.
43
+ export { signatureFromHex, signatureToHex };
44
+ //# sourceMappingURL=canonical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.js","sourceRoot":"","sources":["../../../../src/l2ps/channel/canonical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAA;AACrE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAQ3E;;;;GAIG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,qBAAqB,CAAA;AAClE,MAAM,CAAC,MAAM,wBAAwB,GAAG,qBAAqB,CAAA;AAE7D;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgC;IAC5D,MAAM,SAAS,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IAClD,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAClE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CACtC,QAAgC;IAEhC,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAC3B,6BAA6B,GAAG,eAAe,CAAC,QAAQ,CAAC,CAC5D,CAAA;AACL,CAAC;AAED,MAAM,UAAU,4BAA4B,CACxC,GAAmB;IAEnB,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,CAAA;IAC9C,OAAO,IAAI,CAAA;AACf,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC7B,QAAmC;IAEnC,MAAM,SAAS,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IAClD,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;AAClE,CAAC;AAED,MAAM,UAAU,sBAAsB,CAClC,QAAmC;IAEnC,OAAO,IAAI,WAAW,EAAE,CAAC,MAAM,CAC3B,wBAAwB,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CACzD,CAAA;AACL,CAAC;AAED,MAAM,UAAU,yBAAyB,CACrC,CAAoB;IAEpB,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAA;IAC9C,OAAO,IAAI,CAAA;AACf,CAAC;AAED,6DAA6D;AAC7D,uEAAuE;AACvE,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * CH-6 per-session channelId uniqueness — pluggable persistence.
3
+ *
4
+ * The substrate (L2PS subnet UID) already produces a unique value per
5
+ * session in honest deployments, but the brief requires explicit rejection
6
+ * of any session reusing a prior `channelId`. SDK ships an in-memory
7
+ * default; production callers wire their own store (durable DB, chain
8
+ * lookup, app-side persistence).
9
+ */
10
+ export interface ChannelIdRegistry {
11
+ /** Throws if `channelId` has been registered before. */
12
+ register(channelId: string): Promise<void>;
13
+ has(channelId: string): Promise<boolean>;
14
+ }
15
+ export declare class InMemoryChannelIdRegistry implements ChannelIdRegistry {
16
+ private readonly seen;
17
+ register(channelId: string): Promise<void>;
18
+ has(channelId: string): Promise<boolean>;
19
+ }
@@ -0,0 +1,16 @@
1
+ export class InMemoryChannelIdRegistry {
2
+ constructor() {
3
+ this.seen = new Set();
4
+ }
5
+ async register(channelId) {
6
+ if (!channelId)
7
+ throw new Error("ChannelIdRegistry: channelId required");
8
+ if (this.seen.has(channelId))
9
+ throw new Error(`ChannelIdRegistry: channelId "${channelId}" already registered (CH-6 violation)`);
10
+ this.seen.add(channelId);
11
+ }
12
+ async has(channelId) {
13
+ return this.seen.has(channelId);
14
+ }
15
+ }
16
+ //# sourceMappingURL=channelIdRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channelIdRegistry.js","sourceRoot":"","sources":["../../../../src/l2ps/channel/channelIdRegistry.ts"],"names":[],"mappings":"AAeA,MAAM,OAAO,yBAAyB;IAAtC;QACqB,SAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAc7C,CAAC;IAZG,KAAK,CAAC,QAAQ,CAAC,SAAiB;QAC5B,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAA;QACxE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;YACxB,MAAM,IAAI,KAAK,CACX,iCAAiC,SAAS,uCAAuC,CACpF,CAAA;QACL,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC5B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,SAAiB;QACvB,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACnC,CAAC;CACJ"}
@@ -0,0 +1,26 @@
1
+ import { type ClaimReference } from "../../identity/cci";
2
+ import { Demos } from "../../websdk/demosclass";
3
+ import type { ChannelMessage, UnsignedChannelMessage } from "./types";
4
+ /**
5
+ * Sign a `ChannelMessage` envelope with the sender's primary-claim key.
6
+ *
7
+ * The signature covers `dacs-channelmsg:v1:` || sha256(JCS(envelope w/o
8
+ * signature)). Transport-level wrappers MUST NOT alter the bytes returned
9
+ * here. Throws if `unsigned.sender` is not a demos: ClaimReference or
10
+ * does not match the connected wallet — preserves the
11
+ * in-channel-signer == on-chain-party invariant.
12
+ */
13
+ export declare function signChannelMessage(unsigned: UnsignedChannelMessage, demos: Demos): Promise<ChannelMessage>;
14
+ /**
15
+ * Pure signature check: verifies `msg.signature` covers JCS(msg without
16
+ * signature) with the `dacs-channelmsg:v1:` prefix under the key encoded
17
+ * in `msg.sender`. Membership (`sender ∈ members`) and sequence enforcement
18
+ * are handled by `ChannelSession` — this function does crypto only.
19
+ */
20
+ export declare function verifyChannelMessage(msg: ChannelMessage): boolean;
21
+ /**
22
+ * Pure membership check. Kept separate so `verifyChannelMessage` can be
23
+ * called in contexts where the member set hasn't been resolved yet (e.g.
24
+ * audit replay from a transcript).
25
+ */
26
+ export declare function isSenderInMembers(msg: ChannelMessage, members: Iterable<ClaimReference>): boolean;
@@ -0,0 +1,62 @@
1
+ import { isDemosClaim, signWithPrimaryClaim, verifyPrimaryClaimSignature, } from "../../identity/cci/index.js";
2
+ import { channelMessageSigningBytes, signatureFromHex, signatureToHex, stripChannelMessageSignature, } from "./canonical.js";
3
+ /**
4
+ * Sign a `ChannelMessage` envelope with the sender's primary-claim key.
5
+ *
6
+ * The signature covers `dacs-channelmsg:v1:` || sha256(JCS(envelope w/o
7
+ * signature)). Transport-level wrappers MUST NOT alter the bytes returned
8
+ * here. Throws if `unsigned.sender` is not a demos: ClaimReference or
9
+ * does not match the connected wallet — preserves the
10
+ * in-channel-signer == on-chain-party invariant.
11
+ */
12
+ export async function signChannelMessage(unsigned, demos) {
13
+ if (!isDemosClaim(unsigned.sender))
14
+ throw new Error(`signChannelMessage: sender must be a demos: ClaimReference, got "${unsigned.sender}"`);
15
+ if (!Number.isInteger(unsigned.sequence) || unsigned.sequence < 1)
16
+ throw new Error(`signChannelMessage: sequence must be a positive integer (got ${unsigned.sequence})`);
17
+ if (!unsigned.channelId)
18
+ throw new Error("signChannelMessage: channelId required");
19
+ const payload = channelMessageSigningBytes(unsigned);
20
+ const sigBytes = await signWithPrimaryClaim(unsigned.sender, payload, demos);
21
+ const signature = {
22
+ sigVersion: "1",
23
+ signature: signatureToHex(sigBytes),
24
+ };
25
+ return { ...unsigned, signature };
26
+ }
27
+ /**
28
+ * Pure signature check: verifies `msg.signature` covers JCS(msg without
29
+ * signature) with the `dacs-channelmsg:v1:` prefix under the key encoded
30
+ * in `msg.sender`. Membership (`sender ∈ members`) and sequence enforcement
31
+ * are handled by `ChannelSession` — this function does crypto only.
32
+ */
33
+ export function verifyChannelMessage(msg) {
34
+ if (msg?.signature?.sigVersion !== "1")
35
+ return false;
36
+ if (!isDemosClaim(msg.sender))
37
+ return false;
38
+ if (!Number.isInteger(msg.sequence) || msg.sequence < 1)
39
+ return false;
40
+ if (!msg.channelId)
41
+ return false;
42
+ try {
43
+ const payload = channelMessageSigningBytes(stripChannelMessageSignature(msg));
44
+ const sig = signatureFromHex(msg.signature.signature);
45
+ return verifyPrimaryClaimSignature(msg.sender, payload, sig);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ /**
52
+ * Pure membership check. Kept separate so `verifyChannelMessage` can be
53
+ * called in contexts where the member set hasn't been resolved yet (e.g.
54
+ * audit replay from a transcript).
55
+ */
56
+ export function isSenderInMembers(msg, members) {
57
+ for (const m of members)
58
+ if (m === msg.sender)
59
+ return true;
60
+ return false;
61
+ }
62
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../../src/l2ps/channel/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,YAAY,EACZ,oBAAoB,EACpB,2BAA2B,GAE9B,MAAM,gBAAgB,CAAA;AAOvB,OAAO,EACH,0BAA0B,EAC1B,gBAAgB,EAChB,cAAc,EACd,4BAA4B,GAC/B,MAAM,aAAa,CAAA;AAEpB;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACpC,QAAgC,EAChC,KAAY;IAEZ,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC9B,MAAM,IAAI,KAAK,CACX,oEAAoE,QAAQ,CAAC,MAAM,GAAG,CACzF,CAAA;IACL,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,GAAG,CAAC;QAC7D,MAAM,IAAI,KAAK,CACX,gEAAgE,QAAQ,CAAC,QAAQ,GAAG,CACvF,CAAA;IACL,IAAI,CAAC,QAAQ,CAAC,SAAS;QACnB,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAA;IAE7D,MAAM,OAAO,GAAG,0BAA0B,CAAC,QAAQ,CAAC,CAAA;IACpD,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;IAC5E,MAAM,SAAS,GAA4B;QACvC,UAAU,EAAE,GAAG;QACf,SAAS,EAAE,cAAc,CAAC,QAAQ,CAAC;KACtC,CAAA;IACD,OAAO,EAAE,GAAG,QAAQ,EAAE,SAAS,EAAE,CAAA;AACrC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAmB;IACpD,IAAI,GAAG,EAAE,SAAS,EAAE,UAAU,KAAK,GAAG;QAAE,OAAO,KAAK,CAAA;IACpD,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,QAAQ,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACrE,IAAI,CAAC,GAAG,CAAC,SAAS;QAAE,OAAO,KAAK,CAAA;IAEhC,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,0BAA0B,CACtC,4BAA4B,CAAC,GAAG,CAAC,CACpC,CAAA;QACD,MAAM,GAAG,GAAG,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;QACrD,OAAO,2BAA2B,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,CAAA;IAChE,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAA;IAChB,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC7B,GAAmB,EACnB,OAAiC;IAEjC,KAAK,MAAM,CAAC,IAAI,OAAO;QAAE,IAAI,CAAC,KAAK,GAAG,CAAC,MAAM;YAAE,OAAO,IAAI,CAAA;IAC1D,OAAO,KAAK,CAAA;AAChB,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SR-4 WI-C — finalise a negotiate-rfq session: on agreement, export the
3
+ * signed channel transcript and (per disclosure policy, DACS-3 §8.7)
4
+ * anchor an encrypted copy, returning the `channelTranscriptRef` that the
5
+ * negotiate-rfq output carries.
6
+ *
7
+ * This is the orchestration that ties WI-B's terminal state to WI-3's
8
+ * transcript anchor: `exportTranscript` (sign the ordered messages) →
9
+ * `anchorEncryptedTranscript` (encrypt-to-members + SR-2 anchor). Policy
10
+ * semantics (§8.7):
11
+ * - `none` — transcript stays local; nothing anchored.
12
+ * - `encrypted-anchored-recommended` — anchor only on explicit consent.
13
+ * - `encrypted-anchored-required` — MUST anchor; a null result fails
14
+ * the phase.
15
+ *
16
+ * The anchor call is injectable so the orchestration is unit-tested
17
+ * without a live node (the real anchor deploys an SR-2 storage program).
18
+ */
19
+ import type { Demos } from "../../websdk";
20
+ import type { ClaimReference } from "../../identity/cci";
21
+ import type L2PS from "../l2ps";
22
+ import type { AnchorEncryptedTranscriptOpts, AttestationRef, TranscriptDisclosurePolicy } from "../anchor";
23
+ import type { ChannelMessage, ChannelTranscript } from "./types";
24
+ import type { RfqOutcome, RfqState } from "./negotiate";
25
+ /** Minimal RfqSession surface — structural for testability. */
26
+ export interface RfqLike {
27
+ readonly state: RfqState;
28
+ outcome(): RfqOutcome;
29
+ }
30
+ /** Minimal ChannelSession surface — structural for testability. */
31
+ export interface ChannelSessionView {
32
+ readonly channelId: string;
33
+ readonly members: ReadonlyArray<ClaimReference>;
34
+ messages(): ReadonlyArray<ChannelMessage>;
35
+ }
36
+ export interface FinalizeRfqOpts {
37
+ rfq: RfqLike;
38
+ session: ChannelSessionView;
39
+ /** This party's CCI claim (signs the transcript); must be a member. */
40
+ signer: ClaimReference;
41
+ demos: Demos;
42
+ /** Required when the policy actually anchors (not for `none`). */
43
+ l2ps?: L2PS;
44
+ policy: TranscriptDisclosurePolicy;
45
+ /** Needed to anchor under `encrypted-anchored-recommended`. */
46
+ consent?: boolean;
47
+ /**
48
+ * Injectable anchor implementation; defaults to the real
49
+ * `anchorEncryptedTranscript`. Tests pass a fake to avoid deploying
50
+ * a storage program.
51
+ */
52
+ anchor?: (opts: AnchorEncryptedTranscriptOpts) => Promise<AttestationRef | null>;
53
+ }
54
+ export interface RfqFinalizeResult {
55
+ /** The signed, ordered transcript (always produced). */
56
+ transcript: ChannelTranscript;
57
+ /**
58
+ * The anchored attestation reference, or null when the policy did not
59
+ * anchor (`none`, or `recommended` without consent).
60
+ */
61
+ channelTranscriptRef: AttestationRef | null;
62
+ }
63
+ export declare function finalizeRfq(opts: FinalizeRfqOpts): Promise<RfqFinalizeResult>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * SR-4 WI-C — finalise a negotiate-rfq session: on agreement, export the
3
+ * signed channel transcript and (per disclosure policy, DACS-3 §8.7)
4
+ * anchor an encrypted copy, returning the `channelTranscriptRef` that the
5
+ * negotiate-rfq output carries.
6
+ *
7
+ * This is the orchestration that ties WI-B's terminal state to WI-3's
8
+ * transcript anchor: `exportTranscript` (sign the ordered messages) →
9
+ * `anchorEncryptedTranscript` (encrypt-to-members + SR-2 anchor). Policy
10
+ * semantics (§8.7):
11
+ * - `none` — transcript stays local; nothing anchored.
12
+ * - `encrypted-anchored-recommended` — anchor only on explicit consent.
13
+ * - `encrypted-anchored-required` — MUST anchor; a null result fails
14
+ * the phase.
15
+ *
16
+ * The anchor call is injectable so the orchestration is unit-tested
17
+ * without a live node (the real anchor deploys an SR-2 storage program).
18
+ */
19
+ import { anchorEncryptedTranscript } from "../anchor/index.js";
20
+ import { exportTranscript } from "./transcript.js";
21
+ export async function finalizeRfq(opts) {
22
+ if (opts.rfq.state !== "accepted") {
23
+ throw new Error(`finalizeRfq: negotiation is "${opts.rfq.state}", not "accepted" — ` +
24
+ "only an agreed RFQ produces a transcript anchor");
25
+ }
26
+ // The signer must be a channel member — `exportTranscript` will sign
27
+ // with any demos claim, so a non-member signer produces a transcript
28
+ // that later fails `verifyTranscript` (signer ∉ members). Catch it
29
+ // here rather than emitting a transcript that cannot be verified.
30
+ if (!opts.session.members.includes(opts.signer)) {
31
+ throw new Error(`finalizeRfq: signer "${opts.signer}" is not a channel member — ` +
32
+ "the transcript signature would fail verification");
33
+ }
34
+ // The transcript must actually contain the agreed exchange: the
35
+ // accepted proposal and the matching accept that `rfq.outcome()`
36
+ // points at. A stale or mismatched session would otherwise let us
37
+ // export/anchor a transcript that does not back the agreement.
38
+ const outcome = opts.rfq.outcome();
39
+ const acceptedSequence = outcome.acceptedSequence;
40
+ if (acceptedSequence === undefined) {
41
+ throw new Error("finalizeRfq: accepted outcome has no acceptedSequence — cannot match transcript");
42
+ }
43
+ const messages = opts.session.messages();
44
+ const hasAcceptedProposal = messages.some(m => m.sequence === acceptedSequence);
45
+ const hasAccept = messages.some(m => m.type === "accept" &&
46
+ m.body
47
+ ?.acceptedSequence === acceptedSequence);
48
+ if (!hasAcceptedProposal || !hasAccept) {
49
+ throw new Error(`finalizeRfq: session transcript does not contain the accepted proposal ` +
50
+ `(seq ${acceptedSequence}) and its matching accept — session mismatch`);
51
+ }
52
+ const transcript = await exportTranscript({
53
+ channelId: opts.session.channelId,
54
+ members: [...opts.session.members],
55
+ messages,
56
+ signers: [{ claim: opts.signer, demos: opts.demos }],
57
+ });
58
+ // §8.7 `none`: transcript stays local; nothing anchored.
59
+ // §8.7 `recommended` WITHOUT consent: also a no-anchor outcome — it
60
+ // must NOT require an L2PS instance just to take its null-ref branch.
61
+ const willAnchor = opts.policy === "encrypted-anchored-required" ||
62
+ (opts.policy === "encrypted-anchored-recommended" && !!opts.consent);
63
+ if (!willAnchor) {
64
+ return { transcript, channelTranscriptRef: null };
65
+ }
66
+ if (!opts.l2ps) {
67
+ throw new Error(`finalizeRfq: policy "${opts.policy}" requires an L2PS instance to encrypt the transcript`);
68
+ }
69
+ const anchorFn = opts.anchor ?? anchorEncryptedTranscript;
70
+ const ref = await anchorFn({
71
+ transcript,
72
+ l2ps: opts.l2ps,
73
+ demos: opts.demos,
74
+ signer: opts.signer,
75
+ policy: opts.policy,
76
+ consent: opts.consent,
77
+ });
78
+ // §8.7: under `required`, the absence of an anchor fails the phase.
79
+ if (opts.policy === "encrypted-anchored-required" && !ref) {
80
+ throw new Error("finalizeRfq: policy is 'encrypted-anchored-required' but anchoring " +
81
+ "produced no attestation — phase fails");
82
+ }
83
+ return { transcript, channelTranscriptRef: ref };
84
+ }
85
+ //# sourceMappingURL=finalize.js.map