@interop/was-client 0.13.3 → 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.
- package/README.md +33 -0
- package/dist/Collection.d.ts +58 -0
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +106 -13
- package/dist/Collection.js.map +1 -1
- package/dist/Resource.d.ts +8 -0
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +52 -34
- package/dist/Resource.js.map +1 -1
- package/dist/Space.d.ts +42 -1
- package/dist/Space.d.ts.map +1 -1
- package/dist/Space.js +55 -2
- package/dist/Space.js.map +1 -1
- package/dist/WasClient.d.ts +15 -0
- package/dist/WasClient.d.ts.map +1 -1
- package/dist/WasClient.js +52 -21
- package/dist/WasClient.js.map +1 -1
- package/dist/codec.d.ts +14 -11
- package/dist/codec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.d.ts +34 -56
- package/dist/edv/EdvCodec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.js +155 -52
- package/dist/edv/EdvCodec.js.map +1 -1
- package/dist/edv/WasTransport.d.ts +84 -19
- package/dist/edv/WasTransport.d.ts.map +1 -1
- package/dist/edv/WasTransport.js +198 -44
- package/dist/edv/WasTransport.js.map +1 -1
- package/dist/edv/epochCrypto.d.ts +101 -0
- package/dist/edv/epochCrypto.d.ts.map +1 -0
- package/dist/edv/epochCrypto.js +211 -0
- package/dist/edv/epochCrypto.js.map +1 -0
- package/dist/edv/epochKeys.d.ts +57 -0
- package/dist/edv/epochKeys.d.ts.map +1 -0
- package/dist/edv/epochKeys.js +132 -0
- package/dist/edv/epochKeys.js.map +1 -0
- package/dist/edv/index.d.ts +8 -0
- package/dist/edv/index.d.ts.map +1 -1
- package/dist/edv/index.js +7 -0
- package/dist/edv/index.js.map +1 -1
- package/dist/edv/recipients.d.ts +122 -0
- package/dist/edv/recipients.d.ts.map +1 -0
- package/dist/edv/recipients.js +265 -0
- package/dist/edv/recipients.js.map +1 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +37 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codec.d.ts.map +1 -1
- package/dist/internal/codec.js +12 -4
- package/dist/internal/codec.js.map +1 -1
- package/dist/internal/conditional.d.ts +6 -1
- package/dist/internal/conditional.d.ts.map +1 -1
- package/dist/internal/conditional.js +8 -1
- package/dist/internal/conditional.js.map +1 -1
- package/dist/internal/content.d.ts +2 -2
- package/dist/internal/content.d.ts.map +1 -1
- package/dist/internal/content.js +16 -6
- package/dist/internal/content.js.map +1 -1
- package/dist/internal/grant.d.ts +11 -1
- package/dist/internal/grant.d.ts.map +1 -1
- package/dist/internal/grant.js +32 -3
- package/dist/internal/grant.js.map +1 -1
- package/dist/internal/paths.d.ts +28 -0
- package/dist/internal/paths.d.ts.map +1 -1
- package/dist/internal/paths.js +52 -1
- package/dist/internal/paths.js.map +1 -1
- package/dist/internal/revoke.d.ts +34 -0
- package/dist/internal/revoke.d.ts.map +1 -0
- package/dist/internal/revoke.js +106 -0
- package/dist/internal/revoke.js.map +1 -0
- package/dist/internal/write.d.ts.map +1 -1
- package/dist/internal/write.js +5 -1
- package/dist/internal/write.js.map +1 -1
- package/dist/types.d.ts +77 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -5
|
@@ -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":"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"}
|
package/dist/edv/index.d.ts
CHANGED
|
@@ -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
|
package/dist/edv/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
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
|
package/dist/edv/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
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"}
|