@keri-ts/wallet 0.0.0-stage → 2.0.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.
@@ -0,0 +1,137 @@
1
+ /** Optional encrypted derivation capability; ordinary wallet inventory never constructs or reads it. */
2
+ import { bytesEqual, makeBlindState, makeBoundState, MtrDex, Salter } from "cesr-ts";
3
+ import { Komer } from "keri-ts/db";
4
+ import { RawRecord, ValidationError, vetBlind } from "keri-ts/runtime";
5
+ /** Public event-specific inputs; neither a salt nor a derived opening is retained here. */
6
+ class DerivedRecord extends RawRecord {
7
+ }
8
+ /** Explicit capability over an existing wallet environment and accepted public registry evidence. */
9
+ export class RegistryCustody {
10
+ /** Borrow owners only; construction opens no table and loads no encrypted row. */
11
+ constructor(options) {
12
+ Object.defineProperty(this, "options", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: options
17
+ });
18
+ Object.defineProperty(this, "env", {
19
+ enumerable: true,
20
+ configurable: true,
21
+ writable: true,
22
+ value: null
23
+ });
24
+ Object.defineProperty(this, "derivations", {
25
+ enumerable: true,
26
+ configurable: true,
27
+ writable: true,
28
+ value: void 0
29
+ });
30
+ this.owner();
31
+ }
32
+ /** Bind the actual local habitat and its key owners, never an AID label alone. */
33
+ owner() {
34
+ const { hby, hab } = this.options;
35
+ if (hby.habs.get(hab.pre) !== hab || hab.ks !== hby.ks || hab.mgr !== hby.mgr || hab.db !== hby.db) {
36
+ throw new ValidationError("Custody requires the actual local habitat and Keeper owners");
37
+ }
38
+ }
39
+ /** Rebind optional tables after the caller reopens the same wallet owner. */
40
+ tables() {
41
+ const { db } = this.options;
42
+ if (!db.opened || !db.env)
43
+ throw new ValidationError("Wallet inventory is closed");
44
+ if (this.env !== db.env) {
45
+ this.derivations = new Komer(db, { subkey: "derived-updates.", recordClass: DerivedRecord });
46
+ this.env = db.env;
47
+ }
48
+ return { derivations: this.derivations };
49
+ }
50
+ /** Load only this habitat's Keeper root; possession confers no protocol authority. */
51
+ load(registry) {
52
+ this.owner();
53
+ const root = this.options.hby.mgr.getRegistryRoot(this.options.hab.pre, registry);
54
+ if (!root)
55
+ throw new ValidationError("Registry custody is not provisioned");
56
+ return root;
57
+ }
58
+ /** Provision an issuer-selected root once, refusing replacement behind opaque accepted history. */
59
+ createCustody(registry) {
60
+ this.owner();
61
+ const { hby, hab } = this.options;
62
+ if (!hby.mgr.aeid || !hby.mgr.seed) {
63
+ throw new ValidationError("Private credential custody requires an authenticated encrypted Keeper");
64
+ }
65
+ if (hby.mgr.getRegistryRoot(hab.pre, registry))
66
+ return { registry };
67
+ const rip = this.options.registry.seqEvent(registry, 0);
68
+ if (!rip || rip.issuer !== hab.pre) {
69
+ throw new ValidationError("New custody requires the accepted registry issuer's actual local habitat");
70
+ }
71
+ // Existing clone validation authenticates public openings without another parser or proof algorithm.
72
+ for (const _ of this.options.registry.cloneTelIter(registry)) { /* consume the accepted public prefix */ }
73
+ const head = this.options.registry.headEvent(registry);
74
+ const last = head ? Number(BigInt(`0x${head.sad.n}`)) : 0;
75
+ if (!Number.isSafeInteger(last))
76
+ throw new ValidationError("Accepted registry ordinal is not representable");
77
+ for (let ordinal = 1; ordinal <= last; ordinal++) {
78
+ const event = this.options.registry.seqEvent(registry, ordinal);
79
+ if (!event)
80
+ throw new ValidationError("Accepted registry history is incomplete");
81
+ if (event.ilk === "bup" && !this.options.registry.opening(event.said)) {
82
+ throw new ValidationError("Local custody is missing for blinded registry history; salt cannot be recreated");
83
+ }
84
+ }
85
+ hby.mgr.putRegistryRoot(hab.pre, registry, new Salter({ code: MtrDex.Salt_128, raw: crypto.getRandomValues(new Uint8Array(16)) }));
86
+ return { registry };
87
+ }
88
+ /** Derive a candidate only; consumers still authenticate its exact accepted event and bound KEL reference. */
89
+ derive(registry, ordinal, credential, state, options = {}) {
90
+ if ((typeof ordinal !== "bigint" && (typeof ordinal !== "number" || !Number.isSafeInteger(ordinal))) || ordinal < 1) {
91
+ throw new ValidationError("Update ordinal must be a positive integer");
92
+ }
93
+ const root = this.load(registry);
94
+ const values = { salt: root.qb64, sn: ordinal, tier: "low", acdc: credential, state };
95
+ return options.bound
96
+ ? makeBoundState({ ...values, bsn: options.bsn ?? 0, bd: options.bd ?? "" })
97
+ : makeBlindState(values);
98
+ }
99
+ /** Persist exact nonsecret coordinates before processing/anchoring a newly derived BUP. */
100
+ retainDerivedUpdate(event, opening) {
101
+ if (event.ilk !== "bup" || !event.verify() || !vetBlind(opening, event.sad.b)) {
102
+ throw new ValidationError("Derived update requires its exact authenticated BUP opening");
103
+ }
104
+ const value = {
105
+ credential: opening.td.nonce,
106
+ state: opening.ts.label,
107
+ bound: "bn" in opening ? { bn: opening.bn.num.toString(16), bd: opening.bd.nonce } : null,
108
+ };
109
+ const rows = this.tables().derivations, old = rows.get(event.said);
110
+ if (old && JSON.stringify(old.asDict()) !== JSON.stringify(value)) {
111
+ throw new ValidationError("Different derivation coordinates are already retained for this event");
112
+ }
113
+ rows.pin(event.said, value);
114
+ return value;
115
+ }
116
+ /** Reproduce only the exact accepted event, never today's head or a later credential's coordinates. */
117
+ derivedOpening(event) {
118
+ if (event.ilk !== "bup" || !event.verify())
119
+ throw new ValidationError("Exact accepted blinded update is required");
120
+ const ordinal = BigInt(`0x${event.sad.n}`);
121
+ if (!Number.isSafeInteger(Number(ordinal))) {
122
+ throw new ValidationError("Accepted registry ordinal is not representable");
123
+ }
124
+ const stored = event.regid ? this.options.registry.seqEvent(event.regid, Number(ordinal)) : null;
125
+ if (!stored || !bytesEqual(stored.raw, event.raw)) {
126
+ throw new ValidationError("Exact accepted registry event is required");
127
+ }
128
+ const value = this.tables().derivations.get(event.said);
129
+ if (!value)
130
+ throw new ValidationError("Exact derived update coordinates are unavailable");
131
+ const opening = this.derive(event.regid, ordinal, value.credential, value.state, value.bound ? { bound: true, bsn: BigInt(`0x${value.bound.bn}`), bd: value.bound.bd } : {});
132
+ if (!vetBlind(opening, event.sad.b)) {
133
+ throw new ValidationError("Derived opening does not match the exact accepted event");
134
+ }
135
+ return opening;
136
+ }
137
+ }
@@ -0,0 +1,528 @@
1
+ /** Finite ACDC v2 IPEX composition over exact wallet views and explicitly supplied private capability. */
2
+ import { collectPayload, Compactor, concatBytes, Counter, Diger, nestedPayload, NumberPrimitive, parseBytes, Prefixer, SealSource, SerderACDC, SerderKERI, unblindBlindState, unblindBoundState, Vrsn_2_0, } from "cesr-ts";
3
+ import { call } from "effection";
4
+ import { publicOpeningAttachment } from "keri-ts/db";
5
+ import { acceptedEventReplayMessage, acceptedRegistryEvidence, authenticateIpexNode, envelopeFromFrame, evaluateChain, ipexExnBody, isLocalGroupHab, issuedNodeVerdict, Kever, matchesRegistrySeal, prepareIpexExn, validateAcdcSchema, ValidationError, verifyExchangeProofs, vet, vetBinds, vetBlind, vetBound, walkIpexGraph, } from "keri-ts/runtime";
6
+ /** Reuse a matching accepted KEL seal, or author once and gather real witness receipts. */
7
+ export function* anchorExchange(wallet, receiptor, hab, body, auths) {
8
+ if (receiptor.hby !== wallet.hby)
9
+ throw new ValidationError("Receipt owner must share this wallet's Habery.");
10
+ let anchor = null, event = hab.kever.serder;
11
+ while (event) {
12
+ if (event.seals.some((seal) => body instanceof SerderACDC
13
+ ? matchesRegistrySeal(seal, body)
14
+ : seal && typeof seal === "object" && "d" in seal && seal.d === body.said)) {
15
+ anchor = event;
16
+ }
17
+ event = event.prior ? hab.db.getEvtSerder(hab.pre, event.prior) : null;
18
+ }
19
+ if (!anchor) {
20
+ const data = body instanceof SerderACDC
21
+ ? [{ i: body.regid ?? body.said, s: body.sad.n, d: body.said }]
22
+ : [{ d: body.said }];
23
+ if (hab.kever.estOnly)
24
+ hab.rotate({ data });
25
+ else
26
+ hab.interact({ data });
27
+ anchor = hab.kever.serder;
28
+ }
29
+ yield* receiptor.receipt(hab.pre, { sn: anchor.sn, allowTcp: true, auths });
30
+ if (!hab.db.fullyWitnessed(anchor))
31
+ throw new ValidationError("Source anchor remains pending witness receipts.");
32
+ return { s: NumberPrimitive.fromNum(BigInt(anchor.sn)), d: new Diger({ qb64: anchor.said }) };
33
+ }
34
+ /** Encode one frozen body and its existing prepared nests; source proofs do not alter its SAID. */
35
+ export function exchangeWire(hab, exn, nested = new Uint8Array(), source, gvrsn = Vrsn_2_0) {
36
+ const proof = source
37
+ ? Counter.enclose({ code: "SealSourceCouples", qb64: SealSource.qb64b(source), version: gvrsn })
38
+ : concatBytes(...parseBytes(hab.endorse(exn, { gvrsn, pipelined: false, genusify: true }))[0].attachments.map((g) => g.raw));
39
+ return concatBytes(Counter.makeGVC(gvrsn), exn.raw, Counter.enclose({ qb64: concatBytes(proof, nested), version: gvrsn }));
40
+ }
41
+ /** Borrow concrete owners only. No constructor opens a store, starts a worker or loads registry secrets. */
42
+ export class WalletDiscloser {
43
+ constructor(wallet, receiptor, custody) {
44
+ Object.defineProperty(this, "wallet", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: wallet
49
+ });
50
+ Object.defineProperty(this, "receiptor", {
51
+ enumerable: true,
52
+ configurable: true,
53
+ writable: true,
54
+ value: receiptor
55
+ });
56
+ Object.defineProperty(this, "custody", {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: custody
61
+ });
62
+ if (receiptor.hby !== wallet.hby) {
63
+ throw new ValidationError("Disclosure receipt owner must share the wallet Habery.");
64
+ }
65
+ if (custody
66
+ && (custody.options.hby !== wallet.hby || custody.options.registry !== wallet.registry
67
+ || custody.options.db !== wallet.db)) {
68
+ throw new ValidationError("Disclosure custody must use this wallet's actual owners.");
69
+ }
70
+ }
71
+ /** Resolve only a local individual signer; group approval composes this body in the group workflow. */
72
+ signer(sender, gvrsn = Vrsn_2_0) {
73
+ if (gvrsn.major < 2)
74
+ throw new ValidationError("ACDC v2 disclosure requires attachment genus 2 or later.");
75
+ const hab = this.wallet.hby.habs.get(sender);
76
+ if (!hab || isLocalGroupHab(this.wallet.hby, hab)) {
77
+ throw new ValidationError("ACDC v2 disclosure requires a local individual signer.");
78
+ }
79
+ return hab;
80
+ }
81
+ /** Revalidate exact accepted prior bytes without treating notification or salt possession as acceptance. */
82
+ *prior(said) {
83
+ if (!said)
84
+ return undefined;
85
+ const body = this.wallet.hby.db.exns.get(said);
86
+ if (!body || !this.wallet.exc.complete(said) || !this.wallet.exc.verifyAccepted(said, body.route)) {
87
+ throw new ValidationError("Prior ACDC v2 IPEX exchange must be accepted and authenticated.");
88
+ }
89
+ yield* call(async () => {
90
+ for await (const _ of this.wallet.exc.replay(said)) { /* authenticated retained outer proof */ }
91
+ });
92
+ return body;
93
+ }
94
+ /** Reduce only the selected attribute view using the existing commitment-preserving Compactor. */
95
+ partial(credential, paths) {
96
+ if (!paths?.length)
97
+ return credential;
98
+ if (!credential.sad.a || typeof credential.sad.a !== "object" || Array.isArray(credential.sad.a)) {
99
+ throw new ValidationError("Attribute disclosure requires an expanded selected attribute map.");
100
+ }
101
+ const block = new Compactor({
102
+ mad: structuredClone(credential.sad.a),
103
+ kind: credential.kind,
104
+ verify: false,
105
+ });
106
+ block.compact([...paths], "a");
107
+ const partial = new SerderACDC({
108
+ sad: { ...credential.sad, a: block.partials[paths.join("|")].mad },
109
+ kind: credential.kind,
110
+ makify: true,
111
+ });
112
+ if (partial.said !== credential.said) {
113
+ throw new ValidationError("Disclosure changed the selected credential commitment.");
114
+ }
115
+ return partial;
116
+ }
117
+ /** Authenticate every selected node, then authorize only the shared complete-root expression. */
118
+ requireGraph(origin, raw, pendingPresentation = false) {
119
+ const graph = walkIpexGraph(origin, parseBytes(raw), true);
120
+ if (!graph)
121
+ throw new ValidationError("Selected credential graph is incomplete.");
122
+ const verdicts = new Map();
123
+ for (const [said, node] of graph.nodes) {
124
+ if (pendingPresentation) {
125
+ const schema = validateAcdcSchema(node.body, this.wallet.hby.db);
126
+ if (schema.kind !== "valid") {
127
+ throw new ValidationError(schema.kind === "missing" ? `Missing ACDC schema: ${schema.said}` : schema.reason);
128
+ }
129
+ }
130
+ const envelope = envelopeFromFrame(node);
131
+ const auth = authenticateIpexNode(node.body, envelope, this.wallet.hby.db, this.wallet.registry);
132
+ let verdict = issuedNodeVerdict(auth);
133
+ if (pendingPresentation && auth.kind === "reject" && node.body.regid) {
134
+ // Ordinary node authentication deliberately rejects absent current openings.
135
+ // File consent may wait for that specific evidence, never for a known invalid state.
136
+ const chain = acceptedRegistryEvidence(this.wallet.registry, node.body.regid);
137
+ const binding = chain && vetBinds({
138
+ ...chain,
139
+ db: this.wallet.hby.db,
140
+ acdc: node.body,
141
+ target: said,
142
+ blinders: [...envelope.bsqs, ...envelope.bsss],
143
+ });
144
+ const historical = authenticateIpexNode(node.body, envelope, this.wallet.hby.db, this.wallet.registry, true);
145
+ if (binding?.kind === "missing" && binding.reason === "disclosure"
146
+ && historical.kind === "authenticated" && historical.registration?.state === "issued") {
147
+ verdict = { kind: "unknown", retryable: true, reason: "Missing current registry opening.", schemas: [] };
148
+ }
149
+ }
150
+ verdicts.set(said, verdict);
151
+ }
152
+ const current = evaluateChain(graph, verdicts, this.wallet.hby.db);
153
+ if (current?.kind === "valid")
154
+ return;
155
+ if (pendingPresentation && current?.kind === "unknown" && current.retryable) {
156
+ const historical = new Map();
157
+ for (const [said, node] of graph.nodes) {
158
+ historical.set(said, issuedNodeVerdict(authenticateIpexNode(node.body, envelopeFromFrame(node), this.wallet.hby.db, this.wallet.registry, true)));
159
+ }
160
+ if (evaluateChain(graph, historical, this.wallet.hby.db)?.kind === "valid")
161
+ return;
162
+ }
163
+ throw new ValidationError("Current authenticated graph truth is required before use.");
164
+ }
165
+ /** Recover a candidate using an explicitly possessed UUID; this neither derives a salt nor grants authority. */
166
+ unblind(opening, credential, states, bounds = []) {
167
+ if (!opening.u.nonce)
168
+ throw new ValidationError("Unblinding requires an explicitly possessed disclosure UUID.");
169
+ const options = { said: opening.d.nonce, uuid: opening.u.nonce, acdc: credential, states: [...states] };
170
+ return "bn" in opening
171
+ ? unblindBoundState({ ...options, bounds: [[opening.bn.num, opening.bd.nonce], ...bounds] })
172
+ : unblindBlindState(options);
173
+ }
174
+ /** Attach exact accepted issuer openings from head through vacuous suffix to this credential's binding. */
175
+ registryProof(credential, registry, retained = [], options = {}) {
176
+ const chain = acceptedRegistryEvidence(this.wallet.registry, registry);
177
+ if (!chain || chain.updates.some((update) => update.ilk !== "bup")) {
178
+ throw new ValidationError("Accepted ACDC v2 registry history is required.");
179
+ }
180
+ if (options.derive && !this.custody)
181
+ throw new ValidationError("Explicit derivation requires provisioned custody.");
182
+ const proofs = [];
183
+ let binding = null;
184
+ for (const event of [...chain.updates].reverse()) {
185
+ const blid = event.sad.b;
186
+ let opening = retained.find((proof) => proof.d.nonce === blid) ?? this.wallet.registry.publicOpening(event.said);
187
+ if (!opening && options.derive) {
188
+ // Exact retained coordinates are required; missing local material is a refusal.
189
+ opening = this.custody.derivedOpening(event);
190
+ }
191
+ if (!opening || !vetBlind(opening, blid) || (opening.td.nonce && opening.td.nonce !== credential.said)
192
+ || vetBound(opening, credential, this.wallet.hby.db)) {
193
+ throw new ValidationError("Current exact registry opening is unavailable or binds different evidence.");
194
+ }
195
+ proofs.push(publicOpeningAttachment(opening));
196
+ if (opening.td.nonce) {
197
+ binding = event;
198
+ break;
199
+ }
200
+ }
201
+ if (!binding)
202
+ throw new ValidationError("Registry has no disclosed credential binding.");
203
+ if (!credential.regid) {
204
+ proofs.push(Counter.enclose({
205
+ code: "SealSourceTriples",
206
+ version: Vrsn_2_0,
207
+ qb64: concatBytes(new Prefixer({ qb64: registry }).qb64b, NumberPrimitive.fromNum(BigInt(`0x${binding.sad.n}`)).qb64b, new Diger({ qb64: binding.said }).qb64b),
208
+ }));
209
+ }
210
+ return concatBytes(Counter.makeGVC(Vrsn_2_0), credential.raw, Counter.enclose({ qb64: concatBytes(...proofs), version: Vrsn_2_0 }));
211
+ }
212
+ /** Select immutable admitted/transferred nests; proof refresh changes attachments only when explicitly requested. */
213
+ *selected(selection, offer = false) {
214
+ if ([selection.said, selection.fromGrant, selection.transferred].filter(value => value !== undefined).length !== 1) {
215
+ throw new ValidationError("Select exactly one issued credential, retained Grant or transferred view.");
216
+ }
217
+ const retained = Boolean(selection.fromGrant || selection.transferred);
218
+ if (selection.refresh && !retained)
219
+ throw new ValidationError("Refresh requires a retained view.");
220
+ if (retained && selection.artifacts) {
221
+ throw new ValidationError("A retained Grant keeps its exact original artifacts.");
222
+ }
223
+ if (retained && !selection.refresh
224
+ && (selection.derive || selection.openings?.length)) {
225
+ throw new ValidationError("New held-Grant proof requires explicit refresh.");
226
+ }
227
+ let held = selection.fromGrant
228
+ ? yield* call(() => this.wallet.heldCredential(selection.fromGrant, { presenterAuthorized: selection.presenterAuthorized }))
229
+ : null;
230
+ if (selection.transferred) {
231
+ if (selection.presenterAuthorized !== true) {
232
+ throw new ValidationError("Transferred view requires explicit presenter authorization.");
233
+ }
234
+ const frames = parseBytes(concatBytes(Counter.makeGVC(Vrsn_2_0), selection.transferred.slice()));
235
+ if (!frames.length || frames.some(frame => !(frame.body instanceof SerderACDC) || frame.nests?.length)) {
236
+ throw new ValidationError("Transferred view must contain direct credential node frames.");
237
+ }
238
+ const streams = yield* call(() => Promise.all(frames.map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
239
+ held = { credential: frames[0].body, proof: streams[0], artifacts: streams.slice(1) };
240
+ }
241
+ const credential = this.partial(held?.credential ?? this.wallet.issuedCredential(selection.said), selection.disclose);
242
+ const original = held ? parseBytes(concatBytes(Counter.makeGVC(Vrsn_2_0), held.proof))[0] : null;
243
+ let origin;
244
+ if (offer)
245
+ origin = concatBytes(credential.raw, Counter.enclose({ qb64: new Uint8Array() }));
246
+ else if (held && !selection.refresh) {
247
+ origin = concatBytes(credential.raw, Counter.enclose({ qb64: concatBytes(...original.attachments.map((group) => group.raw)) }));
248
+ }
249
+ else {
250
+ const envelope = original ? envelopeFromFrame(original) : null;
251
+ const candidates = [
252
+ ...new Set(envelope?.ssts.filter((seal) => this.wallet.registry.seqEvent(seal.i.qb64, 0)).map((seal) => seal.i.qb64)),
253
+ ];
254
+ const regid = credential.regid
255
+ ?? (selection.said ? this.wallet.db.issued.get([selection.said])?.registry : undefined)
256
+ ?? (candidates.length === 1 ? candidates[0] : undefined);
257
+ if (!regid)
258
+ throw new ValidationError("Selected issuer registry proof is unavailable.");
259
+ origin = this.registryProof(credential, regid, [
260
+ ...(envelope?.bsqs ?? []),
261
+ ...(envelope?.bsss ?? []),
262
+ ...(selection.openings ?? []),
263
+ ], selection);
264
+ }
265
+ const artifacts = selection.artifacts ?? held?.artifacts ?? [];
266
+ return concatBytes(Counter.makeGVC(Vrsn_2_0), origin, ...artifacts.map((raw) => offer
267
+ ? concatBytes(parseBytes(concatBytes(Counter.makeGVC(Vrsn_2_0), raw))[0].body.raw, Counter.enclose({ qb64: new Uint8Array() }))
268
+ : raw));
269
+ }
270
+ /** Create an application request; no credential-use approval is invented for negotiation. */
271
+ *prepareApply(sender, options) {
272
+ const hab = this.signer(sender, options.gvrsn ?? Vrsn_2_0), exn = ipexExnBody(sender, "/ipex/apply", [], options);
273
+ return {
274
+ exn,
275
+ wire: exchangeWire(hab, exn, undefined, undefined, options.gvrsn ?? Vrsn_2_0),
276
+ support: new Uint8Array(),
277
+ };
278
+ }
279
+ /** Offer only the selected commitment/view without issuer proof attachments. */
280
+ *prepareOffer(sender, selection, options) {
281
+ const hab = this.signer(sender, options.gvrsn ?? Vrsn_2_0), prior = yield* this.prior(options.priorSaid);
282
+ const raw = yield* this.selected(selection, true);
283
+ const [exn, nests] = yield* prepareIpexExn(sender, "/ipex/offer", raw, { ...options, prior });
284
+ return {
285
+ exn,
286
+ wire: exchangeWire(hab, exn, nests, undefined, options.gvrsn ?? Vrsn_2_0),
287
+ support: new Uint8Array(),
288
+ };
289
+ }
290
+ /** Accept or decline the exact authenticated predecessor and its negotiated anchoring choice. */
291
+ *prepareReply(sender, verb, priorSaid, message = "", recipient, auths) {
292
+ const hab = this.signer(sender), prior = yield* this.prior(priorSaid);
293
+ const exn = ipexExnBody(sender, `/ipex/${verb}`, [], { message, prior, recipient });
294
+ const source = verb === "agree" && exn.sad.a.ax?.[0]
295
+ ? yield* anchorExchange(this.wallet, this.receiptor, hab, exn, auths)
296
+ : undefined;
297
+ return { exn, wire: exchangeWire(hab, exn, undefined, source), support: source ? this.kel(hab) : new Uint8Array() };
298
+ }
299
+ /** Select and authorize once before freezing a Grant body, without signing or committing group approval. */
300
+ *prepareGrantBody(sender, selection, options) {
301
+ return yield* this.grantBody(sender, selection, options, false);
302
+ }
303
+ /** Freeze an endorsement file from issued evidence, allowing only retryable current-opening absence. */
304
+ *prepareGrantFileBody(sender, selection, options) {
305
+ return yield* this.grantBody(sender, selection, options, true);
306
+ }
307
+ /** Both public preparation profiles preserve the same selected view and body construction. */
308
+ *grantBody(sender, selection, options, pendingPresentation) {
309
+ const hab = this.wallet.hby.habs.get(sender);
310
+ if (!hab?.kever || (options.gvrsn ?? Vrsn_2_0).major < 2) {
311
+ throw new ValidationError("ACDC v2 Grant preparation requires an accepted local sender and attachment genus 2.");
312
+ }
313
+ if (selection.derive && this.custody?.options.hab !== hab) {
314
+ throw new ValidationError("Derived disclosure requires the actual sender habitat's custody.");
315
+ }
316
+ const prior = yield* this.prior(options.priorSaid), raw = yield* this.selected(selection);
317
+ this.requireGraph(parseBytes(raw)[0].body.said, raw, pendingPresentation);
318
+ const [exn, nests] = yield* prepareIpexExn(sender, "/ipex/grant", raw, { ...options, prior });
319
+ return { exn, raw, nests };
320
+ }
321
+ /** Inspect one exact unanchored Grant without dropping invalid signatures or publishing any evidence. */
322
+ inspectGrantFile(raw, selection) {
323
+ const frames = parseBytes(raw), frame = frames[0], message = frame?.body;
324
+ if (frames.length !== 1 || !(message instanceof SerderKERI) || !message.verify()
325
+ || message.pvrsn.major !== 2 || message.pvrsn.minor !== 0 || !["xip", "exn"].includes(message.ilk ?? "")
326
+ || message.route !== "/ipex/grant" || message.sad.a.ax?.[0]
327
+ || frame.essrs?.length || !frame.nests?.length
328
+ || frame.nests.some(node => !(node.body instanceof SerderACDC) || node.nests?.length || node.essrs?.length)
329
+ || selection && (message.said !== selection.said || message.pre !== selection.sender
330
+ || message.sad.ri !== selection.recipient)) {
331
+ throw new ValidationError("Select one exact unanchored v2 Grant, its disclosed nodes and declared parties.");
332
+ }
333
+ const factors = envelopeFromFrame(frame);
334
+ if (factors.attachmentGroups.some(group => ![
335
+ "TransIdxSigGroups",
336
+ "NonTransReceiptCouples",
337
+ "SealSourceTriples",
338
+ ].includes(group.name.replace(/^Big/, "")))) {
339
+ throw new ValidationError("Grant file permits only named authentication and exact nested evidence.");
340
+ }
341
+ const other = verifyExchangeProofs(this.wallet.hby.db, message, {
342
+ cigars: factors.cigars,
343
+ sourceSeals: factors.ssts,
344
+ });
345
+ if (other.invalid || other.missing.length) {
346
+ throw new ValidationError("Invalid or unavailable Grant authentication.");
347
+ }
348
+ const complete = new Set([
349
+ ...other.cigars.map(proof => proof.verfer.qb64),
350
+ ...other.sourceSeals.map(seal => seal.i.qb64),
351
+ ]);
352
+ const groups = new Map();
353
+ for (const group of factors.tsgs) {
354
+ const accepted = group.sn <= BigInt(Number.MAX_SAFE_INTEGER)
355
+ ? this.wallet.hby.db.kels.getLast(group.pre, Number(group.sn))
356
+ : null;
357
+ const event = accepted ? this.wallet.hby.db.getEvtSerder(group.pre, accepted) : null;
358
+ if (!group.prefixer.transferable || !event?.estive || !event.tholder || accepted !== group.said
359
+ || !group.sigers.length) {
360
+ throw new ValidationError("Grant contribution requires an accepted establishment event.");
361
+ }
362
+ const verified = Kever.verifyIndexedSignatures(message.raw, group.sigers, event.verfers);
363
+ if (new Set(verified.sigers.map(sig => sig.qb64)).size !== new Set(group.sigers.map(sig => sig.qb64)).size) {
364
+ throw new ValidationError("Invalid supplied Grant signature.");
365
+ }
366
+ const key = `${group.pre}:${group.sn}:${group.said}`;
367
+ const aggregate = groups.get(key) ?? { pre: group.pre, event, indices: new Set() };
368
+ for (const index of verified.indices)
369
+ aggregate.indices.add(index);
370
+ groups.set(key, aggregate);
371
+ }
372
+ const partial = new Set();
373
+ for (const { pre, event, indices } of groups.values()) {
374
+ if (event.tholder.satisfy([...indices]))
375
+ complete.add(pre);
376
+ else
377
+ partial.add(pre);
378
+ }
379
+ return { frame, completeSigners: [...complete], partialSigners: [...partial] };
380
+ }
381
+ /** Prepare one fresh issuer origin with explicit authenticated far nodes, without changing disclosure authority. */
382
+ *prepareIssuanceGrantBody(sender, credential, options) {
383
+ const hab = this.wallet.hby.habs.get(sender), origin = new SerderACDC({ raw: credential.raw.slice() });
384
+ if (!hab?.kever || (options.gvrsn ?? Vrsn_2_0).major < 2 || origin.pvrsn.major !== 2
385
+ || !origin.verify() || !["acm", "act", "acg"].includes(origin.ilk) || origin.issuer !== sender
386
+ || !options.recipient || options.recipient === sender || origin.issuee !== options.recipient || !origin.regid) {
387
+ throw new ValidationError("Fresh issuance requires an exact v2 credential from the local issuer to its selected issuee.");
388
+ }
389
+ const chain = acceptedRegistryEvidence(this.wallet.registry, origin.regid);
390
+ if (!chain || chain.updates.length || chain.rip.issuer !== sender
391
+ || vet({ ...chain, db: this.wallet.hby.db }).kind !== "verified") {
392
+ throw new ValidationError("Fresh issuance requires the issuer's accepted anchored RIP without updates.");
393
+ }
394
+ const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), origin.raw, Counter.enclose({ qb64: new Uint8Array(), version: Vrsn_2_0 }), ...(options.artifacts ?? []).map(artifact => artifact.slice()));
395
+ const graph = walkIpexGraph(origin.said, parseBytes(raw), true);
396
+ if (!graph) {
397
+ throw new ValidationError("Fresh issuance requires its complete explicitly disclosed credential graph.");
398
+ }
399
+ for (const [said, node] of graph.nodes) {
400
+ const schema = validateAcdcSchema(node.body, this.wallet.hby.db);
401
+ if (schema.kind !== "valid") {
402
+ throw new ValidationError(schema.kind === "missing" ? `Missing ACDC schema: ${schema.said}` : schema.reason);
403
+ }
404
+ if (said !== origin.said
405
+ && authenticateIpexNode(node.body, envelopeFromFrame(node), this.wallet.hby.db, this.wallet.registry).kind
406
+ !== "authenticated") {
407
+ throw new ValidationError("Fresh issuance supports one new origin; every far node requires authenticated issuer proof.");
408
+ }
409
+ }
410
+ const prior = yield* this.prior(options.priorSaid);
411
+ if (options.anchor || prior && (!["/ipex/apply", "/ipex/agree"].includes(prior.route)
412
+ || prior.sad.a.ax?.[0])) {
413
+ throw new ValidationError("Fresh issuance requires an unanchored Grant and an accepted Apply or Agree when linked.");
414
+ }
415
+ const [exn, nests] = yield* prepareIpexExn(sender, "/ipex/grant", raw, { ...options, prior });
416
+ return { exn, raw, nests };
417
+ }
418
+ /** Extend the frozen Grant's exact origin proof; ordinary intake still authenticates the supplied presentation evidence. */
419
+ *attachPresentationProof(prepared, opening, source) {
420
+ const frames = parseBytes(prepared.raw), node = frames[0].body;
421
+ if (!node.issuee || opening.td.nonce !== prepared.exn.said || opening.ts.label !== "presented") {
422
+ throw new ValidationError("Presentation proof must target this exact Grant and disclosed Issuee.");
423
+ }
424
+ const seal = Counter.enclose({
425
+ code: "SealSourceTriples",
426
+ qb64: concatBytes(new Prefixer({ qb64: node.issuee }).qb64b, source.s.qb64b, source.d.qb64b),
427
+ version: Vrsn_2_0,
428
+ });
429
+ const first = concatBytes(node.raw, Counter.enclose({
430
+ qb64: concatBytes(...frames[0].attachments.map(group => group.raw), publicOpeningAttachment(opening), seal),
431
+ version: Vrsn_2_0,
432
+ }));
433
+ const rest = yield* call(() => Promise.all(frames.slice(1).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
434
+ const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), first, ...rest);
435
+ const payloads = yield* call(() => Promise.all(parseBytes(raw).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
436
+ return { exn: prepared.exn, raw, nests: concatBytes(...payloads) };
437
+ }
438
+ /** Authenticate and authorize the selected graph before the Grant builder and after receipt waits. */
439
+ *prepareGrant(sender, selection, options) {
440
+ const hab = this.signer(sender, options.gvrsn ?? Vrsn_2_0);
441
+ const prepared = yield* this.prepareGrantBody(sender, selection, options);
442
+ let raw = prepared.raw;
443
+ const origin = parseBytes(raw)[0].body.said;
444
+ const exn = prepared.exn;
445
+ let nests = prepared.nests;
446
+ if (options.presentation) {
447
+ const reg = options.presentation, frames = parseBytes(raw), node = frames[0].body;
448
+ const attribute = node.sad.a;
449
+ if (reg.hab.pre !== sender || reg.store.baser !== this.wallet.registry.baser
450
+ || attribute?.rd !== reg.regk || attribute?.i !== sender || isLocalGroupHab(this.wallet.hby, reg.hab)) {
451
+ throw new ValidationError("Presentation registry must match the selected disclosed local Issuee declaration.");
452
+ }
453
+ // The Grant SAID is frozen before the BUP/KEL commitment. Only its nested proof is extended.
454
+ const [proof, update] = reg.present(exn, "presented", { stamp: options.stamp });
455
+ const seal = yield* anchorExchange(this.wallet, this.receiptor, hab, update, options.auths);
456
+ reg.processEscrows();
457
+ if (reg.store.seqEvent(reg.regk, Number(BigInt(`0x${update.sad.n}`)))?.said !== update.said) {
458
+ throw new ValidationError("Presentation BUP remains pending authenticated acceptance.");
459
+ }
460
+ ({ raw, nests } = yield* this.attachPresentationProof({ exn, raw, nests }, proof, seal));
461
+ }
462
+ const source = exn.sad.a.ax?.[0]
463
+ ? yield* anchorExchange(this.wallet, this.receiptor, hab, exn, options.auths)
464
+ : undefined;
465
+ this.requireGraph(origin, raw);
466
+ return {
467
+ exn,
468
+ wire: exchangeWire(hab, exn, nests, source, options.gvrsn ?? Vrsn_2_0),
469
+ support: this.proofSupport(raw, hab),
470
+ };
471
+ }
472
+ /** Carry selected public evidence and root-first delegation ancestry; receiver intake still verifies every approval. */
473
+ proofSupport(raw, hab) {
474
+ const aids = new Set([hab.pre]), registries = new Set();
475
+ for (const frame of parseBytes(raw)) {
476
+ if (!(frame.body instanceof SerderACDC))
477
+ continue;
478
+ const node = frame.body, envelope = envelopeFromFrame(frame);
479
+ if (node.issuer)
480
+ aids.add(node.issuer);
481
+ if (node.issuee)
482
+ aids.add(node.issuee);
483
+ if (node.regid)
484
+ registries.add(node.regid);
485
+ const a = node.sad.a;
486
+ if (a && typeof a === "object" && typeof a.rd === "string")
487
+ registries.add(a.rd);
488
+ for (const seal of envelope.ssts) {
489
+ if (this.wallet.registry.seqEvent(seal.i.qb64, 0))
490
+ registries.add(seal.i.qb64);
491
+ else
492
+ aids.add(seal.i.qb64);
493
+ }
494
+ }
495
+ const messages = [Counter.makeGVC(Vrsn_2_0)];
496
+ const db = this.wallet.hby.db, ordered = new Set();
497
+ // An ancestry claim selects supporting KELs; it does not establish delegation authority.
498
+ for (const aid of aids) {
499
+ const ancestors = [], seen = new Set();
500
+ let current = aid;
501
+ while (current && !ordered.has(current)) {
502
+ if (seen.has(current))
503
+ throw new ValidationError(`Cyclic disclosure delegation ancestry at ${current}.`);
504
+ seen.add(current);
505
+ const state = db.getKever(current);
506
+ if (!state)
507
+ break;
508
+ ancestors.push(current);
509
+ current = state.delpre;
510
+ }
511
+ for (const ancestor of ancestors.reverse())
512
+ ordered.add(ancestor);
513
+ }
514
+ // Keep first-seen authorizers and exact stored attachments, including superseded history.
515
+ // clonePreIter skips broken rows; finite disclosure must instead fail if an accepted row cannot be replayed.
516
+ for (const aid of ordered) {
517
+ for (const [, fn, said] of db.fels.getAllItemIter(aid))
518
+ messages.push(db.cloneEvtMsg(aid, fn, said, Vrsn_2_0));
519
+ }
520
+ for (const regid of registries)
521
+ messages.push(this.wallet.registry.cloneTel(regid));
522
+ return concatBytes(...messages);
523
+ }
524
+ /** Public replay includes actual stored witness receipts; it grants no new consent. */
525
+ kel(hab) {
526
+ return concatBytes(Counter.makeGVC(Vrsn_2_0), ...Array.from({ length: hab.kever.sn + 1 }, (_, sn) => acceptedEventReplayMessage(this.wallet.hby, hab.pre, sn, Vrsn_2_0).message));
527
+ }
528
+ }