@interop/wallet-core 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/clients/rosterPolicy.d.ts +28 -16
- package/dist/clients/rosterPolicy.d.ts.map +1 -1
- package/dist/clients/rosterPolicy.js +51 -26
- package/dist/clients/rosterPolicy.js.map +1 -1
- package/dist/descriptors/index.d.ts +1 -1
- package/dist/descriptors/index.d.ts.map +1 -1
- package/dist/descriptors/index.js +1 -1
- package/dist/descriptors/index.js.map +1 -1
- package/dist/descriptors/logSource.d.ts +58 -6
- package/dist/descriptors/logSource.d.ts.map +1 -1
- package/dist/descriptors/logSource.js +57 -15
- package/dist/descriptors/logSource.js.map +1 -1
- package/dist/enrollment/enrollment.d.ts.map +1 -1
- package/dist/enrollment/enrollment.js +14 -26
- package/dist/enrollment/enrollment.js.map +1 -1
- package/dist/keyring/fetch.d.ts +12 -0
- package/dist/keyring/fetch.d.ts.map +1 -1
- package/dist/keyring/fetch.js +0 -2
- package/dist/keyring/fetch.js.map +1 -1
- package/dist/keyring/index.d.ts +1 -1
- package/dist/keyring/index.d.ts.map +1 -1
- package/dist/keyring/index.js +1 -1
- package/dist/keyring/index.js.map +1 -1
- package/dist/keyring/kdf.d.ts +22 -6
- package/dist/keyring/kdf.d.ts.map +1 -1
- package/dist/keyring/kdf.js +9 -14
- package/dist/keyring/kdf.js.map +1 -1
- package/dist/keys/index.d.ts +1 -1
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js +1 -1
- package/dist/keys/index.js.map +1 -1
- package/dist/keys/rosterLogStore.d.ts +4 -1
- package/dist/keys/rosterLogStore.d.ts.map +1 -1
- package/dist/keys/rosterLogStore.js +13 -16
- package/dist/keys/rosterLogStore.js.map +1 -1
- package/dist/keys/rosterStore.d.ts +13 -1
- package/dist/keys/rosterStore.d.ts.map +1 -1
- package/dist/keys/rosterStore.js +18 -0
- package/dist/keys/rosterStore.js.map +1 -1
- package/dist/keys/userKeyRoster.d.ts +17 -5
- package/dist/keys/userKeyRoster.d.ts.map +1 -1
- package/dist/keys/userKeyRoster.js +38 -13
- package/dist/keys/userKeyRoster.js.map +1 -1
- package/dist/recovery/index.d.ts +14 -12
- package/dist/recovery/index.d.ts.map +1 -1
- package/dist/recovery/index.js +13 -11
- package/dist/recovery/index.js.map +1 -1
- package/dist/recovery/recoveryCode.d.ts +5 -17
- package/dist/recovery/recoveryCode.d.ts.map +1 -1
- package/dist/recovery/recoveryCode.js +4 -14
- package/dist/recovery/recoveryCode.js.map +1 -1
- package/dist/recovery/recoveryDelegation.d.ts +50 -19
- package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
- package/dist/recovery/recoveryDelegation.js +86 -41
- package/dist/recovery/recoveryDelegation.js.map +1 -1
- package/dist/recovery/recoveryWebvh.d.ts +13 -22
- package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
- package/dist/recovery/recoveryWebvh.js +37 -129
- package/dist/recovery/recoveryWebvh.js.map +1 -1
- package/dist/resourceLog/append.d.ts +12 -3
- package/dist/resourceLog/append.d.ts.map +1 -1
- package/dist/resourceLog/append.js +17 -10
- package/dist/resourceLog/append.js.map +1 -1
- package/dist/resourceLog/index.d.ts +1 -1
- package/dist/resourceLog/index.d.ts.map +1 -1
- package/dist/resourceLog/index.js +1 -1
- package/dist/resourceLog/index.js.map +1 -1
- package/dist/resourceLog/pin.d.ts +46 -8
- package/dist/resourceLog/pin.d.ts.map +1 -1
- package/dist/resourceLog/pin.js +32 -7
- package/dist/resourceLog/pin.js.map +1 -1
- package/dist/resourceLog/seal.d.ts +4 -1
- package/dist/resourceLog/seal.d.ts.map +1 -1
- package/dist/resourceLog/seal.js +6 -2
- package/dist/resourceLog/seal.js.map +1 -1
- package/dist/unlock/index.d.ts +40 -0
- package/dist/unlock/index.d.ts.map +1 -0
- package/dist/unlock/index.js +36 -0
- package/dist/unlock/index.js.map +1 -0
- package/dist/unlock/ladder.d.ts +100 -0
- package/dist/unlock/ladder.d.ts.map +1 -0
- package/dist/unlock/ladder.js +144 -0
- package/dist/unlock/ladder.js.map +1 -0
- package/dist/unlock/selfEnroll.d.ts +83 -0
- package/dist/unlock/selfEnroll.d.ts.map +1 -0
- package/dist/unlock/selfEnroll.js +126 -0
- package/dist/unlock/selfEnroll.js.map +1 -0
- package/dist/unlock/standingClient.d.ts +68 -0
- package/dist/unlock/standingClient.d.ts.map +1 -0
- package/dist/unlock/standingClient.js +81 -0
- package/dist/unlock/standingClient.js.map +1 -0
- package/dist/unlock/standingWebvh.d.ts +153 -0
- package/dist/unlock/standingWebvh.d.ts.map +1 -0
- package/dist/unlock/standingWebvh.js +370 -0
- package/dist/unlock/standingWebvh.js.map +1 -0
- package/dist/unlock/unlockRecord.d.ts +207 -0
- package/dist/unlock/unlockRecord.d.ts.map +1 -0
- package/dist/unlock/unlockRecord.js +435 -0
- package/dist/unlock/unlockRecord.js.map +1 -0
- package/dist/webvh/didWebvh.d.ts +94 -6
- package/dist/webvh/didWebvh.d.ts.map +1 -1
- package/dist/webvh/didWebvh.js +175 -19
- package/dist/webvh/didWebvh.js.map +1 -1
- package/dist/webvh/index.d.ts +7 -2
- package/dist/webvh/index.d.ts.map +1 -1
- package/dist/webvh/index.js +7 -2
- package/dist/webvh/index.js.map +1 -1
- package/dist/webvh/keyAgreement.d.ts +22 -6
- package/dist/webvh/keyAgreement.d.ts.map +1 -1
- package/dist/webvh/keyAgreement.js +1 -1
- package/dist/webvh/keyAgreement.js.map +1 -1
- package/dist/webvh/verifyLog.d.ts +15 -2
- package/dist/webvh/verifyLog.d.ts.map +1 -1
- package/dist/webvh/verifyLog.js +22 -3
- package/dist/webvh/verifyLog.js.map +1 -1
- package/package.json +11 -5
- package/dist/recovery/recoveryRecord.d.ts +0 -172
- package/dist/recovery/recoveryRecord.d.ts.map +0 -1
- package/dist/recovery/recoveryRecord.js +0 -288
- package/dist/recovery/recoveryRecord.js.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The update-key ladder: a standing unlock credential's latent-and-consumed
|
|
6
|
+
* did:webvh update authority. Rungs derive deterministically from a random
|
|
7
|
+
* 32-byte ladder seed carried inside the credential's unlock record -- never
|
|
8
|
+
* from the unlock secret itself, because a revealed rung lives verbatim in
|
|
9
|
+
* the world-readable `updateKeys` forever, where no hash commitment can
|
|
10
|
+
* protect it, so a secret-derived rung would be a standing offline grind
|
|
11
|
+
* oracle against the credential.
|
|
12
|
+
*
|
|
13
|
+
* Between uses only `hash(rung i)` stands in the document's `nextKeyHashes`;
|
|
14
|
+
* each self-enrollment is one loud reveal-and-commit entry signing with rung
|
|
15
|
+
* `i` and committing `hash(rung i + 1)`, after which the add entry retires
|
|
16
|
+
* the spent rung. The credential never holds a standing `updateKeys` member.
|
|
17
|
+
*
|
|
18
|
+
* There is no stored counter: which rung is current is recovered by
|
|
19
|
+
* re-deriving and scanning the published log's standing parameters
|
|
20
|
+
* ({@link attributeLadderRung}), and ambiguity fails closed rather than
|
|
21
|
+
* guessing -- the clients-listing attribution precedent. A lost
|
|
22
|
+
* compare-and-swap race resolves by determinism: the winner's entry commits
|
|
23
|
+
* `hash(rung i + 1)`, which IS the loser's retry key, so a re-run
|
|
24
|
+
* re-attributes and climbs one rung.
|
|
25
|
+
*
|
|
26
|
+
* The rung derivation is wire-level (both wallet apps must climb the same
|
|
27
|
+
* ladder from the same seed), so the salt and info labels are permanent.
|
|
28
|
+
*/
|
|
29
|
+
import { deriveNextKeyHash } from '@interop/did-method-webvh';
|
|
30
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
31
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
32
|
+
import { updateKeyMultibase } from '../webvh/didWebvh.js';
|
|
33
|
+
/**
|
|
34
|
+
* The byte length of a ladder seed: 32 random bytes, minted at bind time and
|
|
35
|
+
* carried only inside the unlock record's sealed ladder member.
|
|
36
|
+
*/
|
|
37
|
+
export const LADDER_SEED_BYTES = 32;
|
|
38
|
+
/**
|
|
39
|
+
* The HKDF salt for rung derivation and the per-rung info prefix (the rung
|
|
40
|
+
* index in decimal follows it). Both permanent -- changing either orphans
|
|
41
|
+
* every bound credential's ladder.
|
|
42
|
+
*/
|
|
43
|
+
const LADDER_SALT = 'freewallet/unlock/update-ladder/v1';
|
|
44
|
+
const LADDER_RUNG_INFO_PREFIX = 'rung/';
|
|
45
|
+
/**
|
|
46
|
+
* How many rungs {@link attributeLadderRung} derives before concluding the
|
|
47
|
+
* log commits none of them. Generous: one rung is consumed per
|
|
48
|
+
* self-enrollment, so a real ladder's standing commitment sits at the number
|
|
49
|
+
* of self-enrollments the credential has ever performed.
|
|
50
|
+
*/
|
|
51
|
+
export const LADDER_MAX_SCAN = 128;
|
|
52
|
+
/**
|
|
53
|
+
* Thrown when the published log's standing parameters match no derivable rung
|
|
54
|
+
* -- the credential's posture was revoked (or never published), the ladder
|
|
55
|
+
* seed does not belong to this account, or the scan bound was exceeded -- or
|
|
56
|
+
* when they match more than one rung in the same role, which no legitimate
|
|
57
|
+
* history produces. Self-enrollment refuses loudly rather than guessing.
|
|
58
|
+
*/
|
|
59
|
+
export class LadderAttributionError extends Error {
|
|
60
|
+
constructor(message) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = 'LadderAttributionError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generates a fresh random ladder seed.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Uint8Array}
|
|
69
|
+
*/
|
|
70
|
+
export function generateLadderSeed() {
|
|
71
|
+
return crypto.getRandomValues(new Uint8Array(LADDER_SEED_BYTES));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Derives the 32-byte update-key seed of rung `index`.
|
|
75
|
+
*
|
|
76
|
+
* @param options {object}
|
|
77
|
+
* @param options.ladderSeed {Uint8Array}
|
|
78
|
+
* @param options.index {number} the rung index, from 0
|
|
79
|
+
* @returns {Uint8Array}
|
|
80
|
+
*/
|
|
81
|
+
export function ladderRungSeed({ ladderSeed, index }) {
|
|
82
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
83
|
+
throw new Error(`Invalid ladder rung index "${String(index)}".`);
|
|
84
|
+
}
|
|
85
|
+
return hkdf(sha256, ladderSeed, new TextEncoder().encode(LADDER_SALT), new TextEncoder().encode(`${LADDER_RUNG_INFO_PREFIX}${index}`), 32);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Derives rung `index` in full: seed and public multibase.
|
|
89
|
+
*
|
|
90
|
+
* @param options {object}
|
|
91
|
+
* @param options.ladderSeed {Uint8Array}
|
|
92
|
+
* @param options.index {number} the rung index, from 0
|
|
93
|
+
* @returns {Promise<LadderRung>}
|
|
94
|
+
*/
|
|
95
|
+
export async function ladderRung({ ladderSeed, index }) {
|
|
96
|
+
const seed = ladderRungSeed({ ladderSeed, index });
|
|
97
|
+
return { index, seed, keyMultibase: await updateKeyMultibase({ seed }) };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Recovers the ladder's current rung from the published log's standing
|
|
101
|
+
* parameters -- the counter recovery that replaces any stored counter. Scans
|
|
102
|
+
* rungs `0..maxScan - 1`; a rung whose key stands in `updateKeys` is a torn
|
|
103
|
+
* self-enrollment to resume (`'revealed'`), else a rung whose hash stands in
|
|
104
|
+
* `nextKeyHashes` is the standing commitment (`'committed'`). Exactly one
|
|
105
|
+
* revealed rung, or exactly one committed rung beside it, is legitimate --
|
|
106
|
+
* the reveal entry keeps the spent rung's hash committed so a resumed run can
|
|
107
|
+
* re-state it, which is why a revealed rung wins over a committed one.
|
|
108
|
+
* Anything else fails closed with {@link LadderAttributionError}.
|
|
109
|
+
*
|
|
110
|
+
* @param options {object}
|
|
111
|
+
* @param options.ladderSeed {Uint8Array}
|
|
112
|
+
* @param options.published {object} the resolved log's standing parameters
|
|
113
|
+
* @param options.published.updateKeys {string[]}
|
|
114
|
+
* @param options.published.nextKeyHashes {string[]}
|
|
115
|
+
* @param [options.maxScan] {number} how many rungs to derive before giving
|
|
116
|
+
* up; defaults to {@link LADDER_MAX_SCAN}
|
|
117
|
+
* @returns {Promise<{ rung: LadderRung, state: LadderRungState }>}
|
|
118
|
+
*/
|
|
119
|
+
export async function attributeLadderRung({ ladderSeed, published, maxScan = LADDER_MAX_SCAN }) {
|
|
120
|
+
const revealed = [];
|
|
121
|
+
const committed = [];
|
|
122
|
+
for (let index = 0; index < maxScan; index++) {
|
|
123
|
+
const rung = await ladderRung({ ladderSeed, index });
|
|
124
|
+
if (published.updateKeys.includes(rung.keyMultibase)) {
|
|
125
|
+
revealed.push(rung);
|
|
126
|
+
}
|
|
127
|
+
else if (published.nextKeyHashes.includes(await deriveNextKeyHash(rung.keyMultibase))) {
|
|
128
|
+
committed.push(rung);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (revealed.length > 1 || committed.length > 1) {
|
|
132
|
+
throw new LadderAttributionError('The published log commits more than one rung of this ladder in the ' +
|
|
133
|
+
'same role; refusing to self-enroll on an ambiguous attribution.');
|
|
134
|
+
}
|
|
135
|
+
if (revealed.length === 1) {
|
|
136
|
+
return { rung: revealed[0], state: 'revealed' };
|
|
137
|
+
}
|
|
138
|
+
if (committed.length === 1) {
|
|
139
|
+
return { rung: committed[0], state: 'committed' };
|
|
140
|
+
}
|
|
141
|
+
throw new LadderAttributionError('The published log commits no rung of this ladder; the credential has ' +
|
|
142
|
+
'been revoked, was never published, or does not belong to this account.');
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=ladder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ladder.js","sourceRoot":"","sources":["../../src/unlock/ladder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The self-enrollment completion core: what a fresh browser runs once its
|
|
6
|
+
* typed credential has located the account, proven the unlock record genuine,
|
|
7
|
+
* and handed over the bridge delegation and the ladder seed. One call turns
|
|
8
|
+
* the credential's latent authority into an ordinary enrolled client, with no
|
|
9
|
+
* second party involved:
|
|
10
|
+
*
|
|
11
|
+
* 1. Mint the new client's whole key set locally (client seed, did:webvh
|
|
12
|
+
* update-key seeds). Nothing is durable before the ceremony succeeds.
|
|
13
|
+
* 2. Write the self-enrolling continuation through the delegated `did.jsonl`
|
|
14
|
+
* bridge ({@link selfEnrollWebvhClient}): the reveal-and-commit entry
|
|
15
|
+
* signed by the attributed ladder rung, then the add entry that publishes
|
|
16
|
+
* the new client and retires the rung. Loud by construction -- the
|
|
17
|
+
* world-readable, hash-chained log extends before a single byte can be
|
|
18
|
+
* read.
|
|
19
|
+
* 3. Verify the account log locally and perform the first roster read,
|
|
20
|
+
* signed with the just-published `<did:webvh>#<multibase>` key and
|
|
21
|
+
* unwrapping the user key from the CREDENTIAL's standing wrap (escrowed
|
|
22
|
+
* into every epoch at bind time, kept alive by rotation fan-out).
|
|
23
|
+
* 4. Escrow the new client into the roster as its own recipient
|
|
24
|
+
* (`addUserKeyRosterRecipient`, epochs unwrapped with the credential's
|
|
25
|
+
* key-agreement key), so later logins on this browser read the roster the
|
|
26
|
+
* ordinary enrolled way.
|
|
27
|
+
*
|
|
28
|
+
* Persisting the key set under the app's own unlock layer is the caller's
|
|
29
|
+
* job, exactly as with the enrollment ceremony's completion: this module
|
|
30
|
+
* hands back the key set, the user key, and the roster epoch to pin, and
|
|
31
|
+
* stops there. Every stage is idempotent, so a torn run converges by
|
|
32
|
+
* re-running with the same credential (step 2 resumes from the standing
|
|
33
|
+
* commitments; a re-run after completion mints a fresh client, which is the
|
|
34
|
+
* ordinary next self-enrollment).
|
|
35
|
+
*/
|
|
36
|
+
import type { IKeyAgreementKey } from '@interop/data-integrity-core';
|
|
37
|
+
import type { UserKey } from '../keys/userKey.js';
|
|
38
|
+
import type { AccountPointer } from '../keyring/record.js';
|
|
39
|
+
import { type ResourceLogPinStore } from '../resourceLog/index.js';
|
|
40
|
+
import type { ClientWebvhUpdateKeys } from '../webvh/didWebvh.js';
|
|
41
|
+
import type { UnlockLogStore } from './standingWebvh.js';
|
|
42
|
+
/**
|
|
43
|
+
* Runs the whole self-enrollment described in the module doc. The caller has
|
|
44
|
+
* already unwrapped the credential's unlock record (so it holds the
|
|
45
|
+
* credential-authenticated pointer, the bridge delegation behind `logStore`,
|
|
46
|
+
* and the ladder seed) and derived the credential's client identity (so it
|
|
47
|
+
* holds the key-agreement key with its secret half).
|
|
48
|
+
*
|
|
49
|
+
* @param options {object}
|
|
50
|
+
* @param options.pointer {AccountPointer} the credential-authenticated
|
|
51
|
+
* account pointer; must name a did:webvh
|
|
52
|
+
* @param options.ladderSeed {Uint8Array} the credential's update-key ladder
|
|
53
|
+
* seed, from its unlock record
|
|
54
|
+
* @param options.credentialKeyAgreementKey {IKeyAgreementKey} the
|
|
55
|
+
* credential's own key-agreement key (secret half included) -- its roster
|
|
56
|
+
* entry, which the user key is unwrapped from and every epoch re-wrapped
|
|
57
|
+
* with
|
|
58
|
+
* @param options.logStore {UnlockLogStore} the public log read plus the
|
|
59
|
+
* delegated `did.jsonl` PUT, built by the app around the record's bridge
|
|
60
|
+
* delegation
|
|
61
|
+
* @param [options.accountLogPinStore] {ResourceLogPinStore} this client's
|
|
62
|
+
* chain-head pin for the account log. A fresh browser normally has none
|
|
63
|
+
* (this is its first contact), which is exactly the pin's
|
|
64
|
+
* trust-on-first-use establishment
|
|
65
|
+
* @returns {Promise<object>} the new client's key set (for the caller to
|
|
66
|
+
* persist under its unlock layer), its did:key, the account DID, the user
|
|
67
|
+
* key, and the roster epoch to pin
|
|
68
|
+
*/
|
|
69
|
+
export declare function selfEnrollClientCore({ pointer, ladderSeed, credentialKeyAgreementKey, logStore, accountLogPinStore }: {
|
|
70
|
+
pointer: AccountPointer;
|
|
71
|
+
ladderSeed: Uint8Array;
|
|
72
|
+
credentialKeyAgreementKey: IKeyAgreementKey;
|
|
73
|
+
logStore: UnlockLogStore;
|
|
74
|
+
accountLogPinStore?: ResourceLogPinStore;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
clientSeed: Uint8Array;
|
|
77
|
+
webvhUpdateKeys: ClientWebvhUpdateKeys;
|
|
78
|
+
clientDid: string;
|
|
79
|
+
did: string;
|
|
80
|
+
userKey: UserKey;
|
|
81
|
+
latestEpochId: string;
|
|
82
|
+
}>;
|
|
83
|
+
//# sourceMappingURL=selfEnroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selfEnroll.d.ts","sourceRoot":"","sources":["../../src/unlock/selfEnroll.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AASpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,yBAAyB,CAAA;AAKhC,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,sBAAsB,CAAA;AAQ7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,OAAO,EACP,UAAU,EACV,yBAAyB,EACzB,QAAQ,EACR,kBAAkB,EACnB,EAAE;IACD,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,UAAU,CAAA;IACtB,yBAAyB,EAAE,gBAAgB,CAAA;IAC3C,QAAQ,EAAE,cAAc,CAAA;IACxB,kBAAkB,CAAC,EAAE,mBAAmB,CAAA;CACzC,GAAG,OAAO,CAAC;IACV,UAAU,EAAE,UAAU,CAAA;IACtB,eAAe,EAAE,qBAAqB,CAAA;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,MAAM,CAAA;CACtB,CAAC,CAqGD"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { agentsFromSeed } from '../identity/agents.js';
|
|
2
|
+
import { addUserKeyRosterRecipient, readUserKeyRoster, rosterRecipientKid, userKeyRosterLogSigner } from '../keys/userKeyRoster.js';
|
|
3
|
+
import { userKeyRosterDescriptorStore } from '../keys/rosterStore.js';
|
|
4
|
+
import { memoryResourceLogPinStore, webvhResourceLogController } from '../resourceLog/index.js';
|
|
5
|
+
import { mintClientWebvhUpdateKeys, updateKeyMultibase } from '../webvh/didWebvh.js';
|
|
6
|
+
import { verifyAccountLog } from '../webvh/verifyLog.js';
|
|
7
|
+
import { clientSigningKeyMultibase, isWebvhDid, webvhZcapClient } from '../webvh/zcap.js';
|
|
8
|
+
import { selfEnrollWebvhClient } from './standingWebvh.js';
|
|
9
|
+
/**
|
|
10
|
+
* Runs the whole self-enrollment described in the module doc. The caller has
|
|
11
|
+
* already unwrapped the credential's unlock record (so it holds the
|
|
12
|
+
* credential-authenticated pointer, the bridge delegation behind `logStore`,
|
|
13
|
+
* and the ladder seed) and derived the credential's client identity (so it
|
|
14
|
+
* holds the key-agreement key with its secret half).
|
|
15
|
+
*
|
|
16
|
+
* @param options {object}
|
|
17
|
+
* @param options.pointer {AccountPointer} the credential-authenticated
|
|
18
|
+
* account pointer; must name a did:webvh
|
|
19
|
+
* @param options.ladderSeed {Uint8Array} the credential's update-key ladder
|
|
20
|
+
* seed, from its unlock record
|
|
21
|
+
* @param options.credentialKeyAgreementKey {IKeyAgreementKey} the
|
|
22
|
+
* credential's own key-agreement key (secret half included) -- its roster
|
|
23
|
+
* entry, which the user key is unwrapped from and every epoch re-wrapped
|
|
24
|
+
* with
|
|
25
|
+
* @param options.logStore {UnlockLogStore} the public log read plus the
|
|
26
|
+
* delegated `did.jsonl` PUT, built by the app around the record's bridge
|
|
27
|
+
* delegation
|
|
28
|
+
* @param [options.accountLogPinStore] {ResourceLogPinStore} this client's
|
|
29
|
+
* chain-head pin for the account log. A fresh browser normally has none
|
|
30
|
+
* (this is its first contact), which is exactly the pin's
|
|
31
|
+
* trust-on-first-use establishment
|
|
32
|
+
* @returns {Promise<object>} the new client's key set (for the caller to
|
|
33
|
+
* persist under its unlock layer), its did:key, the account DID, the user
|
|
34
|
+
* key, and the roster epoch to pin
|
|
35
|
+
*/
|
|
36
|
+
export async function selfEnrollClientCore({ pointer, ladderSeed, credentialKeyAgreementKey, logStore, accountLogPinStore }) {
|
|
37
|
+
const expectedDid = pointer.did;
|
|
38
|
+
if (!expectedDid || !isWebvhDid(expectedDid)) {
|
|
39
|
+
throw new Error('The account pointer names no did:webvh; only a promoted account can ' +
|
|
40
|
+
'self-enroll a client.');
|
|
41
|
+
}
|
|
42
|
+
// The new client's whole key set, minted locally; nothing travels but the
|
|
43
|
+
// public halves the log entries publish.
|
|
44
|
+
const clientSeed = crypto.getRandomValues(new Uint8Array(32));
|
|
45
|
+
const { keyAgent, keyAgreementKey } = await agentsFromSeed({
|
|
46
|
+
seed: clientSeed
|
|
47
|
+
});
|
|
48
|
+
const { publicKeyMultibase: keyAgreementKeyMultibase } = keyAgreementKey;
|
|
49
|
+
if (!keyAgreementKeyMultibase) {
|
|
50
|
+
throw new Error('The minted key-agreement key has no public multibase.');
|
|
51
|
+
}
|
|
52
|
+
const signingKeyMultibase = clientSigningKeyMultibase({ keyAgent });
|
|
53
|
+
const webvhUpdateKeys = await mintClientWebvhUpdateKeys();
|
|
54
|
+
const newClientKeys = {
|
|
55
|
+
signingKeyMultibase,
|
|
56
|
+
keyAgreementKeyMultibase,
|
|
57
|
+
updateKeyMultibase: await updateKeyMultibase({
|
|
58
|
+
seed: webvhUpdateKeys.updateSeed
|
|
59
|
+
}),
|
|
60
|
+
stagedUpdateKeyMultibase: await updateKeyMultibase({
|
|
61
|
+
seed: webvhUpdateKeys.stagedSeed
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
// The loud half: two log entries through the delegated bridge.
|
|
65
|
+
await selfEnrollWebvhClient({
|
|
66
|
+
store: logStore,
|
|
67
|
+
ladderSeed,
|
|
68
|
+
newClientKeys,
|
|
69
|
+
newClientUpdateSeeds: webvhUpdateKeys,
|
|
70
|
+
expectedDid
|
|
71
|
+
});
|
|
72
|
+
// Verify the continuation from the world-readable log -- the same
|
|
73
|
+
// first-contact read an enrollee's completion runs, and the controller the
|
|
74
|
+
// roster log's entry proofs are checked against.
|
|
75
|
+
const verified = await verifyAccountLog({
|
|
76
|
+
did: expectedDid,
|
|
77
|
+
spaceId: pointer.spaceId,
|
|
78
|
+
host: pointer.host,
|
|
79
|
+
...(accountLogPinStore ? { pinStore: accountLogPinStore } : {})
|
|
80
|
+
});
|
|
81
|
+
// The first roster read: signed with the `<did:webvh>#<multibase>` keyId
|
|
82
|
+
// the add entry just published, unwrapping the user key from the
|
|
83
|
+
// CREDENTIAL's standing wrap. First contact: the chain-head pin this read
|
|
84
|
+
// establishes is session-local here; the app's durable pin is established
|
|
85
|
+
// by its own first login read.
|
|
86
|
+
const zcapClient = webvhZcapClient({ keyAgent, did: expectedDid });
|
|
87
|
+
const store = userKeyRosterDescriptorStore({
|
|
88
|
+
storageServerUrl: pointer.host,
|
|
89
|
+
zcapClient,
|
|
90
|
+
spaceId: pointer.spaceId,
|
|
91
|
+
resolveController: async () => webvhResourceLogController({ did: expectedDid, log: verified.log }),
|
|
92
|
+
pinStore: memoryResourceLogPinStore(),
|
|
93
|
+
signer: userKeyRosterLogSigner({ keyAgent })
|
|
94
|
+
});
|
|
95
|
+
const read = await readUserKeyRoster({
|
|
96
|
+
store,
|
|
97
|
+
clientKeyAgreementKey: credentialKeyAgreementKey
|
|
98
|
+
});
|
|
99
|
+
if (!read) {
|
|
100
|
+
throw new Error('The account has no user key roster; it must finish provisioning ' +
|
|
101
|
+
'before a client can self-enroll.');
|
|
102
|
+
}
|
|
103
|
+
// Escrow the new client into the roster as its own recipient, so later
|
|
104
|
+
// logins on this browser read the roster the ordinary enrolled way.
|
|
105
|
+
// Idempotent: a wrap already standing is returned as-is.
|
|
106
|
+
await addUserKeyRosterRecipient({
|
|
107
|
+
store,
|
|
108
|
+
recipient: {
|
|
109
|
+
id: rosterRecipientKid({
|
|
110
|
+
signingKeyMultibase,
|
|
111
|
+
keyAgreementKeyMultibase
|
|
112
|
+
}),
|
|
113
|
+
publicKeyMultibase: keyAgreementKeyMultibase
|
|
114
|
+
},
|
|
115
|
+
ownerKeyAgreementKey: credentialKeyAgreementKey
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
clientSeed,
|
|
119
|
+
webvhUpdateKeys,
|
|
120
|
+
clientDid: keyAgent.id,
|
|
121
|
+
did: expectedDid,
|
|
122
|
+
userKey: read.userKey,
|
|
123
|
+
latestEpochId: read.latestEpochId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=selfEnroll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selfEnroll.js","sourceRoot":"","sources":["../../src/unlock/selfEnroll.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ProfileAgents } from '../identity/agents.js';
|
|
2
|
+
/**
|
|
3
|
+
* The HKDF salt for a standing credential's client-side expansions, and the
|
|
4
|
+
* per-key info labels. All permanent. The salt differs from the recovery
|
|
5
|
+
* code's client salt (`freewallet/recovery/client-keys/v1`), so a code and a
|
|
6
|
+
* standing method that somehow shared input material could still never derive
|
|
7
|
+
* the same client identity.
|
|
8
|
+
*/
|
|
9
|
+
export declare const STANDING_CLIENT_SALT = "freewallet/unlock/standing-client/v1";
|
|
10
|
+
/**
|
|
11
|
+
* A credential-derived client identity, assembled from its 32-byte client
|
|
12
|
+
* seed: the derived agents, the client did:key, the public multibases the
|
|
13
|
+
* document and roster carry, and the roster recipient kid. The shared shape
|
|
14
|
+
* of a standing credential's identity and a recovery code's (which extends it
|
|
15
|
+
* with the code's single update key).
|
|
16
|
+
*/
|
|
17
|
+
export interface UnlockClientIdentity {
|
|
18
|
+
clientSeed: Uint8Array;
|
|
19
|
+
agents: ProfileAgents;
|
|
20
|
+
clientDid: string;
|
|
21
|
+
signingKeyMultibase: string;
|
|
22
|
+
keyAgreementKeyMultibase: string;
|
|
23
|
+
/**
|
|
24
|
+
* The kid of the credential's user-key-roster entry -- its key-agreement
|
|
25
|
+
* key's id exactly as `agentsFromSeed` derives it (`did:key:<ed>#<x>`), so
|
|
26
|
+
* the wrap minted at bind time is the one a fresh browser's roster read
|
|
27
|
+
* looks for.
|
|
28
|
+
*/
|
|
29
|
+
recipientKid: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Assembles a client identity from its 32-byte client seed: the one place the
|
|
33
|
+
* agents, multibases, and roster kid are derived, shared by the standing
|
|
34
|
+
* credential derivation here and the recovery code's
|
|
35
|
+
* (`recoveryClientFromCode`), so the two postures can never disagree on how a
|
|
36
|
+
* seed becomes an identity.
|
|
37
|
+
*
|
|
38
|
+
* @param options {object}
|
|
39
|
+
* @param options.clientSeed {Uint8Array} the 32-byte client seed
|
|
40
|
+
* @returns {Promise<UnlockClientIdentity>}
|
|
41
|
+
*/
|
|
42
|
+
export declare function unlockClientIdentityFromSeed({ clientSeed }: {
|
|
43
|
+
clientSeed: Uint8Array;
|
|
44
|
+
}): Promise<UnlockClientIdentity>;
|
|
45
|
+
/**
|
|
46
|
+
* A standing unlock credential's full client-side key set: the client
|
|
47
|
+
* identity plus the binding MAC key that authenticates the unlock record's
|
|
48
|
+
* account core (computed at bind time, verified before the pointer is
|
|
49
|
+
* trusted -- the storage host never holds it).
|
|
50
|
+
*/
|
|
51
|
+
export interface StandingUnlockClient extends UnlockClientIdentity {
|
|
52
|
+
bindingMacKey: Uint8Array;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Derives a standing credential's client key set from the method's 32-byte
|
|
56
|
+
* unlock seed (the output of `deriveUnlockSeed` under the method's own KDF).
|
|
57
|
+
* Deterministic: the same secret under the same KDF always yields the same
|
|
58
|
+
* key set, which is what makes a fresh browser's self-enrollment possible
|
|
59
|
+
* with nothing but the credential in hand.
|
|
60
|
+
*
|
|
61
|
+
* @param options {object}
|
|
62
|
+
* @param options.unlockSeed {Uint8Array} the method's 32-byte unlock seed
|
|
63
|
+
* @returns {Promise<StandingUnlockClient>}
|
|
64
|
+
*/
|
|
65
|
+
export declare function standingClientFromUnlockSeed({ unlockSeed }: {
|
|
66
|
+
unlockSeed: Uint8Array;
|
|
67
|
+
}): Promise<StandingUnlockClient>;
|
|
68
|
+
//# sourceMappingURL=standingClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standingClient.d.ts","sourceRoot":"","sources":["../../src/unlock/standingClient.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,yCAAyC,CAAA;AAI1E;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,UAAU,CAAA;IACtB,MAAM,EAAE,aAAa,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,wBAAwB,EAAE,MAAM,CAAA;IAChC;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,CAAC,EACjD,UAAU,EACX,EAAE;IACD,UAAU,EAAE,UAAU,CAAA;CACvB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAgBhC;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,aAAa,EAAE,UAAU,CAAA;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,CAAC,EACjD,UAAU,EACX,EAAE;IACD,UAAU,EAAE,UAAU,CAAA;CACvB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkBhC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The standing unlock credential's client identity: the deterministic key set
|
|
6
|
+
* every unlock method (a passphrase, a passkey PRF output) derives from its
|
|
7
|
+
* own unlock seed under the standing-credential posture -- the recovery-code
|
|
8
|
+
* posture minus spend-on-use. The credential's key-agreement key holds a
|
|
9
|
+
* standing wrap in the user key roster (escrowed into every epoch, kept alive
|
|
10
|
+
* by rotation fan-out), and its binding MAC key authenticates the unlock
|
|
11
|
+
* record's account core, so a fresh browser holding nothing but the
|
|
12
|
+
* credential can locate the account, prove the record genuine, and decrypt.
|
|
13
|
+
* Update authority stays latent and rides the record instead: the ladder seed
|
|
14
|
+
* (`./ladder`) never derives from the secret.
|
|
15
|
+
*
|
|
16
|
+
* The derivations are wire-level: two wallet apps must produce byte-identical
|
|
17
|
+
* output for the same secret and KDF, so every salt and info label below is
|
|
18
|
+
* permanent. The HKDF input is the method's own 32-byte unlock seed
|
|
19
|
+
* (`deriveUnlockSeed`), so the expensive passphrase stretch runs once per
|
|
20
|
+
* typed secret and each unlock method's distinct KDF salt keeps two methods'
|
|
21
|
+
* client identities from ever colliding.
|
|
22
|
+
*/
|
|
23
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
24
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
25
|
+
import { agentsFromSeed } from '../identity/agents.js';
|
|
26
|
+
/**
|
|
27
|
+
* The HKDF salt for a standing credential's client-side expansions, and the
|
|
28
|
+
* per-key info labels. All permanent. The salt differs from the recovery
|
|
29
|
+
* code's client salt (`freewallet/recovery/client-keys/v1`), so a code and a
|
|
30
|
+
* standing method that somehow shared input material could still never derive
|
|
31
|
+
* the same client identity.
|
|
32
|
+
*/
|
|
33
|
+
export const STANDING_CLIENT_SALT = 'freewallet/unlock/standing-client/v1';
|
|
34
|
+
const STANDING_CLIENT_SEED_INFO = 'client-seed';
|
|
35
|
+
const STANDING_BINDING_MAC_INFO = 'binding-mac';
|
|
36
|
+
/**
|
|
37
|
+
* Assembles a client identity from its 32-byte client seed: the one place the
|
|
38
|
+
* agents, multibases, and roster kid are derived, shared by the standing
|
|
39
|
+
* credential derivation here and the recovery code's
|
|
40
|
+
* (`recoveryClientFromCode`), so the two postures can never disagree on how a
|
|
41
|
+
* seed becomes an identity.
|
|
42
|
+
*
|
|
43
|
+
* @param options {object}
|
|
44
|
+
* @param options.clientSeed {Uint8Array} the 32-byte client seed
|
|
45
|
+
* @returns {Promise<UnlockClientIdentity>}
|
|
46
|
+
*/
|
|
47
|
+
export async function unlockClientIdentityFromSeed({ clientSeed }) {
|
|
48
|
+
const agents = await agentsFromSeed({ seed: clientSeed });
|
|
49
|
+
const { publicKeyMultibase: keyAgreementKeyMultibase } = agents.keyAgreementKey;
|
|
50
|
+
if (!keyAgreementKeyMultibase) {
|
|
51
|
+
throw new Error('The derived key-agreement key has no public multibase.');
|
|
52
|
+
}
|
|
53
|
+
const [, , signingKeyMultibase] = agents.keyAgent.id.split(':');
|
|
54
|
+
return {
|
|
55
|
+
clientSeed,
|
|
56
|
+
agents,
|
|
57
|
+
clientDid: agents.keyAgent.id,
|
|
58
|
+
signingKeyMultibase: signingKeyMultibase,
|
|
59
|
+
keyAgreementKeyMultibase,
|
|
60
|
+
recipientKid: `${agents.keyAgent.id}#${keyAgreementKeyMultibase}`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Derives a standing credential's client key set from the method's 32-byte
|
|
65
|
+
* unlock seed (the output of `deriveUnlockSeed` under the method's own KDF).
|
|
66
|
+
* Deterministic: the same secret under the same KDF always yields the same
|
|
67
|
+
* key set, which is what makes a fresh browser's self-enrollment possible
|
|
68
|
+
* with nothing but the credential in hand.
|
|
69
|
+
*
|
|
70
|
+
* @param options {object}
|
|
71
|
+
* @param options.unlockSeed {Uint8Array} the method's 32-byte unlock seed
|
|
72
|
+
* @returns {Promise<StandingUnlockClient>}
|
|
73
|
+
*/
|
|
74
|
+
export async function standingClientFromUnlockSeed({ unlockSeed }) {
|
|
75
|
+
const salt = new TextEncoder().encode(STANDING_CLIENT_SALT);
|
|
76
|
+
const clientSeed = hkdf(sha256, unlockSeed, salt, new TextEncoder().encode(STANDING_CLIENT_SEED_INFO), 32);
|
|
77
|
+
const bindingMacKey = hkdf(sha256, unlockSeed, salt, new TextEncoder().encode(STANDING_BINDING_MAC_INFO), 32);
|
|
78
|
+
const identity = await unlockClientIdentityFromSeed({ clientSeed });
|
|
79
|
+
return { ...identity, bindingMacKey };
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=standingClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standingClient.js","sourceRoot":"","sources":["../../src/unlock/standingClient.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAGtD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,sCAAsC,CAAA;AAC1E,MAAM,yBAAyB,GAAG,aAAa,CAAA;AAC/C,MAAM,yBAAyB,GAAG,aAAa,CAAA;AAwB/C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,EACjD,UAAU,EAGX;IACC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;IACzD,MAAM,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,GACpD,MAAM,CAAC,eAA6D,CAAA;IACtE,IAAI,CAAC,wBAAwB,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAA;IAC3E,CAAC;IACD,MAAM,CAAC,EAAE,AAAD,EAAG,mBAAmB,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/D,OAAO;QACL,UAAU;QACV,MAAM;QACN,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE;QAC7B,mBAAmB,EAAE,mBAAoB;QACzC,wBAAwB;QACxB,YAAY,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,wBAAwB,EAAE;KAClE,CAAA;AACH,CAAC;AAYD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAAC,EACjD,UAAU,EAGX;IACC,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;IAC3D,MAAM,UAAU,GAAG,IAAI,CACrB,MAAM,EACN,UAAU,EACV,IAAI,EACJ,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,yBAAyB,CAAC,EACnD,EAAE,CACH,CAAA;IACD,MAAM,aAAa,GAAG,IAAI,CACxB,MAAM,EACN,UAAU,EACV,IAAI,EACJ,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,yBAAyB,CAAC,EACnD,EAAE,CACH,CAAA;IACD,MAAM,QAAQ,GAAG,MAAM,4BAA4B,CAAC,EAAE,UAAU,EAAE,CAAC,CAAA;IACnE,OAAO,EAAE,GAAG,QAAQ,EAAE,aAAa,EAAE,CAAA;AACvC,CAAC"}
|