@interop/was-client 0.14.0 → 0.14.1

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 (70) hide show
  1. package/dist/Collection.d.ts +58 -0
  2. package/dist/Collection.d.ts.map +1 -1
  3. package/dist/Collection.js +106 -13
  4. package/dist/Collection.js.map +1 -1
  5. package/dist/Resource.d.ts +8 -0
  6. package/dist/Resource.d.ts.map +1 -1
  7. package/dist/Resource.js +52 -34
  8. package/dist/Resource.js.map +1 -1
  9. package/dist/Space.d.ts +15 -1
  10. package/dist/Space.d.ts.map +1 -1
  11. package/dist/Space.js +25 -2
  12. package/dist/Space.js.map +1 -1
  13. package/dist/WasClient.d.ts +1 -0
  14. package/dist/WasClient.d.ts.map +1 -1
  15. package/dist/WasClient.js +31 -21
  16. package/dist/WasClient.js.map +1 -1
  17. package/dist/codec.d.ts +14 -11
  18. package/dist/codec.d.ts.map +1 -1
  19. package/dist/edv/EdvCodec.d.ts +34 -56
  20. package/dist/edv/EdvCodec.d.ts.map +1 -1
  21. package/dist/edv/EdvCodec.js +155 -52
  22. package/dist/edv/EdvCodec.js.map +1 -1
  23. package/dist/edv/WasTransport.d.ts +21 -4
  24. package/dist/edv/WasTransport.d.ts.map +1 -1
  25. package/dist/edv/WasTransport.js +53 -18
  26. package/dist/edv/WasTransport.js.map +1 -1
  27. package/dist/edv/epochCrypto.d.ts +101 -0
  28. package/dist/edv/epochCrypto.d.ts.map +1 -0
  29. package/dist/edv/epochCrypto.js +211 -0
  30. package/dist/edv/epochCrypto.js.map +1 -0
  31. package/dist/edv/epochKeys.d.ts +57 -0
  32. package/dist/edv/epochKeys.d.ts.map +1 -0
  33. package/dist/edv/epochKeys.js +132 -0
  34. package/dist/edv/epochKeys.js.map +1 -0
  35. package/dist/edv/index.d.ts +8 -0
  36. package/dist/edv/index.d.ts.map +1 -1
  37. package/dist/edv/index.js +7 -0
  38. package/dist/edv/index.js.map +1 -1
  39. package/dist/edv/recipients.d.ts +122 -0
  40. package/dist/edv/recipients.d.ts.map +1 -0
  41. package/dist/edv/recipients.js +265 -0
  42. package/dist/edv/recipients.js.map +1 -0
  43. package/dist/errors.d.ts +31 -0
  44. package/dist/errors.d.ts.map +1 -1
  45. package/dist/errors.js +37 -0
  46. package/dist/errors.js.map +1 -1
  47. package/dist/index.d.ts +2 -2
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/codec.d.ts.map +1 -1
  52. package/dist/internal/codec.js +12 -4
  53. package/dist/internal/codec.js.map +1 -1
  54. package/dist/internal/conditional.d.ts +6 -1
  55. package/dist/internal/conditional.d.ts.map +1 -1
  56. package/dist/internal/conditional.js +8 -1
  57. package/dist/internal/conditional.js.map +1 -1
  58. package/dist/internal/content.d.ts +2 -2
  59. package/dist/internal/content.d.ts.map +1 -1
  60. package/dist/internal/content.js +16 -6
  61. package/dist/internal/content.js.map +1 -1
  62. package/dist/internal/paths.d.ts.map +1 -1
  63. package/dist/internal/paths.js +10 -1
  64. package/dist/internal/paths.js.map +1 -1
  65. package/dist/internal/write.d.ts.map +1 -1
  66. package/dist/internal/write.js +5 -1
  67. package/dist/internal/write.js.map +1 -1
  68. package/dist/types.d.ts +75 -3
  69. package/dist/types.d.ts.map +1 -1
  70. package/package.json +5 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"epochCrypto.d.ts","sourceRoot":"","sources":["../../src/edv/epochCrypto.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAEhE;;GAEG;AACH,eAAO,MAAM,YAAY,mBAAmB,CAAA;AAS5C;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,kBAAkB,EAAE,MAAM,CAAA;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CAAC,EACpC,WAAW,EACX,SAAS,EACV,EAAE;IACD,WAAW,EAAE,UAAU,CAAA;IACvB,SAAS,EAAE,kBAAkB,CAAA;CAC9B,GAAG,OAAO,CAAC,6BAA6B,CAAC,CA4BzC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,EACtC,KAAK,EACL,eAAe,EAChB,EAAE;IACD,KAAK,EAAE,6BAA6B,CAAA;IACpC,eAAe,EAAE,gBAAgB,CAAA;CAClC,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAyB7B;AAED;;;;;;;GAOG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,UAAU,CAAA;CACnB,CAAC,CAQD;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,OAAO,EACP,MAAM,EACP,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,UAAU,CAAA;CACnB,GAAG,gBAAgB,CASnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD;AAkBD;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,EACnC,EAAE,EACH,EAAE;IACD,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBpE"}
@@ -0,0 +1,211 @@
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
4
+ /**
5
+ * The key-epoch cryptography for multi-recipient encrypted Collections: wrapping
6
+ * and unwrapping a per-epoch collection key to each reader's X25519
7
+ * key-agreement key, and minting / reconstructing the epoch key pair the EDV
8
+ * codec encrypts resources under.
9
+ *
10
+ * A collection key epoch is a fresh 32-byte X25519 private key. Resources are
11
+ * encrypted with the ordinary EDV `documentCipher`, naming the epoch's public
12
+ * key as the sole recipient (each document gets a fresh content-encryption key
13
+ * wrapped `ECDH-ES+A256KW` to the epoch key) -- so the read/write machinery is
14
+ * unchanged and only key resolution is epoch-aware. The epoch's public key IS
15
+ * its `did:key`, and the epoch id is that `did:key` string, so the standard
16
+ * `did:key` resolver resolves the recipient named in a resource's JWE.
17
+ *
18
+ * The epoch key itself is shared with a reader by wrapping the 32-byte epoch
19
+ * secret to the reader's own X25519 key-agreement key, `ECDH-ES+A256KW`, and
20
+ * storing the result as a JWE `recipients` entry verbatim on the Collection's
21
+ * `encryption` marker. The wrap is derive-then-wrap per recipient: an ephemeral
22
+ * ECDH against the reader's public key, the RFC 7518 Concat KDF, then A256KW --
23
+ * `@interop/minimal-cipher`'s own `ECDH-ES+A256KW` building blocks (via its
24
+ * `algorithms` subpath), so the wrapped bytes are interchangeable with what its
25
+ * `Cipher` would produce.
26
+ *
27
+ * The two axes stay separate here: this module governs **read** (who can
28
+ * decrypt), which is prospective -- rotating the epoch only protects resources
29
+ * written afterwards. **Pull** (who the server will serve ciphertext to) is the
30
+ * zcap layer, enforced immediately at request time. Neither alone removes a
31
+ * reader; see `removeRecipient`.
32
+ */
33
+ import { base64urlnopad } from '@scure/base';
34
+ import { createKek, deriveKey } from '@interop/minimal-cipher/algorithms';
35
+ import { X25519KeyAgreementKey2020, multibaseDecode, multibaseEncode, MULTICODEC_X25519_PUB_HEADER } from '@interop/x25519-key-agreement-key';
36
+ /**
37
+ * The JOSE key-management algorithm for every epoch/recipient wrap.
38
+ */
39
+ export const KEY_WRAP_ALG = 'ECDH-ES+A256KW';
40
+ /**
41
+ * The X25519 suite tag used for the ephemeral and reconstructed key pairs.
42
+ */
43
+ const X25519_TYPE = 'X25519KeyAgreementKey2020';
44
+ const TEXT_ENCODER = new TextEncoder();
45
+ /**
46
+ * Wraps a 32-byte epoch secret to one recipient's X25519 key-agreement key,
47
+ * producing the JWE `recipients` entry stored on the marker. Generates a fresh
48
+ * ephemeral key per call (ECDH-ES), so each wrap carries its own `epk`.
49
+ *
50
+ * @param options {object}
51
+ * @param options.epochSecret {Uint8Array} the 32-byte epoch key to wrap
52
+ * @param options.recipient {RecipientPublicKey} the reader's public KAK
53
+ * @returns {Promise<CollectionEncryptionRecipient>}
54
+ */
55
+ export async function wrapEpochSecret({ epochSecret, recipient }) {
56
+ const ephemeral = await X25519KeyAgreementKey2020.generate();
57
+ const ephemeralPublicKey = multibaseDecode(MULTICODEC_X25519_PUB_HEADER, ephemeral.publicKeyMultibase);
58
+ const secret = await ephemeral.deriveSecret({
59
+ publicKey: { publicKeyMultibase: recipient.publicKeyMultibase }
60
+ });
61
+ const producerInfo = ephemeralPublicKey;
62
+ const consumerInfo = TEXT_ENCODER.encode(recipient.id);
63
+ const keyData = await deriveKey({ secret, producerInfo, consumerInfo });
64
+ const kek = await createKek({ keyData });
65
+ const encryptedKey = await kek.wrapKey({ unwrappedKey: epochSecret });
66
+ return {
67
+ header: {
68
+ kid: recipient.id,
69
+ alg: KEY_WRAP_ALG,
70
+ epk: {
71
+ kty: 'OKP',
72
+ crv: 'X25519',
73
+ x: base64urlnopad.encode(ephemeralPublicKey)
74
+ },
75
+ apu: base64urlnopad.encode(producerInfo),
76
+ apv: base64urlnopad.encode(consumerInfo)
77
+ },
78
+ encrypted_key: encryptedKey
79
+ };
80
+ }
81
+ /**
82
+ * Unwraps an epoch secret from a marker `recipients` entry using the reader's
83
+ * own key-agreement key. Returns `null` when this key does not match the entry
84
+ * (the wrong recipient, or a corrupt entry) -- never treat `null` as a key; try
85
+ * the next candidate entry or epoch, and fail with a typed error when nothing
86
+ * unwraps.
87
+ *
88
+ * @param options {object}
89
+ * @param options.entry {CollectionEncryptionRecipient} the marker entry
90
+ * @param options.keyAgreementKey {IKeyAgreementKey} the reader's own KAK
91
+ * (its `id` must equal `entry.header.kid` for the derivation to match)
92
+ * @returns {Promise<Uint8Array | null>}
93
+ */
94
+ export async function unwrapEpochSecret({ entry, keyAgreementKey }) {
95
+ const epk = entry.header.epk;
96
+ if (!epk || typeof epk.x !== 'string') {
97
+ return null;
98
+ }
99
+ const ephemeralPublicKey = base64urlnopad.decode(epk.x);
100
+ let secret;
101
+ try {
102
+ secret = await keyAgreementKey.deriveSecret({
103
+ publicKey: {
104
+ type: X25519_TYPE,
105
+ publicKeyMultibase: multibaseEncode(MULTICODEC_X25519_PUB_HEADER, ephemeralPublicKey)
106
+ }
107
+ });
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ const producerInfo = ephemeralPublicKey;
113
+ const consumerInfo = TEXT_ENCODER.encode(keyAgreementKey.id);
114
+ const keyData = await deriveKey({ secret, producerInfo, consumerInfo });
115
+ const kek = await createKek({ keyData });
116
+ return kek.unwrapKey({ wrappedKey: entry.encrypted_key });
117
+ }
118
+ /**
119
+ * Mints a fresh key epoch: a new random X25519 key pair whose `did:key` is the
120
+ * epoch id. Returns the id and the raw 32-byte secret (to wrap to recipients);
121
+ * `resolveEpochKeys` reconstructs the epoch key pair from the id and secret when
122
+ * a resource must be encrypted or decrypted under the epoch.
123
+ *
124
+ * @returns {Promise<{ epochId: string, secret: Uint8Array }>}
125
+ */
126
+ export async function mintEpoch() {
127
+ const generated = await X25519KeyAgreementKey2020.generate();
128
+ if (generated.privateKeyMultibase === undefined) {
129
+ throw new Error('Generated epoch key is missing its private key.');
130
+ }
131
+ const secret = generated.rawSecret;
132
+ const epochId = `did:key:${generated.publicKeyMultibase}`;
133
+ return { epochId, secret };
134
+ }
135
+ /**
136
+ * Reconstructs an epoch key pair from its id (a `did:key`, which carries the
137
+ * public key) and the unwrapped 32-byte secret, ready to hand to the EDV
138
+ * `documentCipher` as its `keyAgreementKey`.
139
+ *
140
+ * @param options {object}
141
+ * @param options.epochId {string} the epoch's `did:key`
142
+ * @param options.secret {Uint8Array} the unwrapped 32-byte epoch secret
143
+ * @returns {X25519KeyAgreementKey2020}
144
+ */
145
+ export function reconstructEpochKeyPair({ epochId, secret }) {
146
+ // The concrete instance always carries a defined `id` (set here), so it
147
+ // satisfies `IKeyAgreementKey` (whose `id` is required); the suite's type
148
+ // declares `id` optional, hence the assertion.
149
+ return X25519KeyAgreementKey2020.fromRawSecret({
150
+ secret,
151
+ controller: epochId,
152
+ id: epochKeyIdFor(epochId)
153
+ });
154
+ }
155
+ /**
156
+ * The verification-method id of an epoch key: `<did:key>#<fingerprint>`. This is
157
+ * the `kid` the EDV `documentCipher` stamps on a resource encrypted under the
158
+ * epoch; a reader maps it back to the epoch by taking the `did:key` portion
159
+ * before the `#` fragment.
160
+ *
161
+ * @param epochId {string} the epoch's `did:key`
162
+ * @returns {string}
163
+ */
164
+ export function epochKeyIdFor(epochId) {
165
+ return `${epochId}#${publicKeyMultibaseFromEpochId(epochId)}`;
166
+ }
167
+ /**
168
+ * Extracts the `publicKeyMultibase` fingerprint from an epoch id (`did:key:z...`).
169
+ *
170
+ * @param epochId {string}
171
+ * @returns {string}
172
+ */
173
+ function publicKeyMultibaseFromEpochId(epochId) {
174
+ const prefix = 'did:key:';
175
+ if (!epochId.startsWith(prefix)) {
176
+ throw new Error(`Epoch id "${epochId}" is not a did:key (expected a "${prefix}z..." id).`);
177
+ }
178
+ return epochId.slice(prefix.length);
179
+ }
180
+ /**
181
+ * A `did:key` key resolver for X25519 key-agreement keys: resolves a
182
+ * `did:key:z...#z...` id (an epoch key, or any self-describing X25519 key) to
183
+ * its public verification method. The public key is the fragment, so no network
184
+ * or registry lookup is needed.
185
+ *
186
+ * @param options {object}
187
+ * @param options.id {string} the key id to resolve
188
+ * @returns {Promise<{ id: string, type: string, publicKeyMultibase: string }>}
189
+ */
190
+ export async function didKeyResolver({ id }) {
191
+ if (!id) {
192
+ throw new Error('A key id is required to resolve.');
193
+ }
194
+ const hash = id.indexOf('#');
195
+ const fragment = hash === -1 ? '' : id.slice(hash + 1);
196
+ if (!fragment.startsWith('z')) {
197
+ throw new Error(`Cannot resolve non-did:key key id "${id}".`);
198
+ }
199
+ // Validate the fragment is a well-formed X25519 public key fingerprint; the
200
+ // suite throws on invalid header bytes.
201
+ const keyPair = X25519KeyAgreementKey2020.fromFingerprint({
202
+ fingerprint: fragment
203
+ });
204
+ keyPair.verifyFingerprint({ fingerprint: fragment });
205
+ return {
206
+ id,
207
+ type: X25519_TYPE,
208
+ publicKeyMultibase: keyPair.publicKeyMultibase
209
+ };
210
+ }
211
+ //# sourceMappingURL=epochCrypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"epochCrypto.js","sourceRoot":"","sources":["../../src/edv/epochCrypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
4
+ /**
5
+ * Resolves a reader's per-epoch keys from a Collection's `encryption` marker.
6
+ * Given the marker (its `epochs` and `currentEpoch`) and the reader's own
7
+ * key-agreement key, it reconstructs each epoch the reader is a recipient of as
8
+ * an X25519 key pair the EDV `documentCipher` can use -- the write epoch's key
9
+ * for writes, and one read key per epoch the reader holds (so a resource written
10
+ * under an older epoch stays readable). The write epoch is unwrapped eagerly;
11
+ * the other epochs' keys unwrap lazily on first decrypt naming them, so a
12
+ * write-only handle does not pay to unwrap history it never reads.
13
+ *
14
+ * This is the read axis: holding an epoch key lets a reader decrypt resources
15
+ * written under it. A reader removed from a later epoch keeps the earlier epoch
16
+ * keys and so keeps reading pre-rotation resources -- rotation is prospective,
17
+ * never retroactive.
18
+ */
19
+ import type { IKeyAgreementKey } from '@interop/data-integrity-core';
20
+ import type { CollectionEncryption } from '../types.js';
21
+ /**
22
+ * The reader's resolved key-epoch material for a Collection.
23
+ */
24
+ export interface ResolvedEpochKeys {
25
+ /**
26
+ * the epoch id writes encrypt under and stamp (the marker's `currentEpoch`)
27
+ */
28
+ writeEpoch: string;
29
+ /**
30
+ * the key writes encrypt under
31
+ */
32
+ writeKey: IKeyAgreementKey;
33
+ /**
34
+ * every epoch key this reader can unwrap, for decrypting any epoch (the
35
+ * `writeKey`, unwrapped eagerly, plus a lazily-unwrapped key per other epoch
36
+ * this reader is a recipient of)
37
+ */
38
+ readKeys: IKeyAgreementKey[];
39
+ }
40
+ /**
41
+ * Resolves the reader's epoch keys from a marker. Returns `null` when the marker
42
+ * declares no epochs (a single-key collection -- the caller keeps its existing
43
+ * single-key path). Throws {@link KeyUnwrapError} when the marker HAS epochs but
44
+ * this reader can unwrap none of them (it is not a recipient), so an encrypted
45
+ * collection is never silently read/written with the wrong key.
46
+ *
47
+ * @param options {object}
48
+ * @param options.encryption {CollectionEncryption} the Collection's marker
49
+ * @param options.keyAgreementKey {IKeyAgreementKey} the reader's own KAK; its
50
+ * `id` must match a recipient `kid` in an epoch for that epoch to unwrap
51
+ * @returns {Promise<ResolvedEpochKeys | null>}
52
+ */
53
+ export declare function resolveEpochKeys({ encryption, keyAgreementKey }: {
54
+ encryption: CollectionEncryption;
55
+ keyAgreementKey: IKeyAgreementKey;
56
+ }): Promise<ResolvedEpochKeys | null>;
57
+ //# sourceMappingURL=epochKeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"epochKeys.d.ts","sourceRoot":"","sources":["../../src/edv/epochKeys.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAEpE,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,aAAa,CAAA;AAOpB;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,QAAQ,EAAE,gBAAgB,CAAA;IAC1B;;;;OAIG;IACH,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,eAAe,EAChB,EAAE;IACD,UAAU,EAAE,oBAAoB,CAAA;IAChC,eAAe,EAAE,gBAAgB,CAAA;CAClC,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CA+DpC"}
@@ -0,0 +1,132 @@
1
+ import { KeyUnwrapError } from '../errors.js';
2
+ import { epochKeyIdFor, reconstructEpochKeyPair, unwrapEpochSecret } from './epochCrypto.js';
3
+ /**
4
+ * Resolves the reader's epoch keys from a marker. Returns `null` when the marker
5
+ * declares no epochs (a single-key collection -- the caller keeps its existing
6
+ * single-key path). Throws {@link KeyUnwrapError} when the marker HAS epochs but
7
+ * this reader can unwrap none of them (it is not a recipient), so an encrypted
8
+ * collection is never silently read/written with the wrong key.
9
+ *
10
+ * @param options {object}
11
+ * @param options.encryption {CollectionEncryption} the Collection's marker
12
+ * @param options.keyAgreementKey {IKeyAgreementKey} the reader's own KAK; its
13
+ * `id` must match a recipient `kid` in an epoch for that epoch to unwrap
14
+ * @returns {Promise<ResolvedEpochKeys | null>}
15
+ */
16
+ export async function resolveEpochKeys({ encryption, keyAgreementKey }) {
17
+ const epochs = encryption.epochs;
18
+ if (!epochs || epochs.length === 0) {
19
+ return null;
20
+ }
21
+ // The epochs this reader is named in (has a recipient entry keyed to its
22
+ // `kid`), in the marker's canonical order. Being named IS being a recipient;
23
+ // whether a named entry actually unwraps is confirmed eagerly for the write
24
+ // epoch and lazily (on first decrypt) for the rest.
25
+ const namedEpochs = epochs.filter(epoch => epoch.recipients.some(recipient => recipient.header.kid === keyAgreementKey.id));
26
+ if (namedEpochs.length === 0) {
27
+ throw new KeyUnwrapError('This reader is not a recipient of any key epoch on this encrypted ' +
28
+ "collection (none of the marker's recipient entries name this " +
29
+ "reader's key-agreement key). Add this reader with addRecipient, or " +
30
+ 'supply the correct key-agreement key.');
31
+ }
32
+ // Choose the write epoch: `currentEpoch` when this reader holds it, otherwise
33
+ // the newest epoch it holds -- defined deterministically as the LAST epoch in
34
+ // the marker's canonical `epochs` order that names this reader, never the
35
+ // incidental order in which secrets happened to unwrap. A reader that is not a
36
+ // recipient of `currentEpoch` is a removed/historical reader whose writes the
37
+ // server rejects via its revoked zcap anyway; selecting a deterministic
38
+ // fallback here only keeps the local `writeEpoch`/`writeKey` well-defined
39
+ // instead of assuming the `epochs` array is append-ordered newest-last.
40
+ const currentEpoch = encryption.currentEpoch;
41
+ const writeEpochEntry = (currentEpoch !== undefined &&
42
+ namedEpochs.find(epoch => epoch.id === currentEpoch)) ||
43
+ namedEpochs[namedEpochs.length - 1];
44
+ // The write epoch is unwrapped eagerly: `writeKey` must be a full key pair the
45
+ // EDV cipher can name recipients with and encrypt under right away.
46
+ const writeKey = await unwrapEpochKey({
47
+ epoch: writeEpochEntry,
48
+ keyAgreementKey
49
+ });
50
+ if (!writeKey) {
51
+ throw new KeyUnwrapError(`This reader's recipient entry for the write epoch ` +
52
+ `"${writeEpochEntry.id}" did not unwrap (a corrupt entry). Re-add ` +
53
+ 'this reader with addRecipient, or supply the correct key-agreement ' +
54
+ 'key.');
55
+ }
56
+ // Read keys: the eagerly-unwrapped write key, plus a LAZY key per other named
57
+ // epoch. Each lazy key knows its `id` up front (derived from the epoch id, so
58
+ // the codec's kid-match filter needs no secret) and unwraps + reconstructs its
59
+ // epoch secret only on first decrypt naming it, caching the result. So a
60
+ // write-only handle -- or a reader that only ever touches current-epoch
61
+ // resources -- never pays the ECDH + KDF + key-unwrap for historical epochs it
62
+ // does not read.
63
+ const readKeys = [writeKey];
64
+ for (const epoch of namedEpochs) {
65
+ if (epoch.id !== writeEpochEntry.id) {
66
+ readKeys.push(lazyEpochKey({ epoch, keyAgreementKey }));
67
+ }
68
+ }
69
+ return { writeEpoch: writeEpochEntry.id, writeKey, readKeys };
70
+ }
71
+ /**
72
+ * Unwraps and reconstructs a single epoch's key pair for this reader, or returns
73
+ * `null` when the reader is not a recipient of the epoch or its entry does not
74
+ * unwrap (a corrupt entry -- never treat `null` as a key).
75
+ *
76
+ * @param options {object}
77
+ * @param options.epoch {CollectionEncryptionEpoch} the epoch to unwrap
78
+ * @param options.keyAgreementKey {IKeyAgreementKey} the reader's own KAK
79
+ * @returns {Promise<IKeyAgreementKey | null>}
80
+ */
81
+ async function unwrapEpochKey({ epoch, keyAgreementKey }) {
82
+ const entry = epoch.recipients.find(recipient => recipient.header.kid === keyAgreementKey.id);
83
+ if (!entry) {
84
+ return null;
85
+ }
86
+ const secret = await unwrapEpochSecret({ entry, keyAgreementKey });
87
+ if (!secret) {
88
+ return null;
89
+ }
90
+ return reconstructEpochKeyPair({ epochId: epoch.id, secret });
91
+ }
92
+ /**
93
+ * Builds a lazily-unwrapping read key for a named epoch: an `IKeyAgreementKey`
94
+ * whose `id` is known up front (the epoch key's verification-method id, derived
95
+ * from the epoch id -- the `kid` a resource written under this epoch stamps), so
96
+ * the codec can kid-match it before any secret is derived, and whose
97
+ * `deriveSecret` unwraps + reconstructs the real epoch key pair on first call
98
+ * and caches it. This defers the ECDH + KDF + key-unwrap cost until (and unless)
99
+ * a resource named for this epoch is actually decrypted.
100
+ *
101
+ * @param options {object}
102
+ * @param options.epoch {CollectionEncryptionEpoch} the epoch this key reads
103
+ * @param options.keyAgreementKey {IKeyAgreementKey} the reader's own KAK
104
+ * @returns {IKeyAgreementKey}
105
+ */
106
+ function lazyEpochKey({ epoch, keyAgreementKey }) {
107
+ let pending;
108
+ const resolve = () => {
109
+ if (pending === undefined) {
110
+ pending = unwrapEpochKey({ epoch, keyAgreementKey }).then(key => {
111
+ if (!key) {
112
+ // The reader was named in this epoch (else no lazy key was built) but
113
+ // its entry did not unwrap: a corrupt entry. The codec's `_decrypt`
114
+ // catches this and tries the next candidate before surfacing its own
115
+ // typed failure.
116
+ throw new KeyUnwrapError(`This reader's recipient entry for epoch "${epoch.id}" did not ` +
117
+ 'unwrap (a corrupt entry).');
118
+ }
119
+ return key;
120
+ });
121
+ }
122
+ return pending;
123
+ };
124
+ return {
125
+ id: epochKeyIdFor(epoch.id),
126
+ async deriveSecret(options) {
127
+ const key = await resolve();
128
+ return key.deriveSecret(options);
129
+ }
130
+ };
131
+ }
132
+ //# sourceMappingURL=epochKeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"epochKeys.js","sourceRoot":"","sources":["../../src/edv/epochKeys.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAK7C,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,kBAAkB,CAAA;AAsBzB;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,EACrC,UAAU,EACV,eAAe,EAIhB;IACC,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAA;IAChC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnC,OAAO,IAAI,CAAA;IACb,CAAC;IACD,yEAAyE;IACzE,6EAA6E;IAC7E,4EAA4E;IAC5E,oDAAoD;IACpD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACxC,KAAK,CAAC,UAAU,CAAC,IAAI,CACnB,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,KAAK,eAAe,CAAC,EAAE,CACzD,CACF,CAAA;IACD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,cAAc,CACtB,oEAAoE;YAClE,+DAA+D;YAC/D,qEAAqE;YACrE,uCAAuC,CAC1C,CAAA;IACH,CAAC;IACD,8EAA8E;IAC9E,8EAA8E;IAC9E,0EAA0E;IAC1E,+EAA+E;IAC/E,8EAA8E;IAC9E,wEAAwE;IACxE,0EAA0E;IAC1E,wEAAwE;IACxE,MAAM,YAAY,GAAG,UAAU,CAAC,YAAY,CAAA;IAC5C,MAAM,eAAe,GACnB,CAAC,YAAY,KAAK,SAAS;QACzB,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,YAAY,CAAC,CAAC;QACvD,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAE,CAAA;IACtC,+EAA+E;IAC/E,oEAAoE;IACpE,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC;QACpC,KAAK,EAAE,eAAe;QACtB,eAAe;KAChB,CAAC,CAAA;IACF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,cAAc,CACtB,oDAAoD;YAClD,IAAI,eAAe,CAAC,EAAE,6CAA6C;YACnE,qEAAqE;YACrE,MAAM,CACT,CAAA;IACH,CAAC;IACD,8EAA8E;IAC9E,8EAA8E;IAC9E,+EAA+E;IAC/E,yEAAyE;IACzE,wEAAwE;IACxE,+EAA+E;IAC/E,iBAAiB;IACjB,MAAM,QAAQ,GAAuB,CAAC,QAAQ,CAAC,CAAA;IAC/C,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;QAChC,IAAI,KAAK,CAAC,EAAE,KAAK,eAAe,CAAC,EAAE,EAAE,CAAC;YACpC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,CAAA;QACzD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAA;AAC/D,CAAC;AAED;;;;;;;;;GASG;AACH,KAAK,UAAU,cAAc,CAAC,EAC5B,KAAK,EACL,eAAe,EAIhB;IACC,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CACjC,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,KAAK,eAAe,CAAC,EAAE,CACzD,CAAA;IACD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAA;IAClE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,uBAAuB,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;AAC/D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,YAAY,CAAC,EACpB,KAAK,EACL,eAAe,EAIhB;IACC,IAAI,OAA8C,CAAA;IAClD,MAAM,OAAO,GAAG,GAA8B,EAAE;QAC9C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,GAAG,cAAc,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;gBAC9D,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,sEAAsE;oBACtE,oEAAoE;oBACpE,qEAAqE;oBACrE,iBAAiB;oBACjB,MAAM,IAAI,cAAc,CACtB,4CAA4C,KAAK,CAAC,EAAE,YAAY;wBAC9D,2BAA2B,CAC9B,CAAA;gBACH,CAAC;gBACD,OAAO,GAAG,CAAA;YACZ,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC,CAAA;IACD,OAAO;QACL,EAAE,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,KAAK,CAAC,YAAY,CAAC,OAA+B;YAChD,MAAM,GAAG,GAAG,MAAM,OAAO,EAAE,CAAA;YAC3B,OAAO,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAA;QAClC,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -15,8 +15,16 @@
15
15
  * override), so `collection.put`/`get` transparently encrypt/decrypt.
16
16
  * - `WasTransport` -- the standalone `@interop/edv-client`
17
17
  * transport, for driving an `EdvClient` directly against WAS.
18
+ *
19
+ * Multi-recipient (key-epoch) collections layer on top: `initRecipients` /
20
+ * `addRecipient` / `removeRecipient` manage the readers and rotate the epoch
21
+ * key, so the same `createEdvEncryption` provider transparently encrypts each
22
+ * write under the current epoch and decrypts any epoch a reader still holds.
18
23
  */
19
24
  export { createEdvEncryption, EdvCodec } from './EdvCodec.js';
20
25
  export type { EdvKeys } from './EdvCodec.js';
21
26
  export { WasTransport, JOSE_CONTENT_TYPE } from './WasTransport.js';
27
+ export { initRecipients, addRecipient, removeRecipient } from './recipients.js';
28
+ export type { OwnerKey, RecipientPublicKey } from './recipients.js';
29
+ export { mintEpoch, epochKeyIdFor } from './epochCrypto.js';
22
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC7D,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC7D,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC/E,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACnE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
package/dist/edv/index.js CHANGED
@@ -15,7 +15,14 @@
15
15
  * override), so `collection.put`/`get` transparently encrypt/decrypt.
16
16
  * - `WasTransport` -- the standalone `@interop/edv-client`
17
17
  * transport, for driving an `EdvClient` directly against WAS.
18
+ *
19
+ * Multi-recipient (key-epoch) collections layer on top: `initRecipients` /
20
+ * `addRecipient` / `removeRecipient` manage the readers and rotate the epoch
21
+ * key, so the same `createEdvEncryption` provider transparently encrypts each
22
+ * write under the current epoch and decrypts any epoch a reader still holds.
18
23
  */
19
24
  export { createEdvEncryption, EdvCodec } from './EdvCodec.js';
20
25
  export { WasTransport, JOSE_CONTENT_TYPE } from './WasTransport.js';
26
+ export { initRecipients, addRecipient, removeRecipient } from './recipients.js';
27
+ export { mintEpoch, epochKeyIdFor } from './epochCrypto.js';
21
28
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE7D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE7D,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE/E,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,122 @@
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
4
+ /**
5
+ * Recipient and key-epoch management for multi-recipient encrypted Collections:
6
+ * initializing the first epoch, adding a reader (escrow -- history included),
7
+ * and removing a reader (the full revoke-and-rotate procedure). Each operation
8
+ * mutates the Collection's `encryption` marker and writes it back with a
9
+ * compare-and-swap (`If-Match`), retrying on a concurrent change so two racing
10
+ * recipient edits cannot clobber one another.
11
+ *
12
+ * The two axes stay separate and are both required to actually remove a reader:
13
+ *
14
+ * - **pull** -- the zcap the server checks at request time. Revoking it stops
15
+ * the server serving that reader ciphertext. Immediate and total.
16
+ * - **read** -- possession of an epoch key. Rotating the epoch means resources
17
+ * written afterward are encrypted under a key the removed reader does not
18
+ * hold. Prospective only.
19
+ *
20
+ * Important: Rotation protects post-rotation writes only. It never claws
21
+ * back data a reader already downloaded, and a removed reader keeps every
22
+ * earlier epoch's key, so any pre-rotation resource whose ciphertext it obtains
23
+ * stays readable to it. {@link removeRecipient} does both halves so a caller
24
+ * cannot accidentally do one; callers who truly want half can call
25
+ * `space.revoke()` or nothing, respectively, themselves.
26
+ */
27
+ import type { IKeyAgreementKey } from '@interop/data-integrity-core';
28
+ import type { Collection } from '../Collection.js';
29
+ import type { Space } from '../Space.js';
30
+ import type { CollectionEncryption, IDelegatedZcap } from '../types.js';
31
+ import type { RecipientPublicKey } from './epochCrypto.js';
32
+ export type { RecipientPublicKey } from './epochCrypto.js';
33
+ /**
34
+ * The caller's own key material, used to unwrap existing epoch keys so they can
35
+ * be re-wrapped to a newly added reader (escrow).
36
+ */
37
+ export interface OwnerKey {
38
+ keyAgreementKey: IKeyAgreementKey;
39
+ }
40
+ /**
41
+ * Initializes the first key epoch on a Collection that is declared encrypted
42
+ * but has no epochs yet: mints a fresh epoch key, wraps it to each initial
43
+ * recipient, and writes `epochs: [epoch]` / `currentEpoch` back with a
44
+ * compare-and-swap. After this, resources written by any recipient are encrypted
45
+ * under the epoch, and readers unwrap it with their own key-agreement key.
46
+ *
47
+ * @param options {object}
48
+ * @param options.collection {Collection} the (already encrypted) collection
49
+ * @param options.recipients {RecipientPublicKey[]} the initial readers' public
50
+ * key-agreement keys (each `id` is the reader's `kid`)
51
+ * @returns {Promise<CollectionEncryption>} the new marker
52
+ */
53
+ export declare function initRecipients({ collection, recipients }: {
54
+ collection: Collection;
55
+ recipients: RecipientPublicKey[];
56
+ }): Promise<CollectionEncryption>;
57
+ /**
58
+ * Adds a reader to a multi-recipient encrypted Collection. Escrow semantics: the
59
+ * new reader receives EVERY epoch's key (the current one and all prior), because
60
+ * "add a reader to a collection" means it can read the Collection, history
61
+ * included. No rotation happens -- **adds are cheap, removals rotate.**
62
+ *
63
+ * The caller must itself be a recipient of every epoch (its `owner` key unwraps
64
+ * each epoch key, which is then re-wrapped to the new reader). Written back with
65
+ * a compare-and-swap, retried on a concurrent change.
66
+ *
67
+ * @param options {object}
68
+ * @param options.collection {Collection} the collection
69
+ * @param options.recipient {RecipientPublicKey} the new reader's public KAK
70
+ * @param options.owner {OwnerKey} the caller's own key-agreement key, to
71
+ * unwrap each epoch key for re-wrapping to the new reader
72
+ * @returns {Promise<CollectionEncryption>} the new marker
73
+ */
74
+ export declare function addRecipient({ collection, recipient, owner }: {
75
+ collection: Collection;
76
+ recipient: RecipientPublicKey;
77
+ owner: OwnerKey;
78
+ }): Promise<CollectionEncryption>;
79
+ /**
80
+ * Removes a reader from a multi-recipient encrypted Collection -- one
81
+ * indivisible operation doing BOTH halves of a removal:
82
+ *
83
+ * 1. **Rotate the epoch**: mint a fresh epoch key, wrap it to each REMAINING
84
+ * recipient (the current epoch's roster minus the removed reader), append it
85
+ * as a new epoch, and repoint `currentEpoch`, with a compare-and-swap.
86
+ * Resources written afterward are unreadable to the removed reader (the read
87
+ * axis; prospective).
88
+ * 2. **Revoke the reader's zcap(s)** via `space.revoke()`, so the server stops
89
+ * serving it ciphertext (the pull axis; immediate).
90
+ *
91
+ * The rotation runs first so it is durable before the irreversible revocation:
92
+ * a rotation that keeps losing the compare-and-swap throws with nothing revoked,
93
+ * leaving the operation safely retryable rather than half-applied. The revoke
94
+ * step tolerates an already-revoked capability (a retry re-revokes) so the
95
+ * operation converges.
96
+ *
97
+ * Important: this does not re-encrypt existing resources, so the removed
98
+ * reader keeps every earlier epoch's key and can still decrypt any pre-rotation
99
+ * resource whose ciphertext it gets. Neither half alone removes a reader.
100
+ *
101
+ * @param options {object}
102
+ * @param options.collection {Collection} the collection
103
+ * @param options.space {Space} the collection's Space, for the zcap revocation
104
+ * @param options.recipientId {string} the removed reader's key-agreement key
105
+ * id (`kid`), dropped from the new epoch's recipients
106
+ * @param options.revoke {IDelegatedZcap | IDelegatedZcap[]} the reader's
107
+ * delegated capability/capabilities to revoke (the pull axis)
108
+ * @param [options.resolveRecipientKey] {function} resolves a remaining
109
+ * recipient's `kid` to its public key-agreement key, so the fresh epoch key
110
+ * can be wrapped to it. Defaults to a `did:key` resolver (the `kid` fragment
111
+ * is the X25519 public key); override for recipients whose `kid` is not a
112
+ * self-describing `did:key`.
113
+ * @returns {Promise<CollectionEncryption>} the new marker
114
+ */
115
+ export declare function removeRecipient({ collection, space, recipientId, revoke, resolveRecipientKey }: {
116
+ collection: Collection;
117
+ space: Space;
118
+ recipientId: string;
119
+ revoke: IDelegatedZcap | IDelegatedZcap[];
120
+ resolveRecipientKey?: (kid: string) => Promise<RecipientPublicKey>;
121
+ }): Promise<CollectionEncryption>;
122
+ //# sourceMappingURL=recipients.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipients.d.ts","sourceRoot":"","sources":["../../src/edv/recipients.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAExC,OAAO,KAAK,EACV,oBAAoB,EAEpB,cAAc,EACf,MAAM,aAAa,CAAA;AAOpB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,eAAe,EAAE,gBAAgB,CAAA;CAClC;AAQD;;;;;;;;;;;;GAYG;AACH,wBAAsB,cAAc,CAAC,EACnC,UAAU,EACV,UAAU,EACX,EAAE;IACD,UAAU,EAAE,UAAU,CAAA;IACtB,UAAU,EAAE,kBAAkB,EAAE,CAAA;CACjC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA2BhC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,YAAY,CAAC,EACjC,UAAU,EACV,SAAS,EACT,KAAK,EACN,EAAE;IACD,UAAU,EAAE,UAAU,CAAA;IACtB,SAAS,EAAE,kBAAkB,CAAA;IAC7B,KAAK,EAAE,QAAQ,CAAA;CAChB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkDhC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,eAAe,CAAC,EACpC,UAAU,EACV,KAAK,EACL,WAAW,EACX,MAAM,EACN,mBAAgD,EACjD,EAAE;IACD,UAAU,EAAE,UAAU,CAAA;IACtB,KAAK,EAAE,KAAK,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAA;IACzC,mBAAmB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAA;CACnE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA6EhC"}