@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,120 @@
1
+ import { isDemosClaim, signWithPrimaryClaim, verifyPrimaryClaimSignature, } from "../../identity/cci/index.js";
2
+ import { signatureFromHex, signatureToHex, stripTranscriptSignatures, transcriptSigningBytes, } from "./canonical.js";
3
+ import { isSenderInMembers, verifyChannelMessage } from "./envelope.js";
4
+ /**
5
+ * Build the unsigned transcript shell. Messages must already be in
6
+ * sequence order — typically `session.messages()`.
7
+ */
8
+ export function buildUnsignedTranscript(opts) {
9
+ return {
10
+ transcriptVersion: "1",
11
+ channelId: opts.channelId,
12
+ members: [...opts.members],
13
+ messages: [...opts.messages],
14
+ generatedAt: opts.generatedAt ?? Date.now(),
15
+ };
16
+ }
17
+ /**
18
+ * Produce one party's `TranscriptSignature` over the unsigned transcript.
19
+ * Domain-separated under `dacs-transcript:v1:` — cannot be confused with
20
+ * a channel-message or binding signature.
21
+ */
22
+ export async function signTranscript(unsigned, signer, demos) {
23
+ if (!isDemosClaim(signer))
24
+ throw new Error(`signTranscript: signer must be a demos: ClaimReference, got "${signer}"`);
25
+ const payload = transcriptSigningBytes(unsigned);
26
+ const sigBytes = await signWithPrimaryClaim(signer, payload, demos);
27
+ return {
28
+ sigVersion: "1",
29
+ signer,
30
+ signature: signatureToHex(sigBytes),
31
+ };
32
+ }
33
+ /**
34
+ * Convenience: assemble the full transcript with one or more party
35
+ * signatures. Pass signers in the order their signatures should appear.
36
+ */
37
+ export async function exportTranscript(opts) {
38
+ const unsigned = buildUnsignedTranscript(opts);
39
+ const signatures = [];
40
+ for (const s of opts.signers) {
41
+ signatures.push(await signTranscript(unsigned, s.claim, s.demos));
42
+ }
43
+ return { ...unsigned, signatures };
44
+ }
45
+ /**
46
+ * Verify every claim about a transcript a non-member can check given only
47
+ * the public keys: (a) each `TranscriptSignature` is valid, (b) each
48
+ * `ChannelMessage` carries a valid signature, (c) every message's sender
49
+ * is in `members`, (d) sequences are strictly monotonic per channel,
50
+ * (e) every message's `channelId` matches the transcript.
51
+ *
52
+ * Returns a list of errors rather than throwing so auditors can collect
53
+ * every failure in one pass.
54
+ */
55
+ export function verifyTranscript(t) {
56
+ const errors = [];
57
+ if (t?.transcriptVersion !== "1") {
58
+ errors.push("transcriptVersion is not 1");
59
+ return { ok: false, errors };
60
+ }
61
+ if (!t.channelId)
62
+ errors.push("missing channelId");
63
+ if (!t.members?.length)
64
+ errors.push("missing members");
65
+ let unsigned;
66
+ try {
67
+ unsigned = stripTranscriptSignatures(t);
68
+ }
69
+ catch {
70
+ errors.push("could not strip signatures");
71
+ return { ok: false, errors };
72
+ }
73
+ if (!t.signatures?.length) {
74
+ errors.push("no transcript signatures");
75
+ }
76
+ else {
77
+ const payload = transcriptSigningBytes(unsigned);
78
+ for (const s of t.signatures) {
79
+ if (s.sigVersion !== "1") {
80
+ errors.push(`transcript signature: unknown sigVersion ${s.sigVersion}`);
81
+ continue;
82
+ }
83
+ if (!t.members.includes(s.signer)) {
84
+ errors.push(`transcript signature signer "${s.signer}" not in members`);
85
+ continue;
86
+ }
87
+ try {
88
+ const ok = verifyPrimaryClaimSignature(s.signer, payload, signatureFromHex(s.signature));
89
+ if (!ok)
90
+ errors.push(`transcript signature by "${s.signer}" failed verification`);
91
+ }
92
+ catch (e) {
93
+ errors.push(`transcript signature by "${s.signer}" malformed: ${e.message}`);
94
+ }
95
+ }
96
+ }
97
+ let highestSeen = 0;
98
+ for (const msg of t.messages ?? []) {
99
+ if (msg.channelId !== t.channelId) {
100
+ errors.push(`message seq=${msg.sequence}: channelId mismatch (${msg.channelId} vs ${t.channelId})`);
101
+ }
102
+ if (!isSenderInMembers(msg, t.members)) {
103
+ errors.push(`message seq=${msg.sequence}: sender "${msg.sender}" not in members`);
104
+ }
105
+ if (!Number.isInteger(msg.sequence) || msg.sequence < 1) {
106
+ errors.push(`message: invalid sequence ${msg.sequence}`);
107
+ }
108
+ else if (msg.sequence <= highestSeen) {
109
+ errors.push(`message seq=${msg.sequence}: non-monotonic (highest seen ${highestSeen})`);
110
+ }
111
+ else {
112
+ highestSeen = msg.sequence;
113
+ }
114
+ if (!verifyChannelMessage(msg)) {
115
+ errors.push(`message seq=${msg.sequence}: signature verification failed`);
116
+ }
117
+ }
118
+ return { ok: errors.length === 0, errors };
119
+ }
120
+ //# sourceMappingURL=transcript.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.js","sourceRoot":"","sources":["../../../../src/l2ps/channel/transcript.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * SR-4 WI-A — Channel-over-L2PS transport adapter.
3
+ *
4
+ * The DACS channel layer (`ChannelSession`) is transport-agnostic: it
5
+ * produces signed `ChannelMessage` envelopes and verifies incoming ones,
6
+ * but does not move bytes. The L2PS instant-messaging layer
7
+ * (`L2PSMessagingPeer`) moves encrypted bytes between subnet members but
8
+ * knows nothing about channel envelopes. This adapter bridges the two:
9
+ * it serialises + encrypts an outgoing envelope onto the messaging
10
+ * transport, and on the receive side decrypts, deserialises, and feeds
11
+ * envelopes into the session **in strict sequence order**.
12
+ *
13
+ * Why a reorder buffer is mandatory:
14
+ * `ChannelSession.receiveIncoming` accepts any sequence strictly greater
15
+ * than the highest it has seen and then advances its counter to that
16
+ * value — so a *gap* (seq N+2 arriving before N+1) is not rejected, it
17
+ * permanently skips the missing messages. The L2PS transport delivers by
18
+ * timestamp and replays an offline queue, so out-of-order arrival is
19
+ * normal. This adapter therefore only hands the session the next
20
+ * contiguous sequence and buffers anything ahead of the gap until it
21
+ * fills.
22
+ *
23
+ * Scope / assumptions:
24
+ * - The shared per-channel sequence counter assumes turn-based messaging
25
+ * (DACS negotiation is offer → counter → accept; a party waits for the
26
+ * counter before replying). Two parties emitting the same sequence
27
+ * concurrently is out of scope for SR-4 v1; such a colliding inbound
28
+ * message (seq <= already-applied) is dropped as a duplicate.
29
+ * - Membership is the channel's `members`; the transport sends to every
30
+ * member except self. Confidentiality is the subnet AES key (only
31
+ * members hold it) — CH-2.
32
+ */
33
+ import type { ChannelMessage, ChannelMessageType } from "./types";
34
+ /**
35
+ * Minimal surface of `ChannelSession` the adapter depends on. Declared
36
+ * structurally so tests can inject a fake without real signing keys.
37
+ */
38
+ export interface ChannelSessionLike {
39
+ readonly channelId: string;
40
+ sendOutgoing(opts: {
41
+ type: ChannelMessageType;
42
+ body: unknown;
43
+ sentAt?: number;
44
+ repliesTo?: number;
45
+ }): Promise<ChannelMessage>;
46
+ receiveIncoming(msg: ChannelMessage): Promise<void>;
47
+ }
48
+ /** Wire shape the L2PS messaging transport carries (ciphertext + nonce, base64). */
49
+ export interface SerializedEncryptedMessage {
50
+ ciphertext: string;
51
+ nonce: string;
52
+ ephemeralKey?: string;
53
+ }
54
+ /** Incoming payload the messaging peer hands to an onMessage handler. */
55
+ export interface IncomingMessagePayload {
56
+ from: string;
57
+ encrypted: SerializedEncryptedMessage;
58
+ messageHash: string;
59
+ offline?: boolean;
60
+ }
61
+ /**
62
+ * Minimal surface of `L2PSMessagingPeer` the adapter depends on.
63
+ * Declared structurally for the same reason as `ChannelSessionLike`.
64
+ */
65
+ export interface L2PSMessagingPeerLike {
66
+ send(to: string, encrypted: SerializedEncryptedMessage, messageHash: string): Promise<unknown>;
67
+ onMessage(handler: (payload: IncomingMessagePayload) => void): void;
68
+ }
69
+ export interface L2PSChannelTransportOpts {
70
+ session: ChannelSessionLike;
71
+ peer: L2PSMessagingPeerLike;
72
+ /** Subnet AES-256 key (raw 32 bytes) — only members hold it (CH-2). */
73
+ sharedKey: Uint8Array;
74
+ /**
75
+ * Recipient public keys to deliver to (every channel member except
76
+ * self), in the format the messaging peer routes on.
77
+ */
78
+ recipients: string[];
79
+ /** Called with each envelope once it has been applied in order. */
80
+ onMessage?: (msg: ChannelMessage) => void;
81
+ /** Called on a decrypt / parse / verification failure. */
82
+ onError?: (err: Error) => void;
83
+ }
84
+ export declare class L2PSChannelTransport {
85
+ private readonly session;
86
+ private readonly peer;
87
+ private readonly sharedKey;
88
+ private readonly recipients;
89
+ private readonly onMessage?;
90
+ private readonly onError?;
91
+ /** Sequences seen ahead of the gap, awaiting contiguous application. */
92
+ private readonly buffer;
93
+ /** Highest contiguous sequence applied locally (sent or received). */
94
+ private appliedSeq;
95
+ /** Serialises drain() so concurrent inbound frames can't interleave. */
96
+ private draining;
97
+ private started;
98
+ constructor(opts: L2PSChannelTransportOpts);
99
+ /** Wire the peer's inbound handler. Call once. */
100
+ start(): void;
101
+ /**
102
+ * Build + sign the next outgoing envelope via the session, encrypt
103
+ * it under the subnet key, and deliver to every recipient.
104
+ */
105
+ send(opts: {
106
+ type: ChannelMessageType;
107
+ body: unknown;
108
+ repliesTo?: number;
109
+ }): Promise<ChannelMessage>;
110
+ private ingest;
111
+ /** Apply every buffered message that is now contiguous with appliedSeq. */
112
+ private drain;
113
+ /** Count of out-of-order messages currently held awaiting the gap. */
114
+ get bufferedCount(): number;
115
+ private encrypt;
116
+ private decrypt;
117
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * SR-4 WI-A — Channel-over-L2PS transport adapter.
3
+ *
4
+ * The DACS channel layer (`ChannelSession`) is transport-agnostic: it
5
+ * produces signed `ChannelMessage` envelopes and verifies incoming ones,
6
+ * but does not move bytes. The L2PS instant-messaging layer
7
+ * (`L2PSMessagingPeer`) moves encrypted bytes between subnet members but
8
+ * knows nothing about channel envelopes. This adapter bridges the two:
9
+ * it serialises + encrypts an outgoing envelope onto the messaging
10
+ * transport, and on the receive side decrypts, deserialises, and feeds
11
+ * envelopes into the session **in strict sequence order**.
12
+ *
13
+ * Why a reorder buffer is mandatory:
14
+ * `ChannelSession.receiveIncoming` accepts any sequence strictly greater
15
+ * than the highest it has seen and then advances its counter to that
16
+ * value — so a *gap* (seq N+2 arriving before N+1) is not rejected, it
17
+ * permanently skips the missing messages. The L2PS transport delivers by
18
+ * timestamp and replays an offline queue, so out-of-order arrival is
19
+ * normal. This adapter therefore only hands the session the next
20
+ * contiguous sequence and buffers anything ahead of the gap until it
21
+ * fills.
22
+ *
23
+ * Scope / assumptions:
24
+ * - The shared per-channel sequence counter assumes turn-based messaging
25
+ * (DACS negotiation is offer → counter → accept; a party waits for the
26
+ * counter before replying). Two parties emitting the same sequence
27
+ * concurrently is out of scope for SR-4 v1; such a colliding inbound
28
+ * message (seq <= already-applied) is dropped as a duplicate.
29
+ * - Membership is the channel's `members`; the transport sends to every
30
+ * member except self. Confidentiality is the subnet AES key (only
31
+ * members hold it) — CH-2.
32
+ */
33
+ /**
34
+ * Max number of sequences a buffered inbound frame may sit ahead of the
35
+ * current contiguous point before it is dropped, bounding reorder-buffer
36
+ * memory. Turn-based negotiation never legitimately exceeds this.
37
+ */
38
+ const MAX_REORDER_AHEAD = 256;
39
+ export class L2PSChannelTransport {
40
+ constructor(opts) {
41
+ /** Sequences seen ahead of the gap, awaiting contiguous application. */
42
+ this.buffer = new Map();
43
+ /** Highest contiguous sequence applied locally (sent or received). */
44
+ this.appliedSeq = 0;
45
+ /** Serialises drain() so concurrent inbound frames can't interleave. */
46
+ this.draining = Promise.resolve();
47
+ this.started = false;
48
+ this.session = opts.session;
49
+ this.peer = opts.peer;
50
+ this.sharedKey = opts.sharedKey;
51
+ this.recipients = opts.recipients;
52
+ this.onMessage = opts.onMessage;
53
+ this.onError = opts.onError;
54
+ }
55
+ /** Wire the peer's inbound handler. Call once. */
56
+ start() {
57
+ if (this.started)
58
+ throw new Error("L2PSChannelTransport: already started");
59
+ this.started = true;
60
+ this.peer.onMessage(payload => {
61
+ // Chain onto `draining` so frames are processed one at a time
62
+ // in arrival order; each appends to the buffer then drains.
63
+ this.draining = this.draining
64
+ .then(() => this.ingest(payload))
65
+ .catch(err => this.onError?.(err));
66
+ });
67
+ }
68
+ /**
69
+ * Build + sign the next outgoing envelope via the session, encrypt
70
+ * it under the subnet key, and deliver to every recipient.
71
+ */
72
+ async send(opts) {
73
+ const signed = await this.session.sendOutgoing(opts);
74
+ const encrypted = await this.encrypt(JSON.stringify(signed));
75
+ const messageHash = signed.signature.signature; // unique per signed envelope
76
+ // Deliver to every recipient BEFORE committing the sequence locally.
77
+ // If any send rejects, the throw propagates here and appliedSeq is
78
+ // left untouched, so a retry re-emits the same sequence rather than
79
+ // skipping ahead and stranding peers behind a reorder gap. (Recipients
80
+ // that did receive it will drop the duplicate on resend — seq <=
81
+ // appliedSeq — so at-least-once delivery is safe.)
82
+ for (const to of this.recipients) {
83
+ await this.peer.send(to, encrypted, messageHash);
84
+ }
85
+ // Our own send advances the shared per-channel counter; record it
86
+ // so we expect the peer's reply at appliedSeq + 1.
87
+ if (signed.sequence > this.appliedSeq)
88
+ this.appliedSeq = signed.sequence;
89
+ return signed;
90
+ }
91
+ async ingest(payload) {
92
+ let msg;
93
+ try {
94
+ const json = await this.decrypt(payload.encrypted);
95
+ msg = JSON.parse(json);
96
+ }
97
+ catch (e) {
98
+ throw new Error(`L2PSChannelTransport: failed to decrypt/parse inbound frame from ${payload.from}: ${e.message}`);
99
+ }
100
+ // Ignore traffic for other channels sharing the same subnet.
101
+ if (msg.channelId !== this.session.channelId)
102
+ return;
103
+ // Duplicate / already-applied (or a colliding concurrent send): drop.
104
+ if (msg.sequence <= this.appliedSeq)
105
+ return;
106
+ // Bound the reorder window: a member could otherwise stream many
107
+ // validly-encrypted frames at far-future sequences without ever
108
+ // filling the gap, growing `buffer` without limit. Drop anything
109
+ // beyond MAX_REORDER_AHEAD of the current contiguous point — a
110
+ // legitimate turn-based negotiation never runs that far ahead, and
111
+ // a dropped frame is re-deliverable (the offline queue replays it
112
+ // once the gap closes and appliedSeq advances).
113
+ if (msg.sequence > this.appliedSeq + MAX_REORDER_AHEAD) {
114
+ this.onError?.(new Error(`L2PSChannelTransport: dropping seq ${msg.sequence} — ` +
115
+ `more than ${MAX_REORDER_AHEAD} ahead of applied ${this.appliedSeq}`));
116
+ return;
117
+ }
118
+ // Buffer; a later sequence with the same value would be a protocol
119
+ // error — keep the first seen.
120
+ if (!this.buffer.has(msg.sequence))
121
+ this.buffer.set(msg.sequence, msg);
122
+ await this.drain();
123
+ }
124
+ /** Apply every buffered message that is now contiguous with appliedSeq. */
125
+ async drain() {
126
+ // Sequence counter is shared across both directions, so the next
127
+ // expected inbound is appliedSeq + 1.
128
+ let next = this.buffer.get(this.appliedSeq + 1);
129
+ while (next) {
130
+ this.buffer.delete(next.sequence);
131
+ // Throws on tamper (bad signature, wrong sender, channelId
132
+ // mismatch) — propagate as channel-fatal per §8.12.
133
+ await this.session.receiveIncoming(next);
134
+ this.appliedSeq = next.sequence;
135
+ this.onMessage?.(next);
136
+ next = this.buffer.get(this.appliedSeq + 1);
137
+ }
138
+ }
139
+ /** Count of out-of-order messages currently held awaiting the gap. */
140
+ get bufferedCount() {
141
+ return this.buffer.size;
142
+ }
143
+ // ── AES-256-GCM (Web Crypto) — matches the messaging wire format:
144
+ // ciphertext (with auth tag) + 12-byte nonce, both base64. ──
145
+ async encrypt(plaintext) {
146
+ const nonce = crypto.getRandomValues(new Uint8Array(12));
147
+ const key = await crypto.subtle.importKey("raw", keyBytes(this.sharedKey), "AES-GCM", false, ["encrypt"]);
148
+ const cipher = await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, key, new TextEncoder().encode(plaintext));
149
+ return {
150
+ ciphertext: bytesToBase64(new Uint8Array(cipher)),
151
+ nonce: bytesToBase64(nonce),
152
+ };
153
+ }
154
+ async decrypt(enc) {
155
+ const key = await crypto.subtle.importKey("raw", keyBytes(this.sharedKey), "AES-GCM", false, ["decrypt"]);
156
+ const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: base64ToBytes(enc.nonce) }, key, base64ToBytes(enc.ciphertext));
157
+ return new TextDecoder().decode(plain);
158
+ }
159
+ }
160
+ /**
161
+ * Return a standalone `ArrayBuffer` holding exactly the bytes of `view`.
162
+ * `view.buffer` exposes the whole backing buffer, which for a `subarray`
163
+ * (e.g. a 32-byte key sliced out of a larger buffer) is longer than the
164
+ * view — Web Crypto would then see the wrong length / wrong bytes and
165
+ * reject or mis-key the AES import. Copying the exact `[byteOffset,
166
+ * byteOffset+byteLength)` window guarantees the key import sees only the
167
+ * intended bytes.
168
+ */
169
+ function keyBytes(view) {
170
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
171
+ }
172
+ function bytesToBase64(bytes) {
173
+ return Buffer.from(bytes).toString("base64");
174
+ }
175
+ function base64ToBytes(b64) {
176
+ return new Uint8Array(Buffer.from(b64, "base64"));
177
+ }
178
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../../../src/l2ps/channel/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { ClaimReference } from "../../identity/cci";
2
+ /**
3
+ * Allowed message types — verbatim from DACS-3 §8.3.3. Closed union by
4
+ * design: the substrate enforces the conformance set; application bodies
5
+ * are carried in `body: unknown`.
6
+ */
7
+ export type ChannelMessageType = "offer" | "counter" | "accept" | "reject" | "sealed-envelope-commit" | "sealed-envelope-reveal" | "abort";
8
+ /**
9
+ * Brief doesn't define the wire shape of the signature container. Versioned
10
+ * for forward compatibility (e.g. PQC swap-out) without breaking the
11
+ * envelope layout. Signature is hex-encoded over `channelMessageSigningBytes`.
12
+ */
13
+ export interface ChannelMessageSignature {
14
+ sigVersion: "1";
15
+ signature: string;
16
+ }
17
+ /**
18
+ * DACS-3 §8.3.3 envelope — verbatim. Transport-level fields may wrap this
19
+ * but MUST NOT change the bytes that the signature covers.
20
+ */
21
+ export interface ChannelMessage {
22
+ channelId: string;
23
+ sequence: number;
24
+ sender: ClaimReference;
25
+ sentAt: number;
26
+ type: ChannelMessageType;
27
+ body: unknown;
28
+ refs?: {
29
+ repliesTo?: number;
30
+ };
31
+ signature: ChannelMessageSignature;
32
+ }
33
+ export type UnsignedChannelMessage = Omit<ChannelMessage, "signature">;
34
+ /**
35
+ * Per-signer transcript signature. Each member signs the full transcript
36
+ * (without `signatures`) — see WI-3 reference in the brief.
37
+ */
38
+ export interface TranscriptSignature {
39
+ sigVersion: "1";
40
+ signer: ClaimReference;
41
+ signature: string;
42
+ }
43
+ /**
44
+ * DACS-3 §8.7 — the ordered, member-signed record of a channel session.
45
+ * Consumed by WI-3 for encrypted-anchor flows.
46
+ */
47
+ export interface ChannelTranscript {
48
+ transcriptVersion: "1";
49
+ channelId: string;
50
+ members: ClaimReference[];
51
+ messages: ChannelMessage[];
52
+ generatedAt: number;
53
+ signatures: TranscriptSignature[];
54
+ }
55
+ export type UnsignedChannelTranscript = Omit<ChannelTranscript, "signatures">;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/l2ps/channel/types.ts"],"names":[],"mappings":""}
@@ -1,3 +1,6 @@
1
1
  import L2PS from "./l2ps";
2
- import { L2PSConfig, L2PSEncryptedPayload } from "./l2ps";
3
- export { L2PS, L2PSConfig, L2PSEncryptedPayload };
2
+ import { L2PSConfig, L2PSEncryptedBytes, L2PSEncryptedPayload } from "./l2ps";
3
+ export { L2PS, L2PSConfig, L2PSEncryptedBytes, L2PSEncryptedPayload };
4
+ export * as binding from "./binding";
5
+ export * as channel from "./channel";
6
+ export * as anchor from "./anchor";
@@ -1,3 +1,6 @@
1
1
  import L2PS from "./l2ps.js";
2
2
  export { L2PS };
3
+ export * as binding from "./binding/index.js";
4
+ export * as channel from "./channel/index.js";
5
+ export * as anchor from "./anchor/index.js";
3
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/l2ps/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,QAAQ,CAAA;AAGzB,OAAO,EAAE,IAAI,EAAoC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/l2ps/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,QAAQ,CAAA;AAGzB,OAAO,EAAE,IAAI,EAAwD,CAAA;AAErE,OAAO,KAAK,OAAO,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,OAAO,MAAM,WAAW,CAAA;AACpC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAA"}
@@ -47,6 +47,21 @@ export interface L2PSEncryptedPayload {
47
47
  */
48
48
  nonce?: string;
49
49
  }
50
+ /**
51
+ * Payload shape for `L2PS.encryptBytes` / `L2PS.decryptBytes`. Used by
52
+ * SR-4 WI-3 to encrypt channel transcripts to the subnet member set
53
+ * without dragging the L2PS-transaction envelope into a non-tx context.
54
+ */
55
+ export interface L2PSEncryptedBytes {
56
+ /** UID of the L2PS network that encrypted these bytes. */
57
+ l2ps_uid: string;
58
+ /** Base64-encoded AES-GCM ciphertext. */
59
+ ciphertext: string;
60
+ /** Base64-encoded AES-GCM authentication tag. */
61
+ tag: string;
62
+ /** Base64-encoded AES-GCM nonce (always 12 bytes, fresh per call). */
63
+ nonce: string;
64
+ }
50
65
  /**
51
66
  * L2PS (Layer 2 Private Subnets) class for encrypted transaction processing.
52
67
  *
@@ -207,4 +222,21 @@ export default class L2PS {
207
222
  * @returns Promise resolving to a 16-character fingerprint string
208
223
  */
209
224
  getKeyFingerprint(): Promise<string>;
225
+ /**
226
+ * AES-GCM-encrypt arbitrary bytes under the subnet key.
227
+ *
228
+ * Mirrors the per-call-nonce + nonce-as-AAD pattern in `encryptTx`: a
229
+ * fresh 12-byte nonce per call, bound into the auth tag via AAD so an
230
+ * attacker cannot DoS specific ciphertexts by flipping the stored
231
+ * nonce. Used by SR-4 WI-3 to encrypt channel transcripts to the
232
+ * subnet member set; nothing in the payload shape is L2PS-transaction
233
+ * specific.
234
+ */
235
+ encryptBytes(plaintext: Uint8Array): Promise<L2PSEncryptedBytes>;
236
+ /**
237
+ * Inverse of `encryptBytes`. Verifies the `l2ps_uid` matches this
238
+ * instance and that the AES-GCM auth tag (covering nonce + ciphertext)
239
+ * is intact; throws on either failure.
240
+ */
241
+ decryptBytes(payload: L2PSEncryptedBytes): Promise<Uint8Array>;
210
242
  }
@@ -309,6 +309,66 @@ class L2PS {
309
309
  async getKeyFingerprint() {
310
310
  return Hashing.sha256(this.privateKey).substring(0, 16);
311
311
  }
312
+ /**
313
+ * AES-GCM-encrypt arbitrary bytes under the subnet key.
314
+ *
315
+ * Mirrors the per-call-nonce + nonce-as-AAD pattern in `encryptTx`: a
316
+ * fresh 12-byte nonce per call, bound into the auth tag via AAD so an
317
+ * attacker cannot DoS specific ciphertexts by flipping the stored
318
+ * nonce. Used by SR-4 WI-3 to encrypt channel transcripts to the
319
+ * subnet member set; nothing in the payload shape is L2PS-transaction
320
+ * specific.
321
+ */
322
+ async encryptBytes(plaintext) {
323
+ const cipher = forge.cipher.createCipher('AES-GCM', this.privateKey);
324
+ const nonce = forge.random.getBytesSync(12);
325
+ cipher.start({ iv: nonce, additionalData: nonce });
326
+ cipher.update(forge.util.createBuffer(Buffer.from(plaintext).toString('binary'), 'binary'));
327
+ if (!cipher.finish()) {
328
+ throw new Error('L2PS.encryptBytes: AES-GCM finish() failed');
329
+ }
330
+ return {
331
+ l2ps_uid: this.config?.uid || this.id,
332
+ ciphertext: forge.util.encode64(cipher.output.getBytes()),
333
+ tag: forge.util.encode64(cipher.mode.tag.getBytes()),
334
+ nonce: forge.util.encode64(nonce),
335
+ };
336
+ }
337
+ /**
338
+ * Inverse of `encryptBytes`. Verifies the `l2ps_uid` matches this
339
+ * instance and that the AES-GCM auth tag (covering nonce + ciphertext)
340
+ * is intact; throws on either failure.
341
+ */
342
+ async decryptBytes(payload) {
343
+ if (payload.l2ps_uid !== (this.config?.uid || this.id)) {
344
+ throw new Error('L2PS.decryptBytes: payload encrypted for different L2PS uid');
345
+ }
346
+ // Storage-loaded JSON can violate the L2PSEncryptedBytes shape at
347
+ // runtime; without an explicit shape check forge would throw a
348
+ // base64 error that does not tell the caller which field is wrong.
349
+ if (typeof payload.ciphertext !== 'string' ||
350
+ typeof payload.tag !== 'string' ||
351
+ typeof payload.nonce !== 'string') {
352
+ throw new Error('L2PS.decryptBytes: payload must include base64 strings for ciphertext, tag, and nonce');
353
+ }
354
+ const ciphertext = forge.util.createBuffer(forge.util.decode64(payload.ciphertext));
355
+ const tag = forge.util.createBuffer(forge.util.decode64(payload.tag));
356
+ const iv = forge.util.decode64(payload.nonce);
357
+ if (iv.length !== 12) {
358
+ throw new Error('L2PS.decryptBytes: invalid nonce (expected base64-encoded 12-byte value)');
359
+ }
360
+ const decipher = forge.cipher.createDecipher('AES-GCM', this.privateKey);
361
+ decipher.start({ iv, tag, additionalData: iv });
362
+ decipher.update(ciphertext);
363
+ if (!decipher.finish()) {
364
+ throw new Error('L2PS.decryptBytes: AES-GCM authentication failed');
365
+ }
366
+ const bin = decipher.output.getBytes();
367
+ const out = new Uint8Array(bin.length);
368
+ for (let i = 0; i < bin.length; i++)
369
+ out[i] = bin.charCodeAt(i) & 0xff;
370
+ return out;
371
+ }
312
372
  }
313
373
  /** Static map of all L2PS instances, keyed by instance ID */
314
374
  L2PS.instances = new Map();