@forgezero/runtime 0.1.0 → 0.1.2

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 CHANGED
@@ -5,7 +5,7 @@ themselves, keyed queues, a transactional outbox, a hash-chained audit trail,
5
5
  templated mail, encrypted backups, schema validation, and money that is never a
6
6
  floating-point number.
7
7
 
8
- Twenty-six modules. Each is its own entry point, so you install one package and
8
+ Thirty-three public modules. Each is its own entry point, so you install one package and
9
9
  your bundler includes only what you imported.
10
10
 
11
11
  ```bash
@@ -24,7 +24,7 @@ import { createScheduler } from '@forgezero/runtime/jobs';
24
24
 
25
25
  | | |
26
26
  |---|---|
27
- | **Spine** | `jobs` `queue` `serial` `outbox` |
27
+ | **Spine** | `jobs` `queue` `outbox` |
28
28
  | **Record** | `audit` `backup` `compliance` `calendar` |
29
29
  | **Identity** | `identity` `totp` `notify` `notify/templates` `schema` `schema/typebox` |
30
30
  | **Finance** | `finance/money` `finance/storage` `finance/ledger` `finance/commission` `finance/rates` `finance/transfers` `finance/tax` `finance/chain` `finance/custody` `finance/derive` `finance/venues` `finance/binance` |
@@ -32,6 +32,29 @@ import { createScheduler } from '@forgezero/runtime/jobs';
32
32
  None of it knows what your product does. You bind your own rules to these; you
33
33
  do not fork them.
34
34
 
35
+ ## One reusable async queue
36
+
37
+ `queue` is deliberately memory-only: it accepts a function, returns that
38
+ function's value through a Promise, runs one key sequentially, and runs
39
+ different keys in parallel. Persistence and cluster ownership remain in the
40
+ business layer that knows what a pending request means; ForgeZero uses an
41
+ ArangoDB unique claim, while another caller may use any database or no database.
42
+
43
+ ```ts
44
+ import { createQueue } from '@forgezero/runtime/queue';
45
+
46
+ const queue = createQueue({ width: 8 });
47
+ const task = queue.run('tenant-a:wallet-7', transfer, amount, destination);
48
+ const receipt = await task.result;
49
+
50
+ queue.pauseKey('tenant-a:wallet-7');
51
+ queue.resumeKey('tenant-a:wallet-7');
52
+ queue.stopKey('tenant-a:wallet-7'); // running finishes; pending is rejected
53
+ queue.startKey('tenant-a:wallet-7');
54
+ queue.cancel(task.id); // pending task only
55
+ await queue.stop(30_000); // close intake and drain all work
56
+ ```
57
+
35
58
  ## Three things worth knowing before you use it
36
59
 
37
60
  **Money is never a number.** An amount is minor units as a `bigint` with its
@@ -81,9 +104,9 @@ node_modules/@forgezero/runtime/contracts/src/
81
104
  ColdVault.sol M-of-N EIP-712, signatures ordered by ascending signer
82
105
  ```
83
106
 
84
- Full documentation: **https://forgezero.net/docs/runtime**
107
+ Full documentation: **https://www.forgezero.net/docs/runtime**
85
108
 
86
109
  ## Licence
87
110
 
88
- MIT. Part of [ForgeZero](https://forgezero.net) — secrets, attested compute and
111
+ MIT. Part of [ForgeZero](https://www.forgezero.net) — secrets, attested compute and
89
112
  deploys — and usable entirely on its own, with no ForgeZero account.
package/dist/audit.js CHANGED
@@ -6,32 +6,271 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
6
6
  throw Error('Dynamic require of "' + x + '" is not supported');
7
7
  });
8
8
 
9
- // src/serial.ts
10
- function createKeyedSerial(options = {}) {
11
- const maxKeys = options.maxKeys ?? 1e4;
12
- const chains = new Map;
13
- return {
14
- run(key, work) {
15
- const previous = chains.get(key) ?? Promise.resolve();
16
- const next = previous.then(work, work);
17
- const settled = next.then(() => {
9
+ // src/queue.ts
10
+ class QueueStoppedError extends Error {
11
+ constructor() {
12
+ super("queue: stopped before this task could run");
13
+ this.name = "QueueStoppedError";
14
+ }
15
+ }
16
+
17
+ class QueueKeyStoppedError extends Error {
18
+ key;
19
+ constructor(key) {
20
+ super(`queue: key ${key} is stopped`);
21
+ this.key = key;
22
+ this.name = "QueueKeyStoppedError";
23
+ }
24
+ }
25
+
26
+ class TaskCancelledError extends Error {
27
+ constructor() {
28
+ super("queue: task cancelled");
29
+ this.name = "TaskCancelledError";
30
+ }
31
+ }
32
+ var DEFAULT_RETRY = {
33
+ attempts: 1,
34
+ backoffMs: (attempt) => Math.min(30000, 2 ** attempt * 100)
35
+ };
36
+ function createQueue(options = {}) {
37
+ const width = options.width ?? 8;
38
+ const retry = { ...DEFAULT_RETRY, ...options.retry };
39
+ if (!Number.isSafeInteger(width) || width < 1) {
40
+ throw new RangeError("queue: width must be a positive integer");
41
+ }
42
+ if (!Number.isSafeInteger(retry.attempts) || retry.attempts < 1) {
43
+ throw new RangeError("queue: retry attempts must be a positive integer");
44
+ }
45
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
46
+ const lanes = new Map;
47
+ const running = new Set;
48
+ const paused = new Set;
49
+ const stoppedKeys = new Set;
50
+ let sequence = 0;
51
+ let globallyPaused = false;
52
+ let accepting = true;
53
+ let aborted = false;
54
+ let completed = 0;
55
+ let failed = 0;
56
+ const idle = [];
57
+ const announceIdle = () => {
58
+ if (running.size > 0)
59
+ return;
60
+ for (const lane of lanes.values())
61
+ if (lane.length > 0)
18
62
  return;
19
- }, () => {
63
+ while (idle.length > 0)
64
+ idle.shift()();
65
+ };
66
+ function pump() {
67
+ if (globallyPaused || aborted) {
68
+ announceIdle();
69
+ return;
70
+ }
71
+ for (const [key, lane] of lanes) {
72
+ if (running.size >= width)
73
+ break;
74
+ if (running.has(key) || paused.has(key) || lane.length === 0)
75
+ continue;
76
+ execute(key);
77
+ }
78
+ announceIdle();
79
+ }
80
+ async function execute(key) {
81
+ running.add(key);
82
+ try {
83
+ const lane = lanes.get(key);
84
+ const entry = lane?.[0];
85
+ if (entry && !globallyPaused && !paused.has(key) && !aborted) {
86
+ lane.shift();
87
+ await attempt(entry);
88
+ }
89
+ } finally {
90
+ running.delete(key);
91
+ const remaining = lanes.get(key);
92
+ if (remaining?.length === 0)
93
+ lanes.delete(key);
94
+ else if (remaining) {
95
+ lanes.delete(key);
96
+ lanes.set(key, remaining);
97
+ }
98
+ pump();
99
+ }
100
+ }
101
+ async function attempt(entry) {
102
+ if (entry.cancelled) {
103
+ entry.reject(new TaskCancelledError);
104
+ return;
105
+ }
106
+ for (;; ) {
107
+ entry.attempt += 1;
108
+ try {
109
+ const value = await entry.run();
110
+ completed += 1;
111
+ entry.resolve(value);
20
112
  return;
113
+ } catch (cause) {
114
+ if (entry.attempt >= retry.attempts || entry.cancelled || aborted) {
115
+ failed += 1;
116
+ entry.reject(cause);
117
+ return;
118
+ }
119
+ const delay = retry.backoffMs(entry.attempt);
120
+ if (!Number.isFinite(delay) || delay < 0) {
121
+ failed += 1;
122
+ entry.reject(new RangeError("queue: retry backoff must be a non-negative finite number"));
123
+ return;
124
+ }
125
+ await sleep(delay);
126
+ }
127
+ }
128
+ }
129
+ return {
130
+ run(key, handler, ...args) {
131
+ const id = `q_${++sequence}`;
132
+ if (!accepting || stoppedKeys.has(key)) {
133
+ const refused = Promise.reject(accepting ? new QueueKeyStoppedError(key) : new QueueStoppedError);
134
+ refused.catch(() => {
135
+ return;
136
+ });
137
+ return { id, key, result: refused };
138
+ }
139
+ let resolve;
140
+ let reject;
141
+ const result = new Promise((ok, no) => {
142
+ resolve = ok;
143
+ reject = no;
21
144
  });
22
- chains.set(key, settled);
23
- settled.then(() => {
24
- if (chains.get(key) === settled)
25
- chains.delete(key);
26
- });
27
- if (chains.size > maxKeys) {
28
- console.warn(`[serial] ${chains.size} keys in flight, above the ${maxKeys} guideline.`);
145
+ const entry = {
146
+ id,
147
+ key,
148
+ run: async () => handler(...args),
149
+ resolve,
150
+ reject,
151
+ attempt: 0,
152
+ cancelled: false
153
+ };
154
+ const lane = lanes.get(key);
155
+ if (lane)
156
+ lane.push(entry);
157
+ else
158
+ lanes.set(key, [entry]);
159
+ pump();
160
+ return { id, key, result };
161
+ },
162
+ cancel(id) {
163
+ for (const [key, lane] of lanes) {
164
+ const index = lane.findIndex((entry2) => entry2.id === id);
165
+ if (index === -1)
166
+ continue;
167
+ const [entry] = lane.splice(index, 1);
168
+ entry.cancelled = true;
169
+ entry.reject(new TaskCancelledError);
170
+ if (lane.length === 0 && !running.has(key))
171
+ lanes.delete(key);
172
+ announceIdle();
173
+ return true;
174
+ }
175
+ return false;
176
+ },
177
+ pauseKey(key) {
178
+ paused.add(key);
179
+ },
180
+ resumeKey(key) {
181
+ paused.delete(key);
182
+ pump();
183
+ },
184
+ stopKey(key) {
185
+ stoppedKeys.add(key);
186
+ paused.delete(key);
187
+ const lane = lanes.get(key);
188
+ if (!lane)
189
+ return 0;
190
+ let removed = 0;
191
+ for (const entry of lane.splice(0)) {
192
+ removed += 1;
193
+ entry.cancelled = true;
194
+ entry.reject(new QueueKeyStoppedError(key));
195
+ }
196
+ if (!running.has(key))
197
+ lanes.delete(key);
198
+ announceIdle();
199
+ return removed;
200
+ },
201
+ startKey(key) {
202
+ const changed = stoppedKeys.delete(key);
203
+ pump();
204
+ return changed;
205
+ },
206
+ pause() {
207
+ globallyPaused = true;
208
+ },
209
+ resume() {
210
+ globallyPaused = false;
211
+ pump();
212
+ },
213
+ snapshot() {
214
+ let queued = 0;
215
+ for (const lane of lanes.values())
216
+ queued += lane.length;
217
+ return {
218
+ running: running.size,
219
+ queued,
220
+ keys: lanes.size,
221
+ paused: globallyPaused,
222
+ pausedKeys: [...paused],
223
+ stoppedKeys: [...stoppedKeys],
224
+ completed,
225
+ failed
226
+ };
227
+ },
228
+ whenIdle() {
229
+ if (running.size === 0 && [...lanes.values()].every((lane) => lane.length === 0)) {
230
+ return Promise.resolve();
29
231
  }
30
- return next;
232
+ return new Promise((resolve) => idle.push(resolve));
31
233
  },
32
- size: () => chains.size,
33
- drain: async () => {
34
- await Promise.allSettled([...chains.values()]);
234
+ async stop(deadlineMs = 30000) {
235
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs < 0) {
236
+ throw new RangeError("queue: stop deadline must be a non-negative integer");
237
+ }
238
+ accepting = false;
239
+ const before = { completed, failed };
240
+ globallyPaused = false;
241
+ paused.clear();
242
+ pump();
243
+ let timedOut = false;
244
+ let deadlineHandle;
245
+ const deadline = new Promise((resolve) => {
246
+ deadlineHandle = setTimeout(() => {
247
+ timedOut = true;
248
+ resolve();
249
+ }, deadlineMs);
250
+ });
251
+ await Promise.race([this.whenIdle(), deadline]);
252
+ if (!timedOut && deadlineHandle !== undefined)
253
+ clearTimeout(deadlineHandle);
254
+ if (timedOut)
255
+ aborted = true;
256
+ let abandoned = 0;
257
+ for (const lane of lanes.values()) {
258
+ abandoned += lane.length;
259
+ for (const entry of lane.splice(0))
260
+ entry.reject(new QueueStoppedError);
261
+ }
262
+ abandoned += running.size;
263
+ for (const [key, lane] of lanes) {
264
+ if (lane.length === 0 && !running.has(key))
265
+ lanes.delete(key);
266
+ }
267
+ announceIdle();
268
+ return {
269
+ completed: completed - before.completed,
270
+ failed: failed - before.failed,
271
+ abandoned,
272
+ timedOut
273
+ };
35
274
  }
36
275
  };
37
276
  }
@@ -201,8 +440,8 @@ async function verifyExport(slice, options = {}) {
201
440
  }
202
441
  function createAuditChain(options) {
203
442
  const digester = options.digester ?? hashDigester;
204
- const serial = createKeyedSerial();
205
- const enqueue = (realm, run) => serial.run(realm ?? "\x00platform", run);
443
+ const queue = createQueue();
444
+ const enqueue = (realm, run) => queue.run(realm ?? "\x00platform", run).result;
206
445
  return {
207
446
  append: (entry) => enqueue(entry.realm, () => appendRecord(options.store, entry, { ...options, digester })),
208
447
  verify: async (realm, fromSequence = 1, toSequence = Number.MAX_SAFE_INTEGER) => {
@@ -0,0 +1,53 @@
1
+ /** The wire shape, identical to the server's `CipherBox`. */
2
+ export interface CipherBox {
3
+ alg: 'aes-256-gcm';
4
+ nonce: string;
5
+ ciphertext: string;
6
+ }
7
+ /** HKDF-SHA256, the same construction and parameter order as `hkdfSync`. */
8
+ export declare function deriveKey(secret: Uint8Array, salt: Uint8Array, info: string, length?: number): Uint8Array;
9
+ /**
10
+ * Seal under a key, with AAD bound but not encrypted.
11
+ *
12
+ * `randomBytes` comes from the platform: `crypto.getRandomValues` exists in
13
+ * every browser and in Bun, and a nonce from anywhere else is not a nonce.
14
+ */
15
+ export declare function sealWithKey(key: Uint8Array, plaintext: Uint8Array, aad: string): CipherBox;
16
+ /** Open a box. Throws on a wrong key, a wrong AAD, or any modification. */
17
+ export declare function openWithKey(key: Uint8Array, box: CipherBox, aad: string): Uint8Array;
18
+ /**
19
+ * Sealing to a PUBLIC key, so the holder of the private half is the only reader.
20
+ *
21
+ * This exists because the server was handing custodians their raw Shamir share
22
+ * and trusting them to report that both envelopes opened. Capturing one
23
+ * enrolment response therefore bypassed both recovery factors permanently, and
24
+ * the two factor "tests" were attestations by the client rather than evidence.
25
+ *
26
+ * With this, the client derives a wrapping key pair from each factor and sends
27
+ * only the public halves. The server seals the share to those and never emits
28
+ * plaintext — so a captured response is ciphertext, and opening it requires the
29
+ * factor itself.
30
+ *
31
+ * X25519 + HKDF-SHA256 + AES-256-GCM, with the ephemeral public key carried in
32
+ * the box. Nothing bespoke: the ephemeral-static shape is what every sealed-box
33
+ * construction uses, and doing it by hand is how people lose the AAD.
34
+ */
35
+ export interface SealedToKey extends CipherBox {
36
+ /** base64 — the ephemeral public key this box was sealed with. */
37
+ ephemeral: string;
38
+ }
39
+ /** Seal to a recipient's public key. */
40
+ export declare function sealToKey(recipientPublicKey: Uint8Array, plaintext: Uint8Array, aad: string): SealedToKey;
41
+ /** Open a box sealed to this private key. */
42
+ export declare function openFromKey(recipientSecretKey: Uint8Array, box: SealedToKey, aad: string): Uint8Array;
43
+ /**
44
+ * A wrapping key pair derived from factor material.
45
+ *
46
+ * Deterministic, because a custodian must reproduce the same private key on a
47
+ * different machine months later from the same passkey or the same words. The
48
+ * factor never leaves the caller — only the public half is ever sent.
49
+ */
50
+ export declare function wrappingKeyPair(factorMaterial: Uint8Array, info: string): {
51
+ secretKey: Uint8Array;
52
+ publicKey: Uint8Array;
53
+ };
@@ -0,0 +1,89 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/custody-crypto.ts
10
+ import { gcm } from "@noble/ciphers/aes.js";
11
+ import { x25519 } from "@noble/curves/ed25519.js";
12
+ import { hkdf } from "@noble/hashes/hkdf.js";
13
+ import { sha256 } from "@noble/hashes/sha2.js";
14
+ var KEY_BYTES = 32;
15
+ var NONCE_BYTES = 12;
16
+ var toBase64 = (bytes) => {
17
+ let binary = "";
18
+ for (const byte of bytes)
19
+ binary += String.fromCharCode(byte);
20
+ return btoa(binary);
21
+ };
22
+ var fromBase64 = (value) => {
23
+ const binary = atob(value);
24
+ const bytes = new Uint8Array(binary.length);
25
+ for (let index = 0;index < binary.length; index += 1)
26
+ bytes[index] = binary.charCodeAt(index);
27
+ return bytes;
28
+ };
29
+ var utf8 = (value) => new TextEncoder().encode(value);
30
+ function deriveKey(secret, salt, info, length = KEY_BYTES) {
31
+ return hkdf(sha256, secret, salt, utf8(info), length);
32
+ }
33
+ function sealWithKey(key, plaintext, aad) {
34
+ if (key.length !== KEY_BYTES)
35
+ throw new Error("custody: key must be 32 bytes");
36
+ const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
37
+ const sealed = gcm(key, nonce, utf8(aad)).encrypt(plaintext);
38
+ return { alg: "aes-256-gcm", nonce: toBase64(nonce), ciphertext: toBase64(sealed) };
39
+ }
40
+ function openWithKey(key, box, aad) {
41
+ if (key.length !== KEY_BYTES)
42
+ throw new Error("custody: key must be 32 bytes");
43
+ if (box.alg !== "aes-256-gcm")
44
+ throw new Error(`custody: unknown algorithm ${box.alg}`);
45
+ return gcm(key, fromBase64(box.nonce), utf8(aad)).decrypt(fromBase64(box.ciphertext));
46
+ }
47
+ var WRAP_INFO = "forgezero:custody:wrap:v1";
48
+ var wrapKey = (shared, ephemeral, recipient) => hkdf(sha256, shared, concatBytes(ephemeral, recipient), utf8(WRAP_INFO), 32);
49
+ function concatBytes(left, right) {
50
+ const out = new Uint8Array(left.length + right.length);
51
+ out.set(left, 0);
52
+ out.set(right, left.length);
53
+ return out;
54
+ }
55
+ function sealToKey(recipientPublicKey, plaintext, aad) {
56
+ const ephemeralSecret = x25519.utils.randomSecretKey();
57
+ const ephemeralPublic = x25519.getPublicKey(ephemeralSecret);
58
+ const shared = x25519.getSharedSecret(ephemeralSecret, recipientPublicKey);
59
+ const key = wrapKey(shared, ephemeralPublic, recipientPublicKey);
60
+ try {
61
+ return { ...sealWithKey(key, plaintext, aad), ephemeral: toBase64(ephemeralPublic) };
62
+ } finally {
63
+ key.fill(0);
64
+ ephemeralSecret.fill(0);
65
+ }
66
+ }
67
+ function openFromKey(recipientSecretKey, box, aad) {
68
+ const ephemeralPublic = fromBase64(box.ephemeral);
69
+ const recipientPublic = x25519.getPublicKey(recipientSecretKey);
70
+ const shared = x25519.getSharedSecret(recipientSecretKey, ephemeralPublic);
71
+ const key = wrapKey(shared, ephemeralPublic, recipientPublic);
72
+ try {
73
+ return openWithKey(key, box, aad);
74
+ } finally {
75
+ key.fill(0);
76
+ }
77
+ }
78
+ function wrappingKeyPair(factorMaterial, info) {
79
+ const secretKey = hkdf(sha256, factorMaterial, utf8("forgezero:custody:wrapkey:v1"), utf8(info), 32);
80
+ return { secretKey, publicKey: x25519.getPublicKey(secretKey) };
81
+ }
82
+ export {
83
+ wrappingKeyPair,
84
+ sealWithKey,
85
+ sealToKey,
86
+ openWithKey,
87
+ openFromKey,
88
+ deriveKey
89
+ };
@@ -0,0 +1,117 @@
1
+ import { type CipherBox, type SealedToKey } from './custody-crypto';
2
+ export interface SealedShare {
3
+ shareIndex: number;
4
+ passkeyEnvelope: CipherBox;
5
+ phraseEnvelope: CipherBox;
6
+ /** base64 — the HKDF salt for the phrase key and the verifier. */
7
+ phraseSalt: string;
8
+ /** hex — proves the right phrase without being able to rebuild it. */
9
+ phraseVerifier: string;
10
+ }
11
+ type EnvelopeKind = 'passkey' | 'phrase';
12
+ /** The x-coordinate a share was cut at — its custodian-facing share number. */
13
+ export declare function shareIndexOf(share: Uint8Array): number;
14
+ /**
15
+ * Seal one share under BOTH factors.
16
+ *
17
+ * The two `sealWithKey` calls are siblings: the same `share` goes into each and
18
+ * neither output is an input to the other. That is what makes recovery an OR —
19
+ * lose the passkey, open with the phrase.
20
+ */
21
+ export declare function sealShare(args: {
22
+ share: Uint8Array;
23
+ custodianKey: string;
24
+ passkeyPrfOutput: Uint8Array;
25
+ phraseWords: string[];
26
+ }): SealedShare;
27
+ /** Open the share with the passkey alone. The phrase is not consulted. */
28
+ export declare function openShareWithPasskey(sealed: SealedShare, custodianKey: string, passkeyPrfOutput: Uint8Array): Uint8Array;
29
+ /** Open the share with the phrase alone. The passkey is not consulted. */
30
+ export declare function openShareWithPhrase(sealed: SealedShare, custodianKey: string, phraseWords: string[]): Uint8Array;
31
+ /**
32
+ * The public halves a custodian offers so the server can seal to them.
33
+ *
34
+ * The server used to hand over the raw Shamir share and trust the client to
35
+ * report that both envelopes opened. Capturing one enrolment response therefore
36
+ * bypassed both recovery factors permanently, and the factor "tests" were
37
+ * attestations rather than evidence.
38
+ *
39
+ * These are derived from the factors themselves and are deterministic, so a
40
+ * custodian reproduces the same private halves months later on a different
41
+ * machine from the same passkey or the same words. Only the public halves are
42
+ * ever sent.
43
+ */
44
+ export interface WrappingKeys {
45
+ passkeyPublicKey: string;
46
+ phrasePublicKey: string;
47
+ phraseSalt: string;
48
+ phraseVerifier: string;
49
+ }
50
+ /** The private half for one factor. Never leaves the machine that made it. */
51
+ export declare function passkeyWrappingKey(custodianKey: string, passkeyPrfOutput: Uint8Array): {
52
+ secretKey: Uint8Array;
53
+ publicKey: Uint8Array;
54
+ };
55
+ /** The same for the phrase, over the salt that will be stored beside it. */
56
+ export declare function phraseWrappingKey(custodianKey: string, phraseWords: string[], salt: Uint8Array): {
57
+ secretKey: Uint8Array;
58
+ publicKey: Uint8Array;
59
+ };
60
+ /** Everything the server needs, and nothing it must not have. */
61
+ export declare function wrappingKeysFor(args: {
62
+ custodianKey: string;
63
+ passkeyPrfOutput: Uint8Array;
64
+ phraseWords: string[];
65
+ }): WrappingKeys;
66
+ /** Open something the server sealed to one of those public keys. */
67
+ export declare function openSealedToFactor(args: {
68
+ custodianKey: string;
69
+ factor: EnvelopeKind;
70
+ box: SealedToKey;
71
+ passkeyPrfOutput?: Uint8Array;
72
+ phraseWords?: string[];
73
+ phraseSalt?: string;
74
+ }): Uint8Array;
75
+ /** The aad the server must use when sealing to a custodian's factor key. */
76
+ export declare const factorWrapAad: (custodianKey: string, factor: EnvelopeKind) => string;
77
+ /**
78
+ * A share plus a probe, sealed together.
79
+ *
80
+ * The probe is DIFFERENT in each envelope, and that difference is what makes
81
+ * the two factor tests independent. Sealing only the share would let a client
82
+ * that opened the passkey envelope answer the phrase test with the same bytes —
83
+ * proving one factor and being credited with both, which is the attestation
84
+ * problem this whole change exists to remove.
85
+ *
86
+ * Fixed 32-byte probe at the front, so splitting needs no length prefix and a
87
+ * truncated envelope fails the GCM tag rather than being parsed.
88
+ */
89
+ export declare const PROBE_BYTES = 32;
90
+ export declare function joinProbeAndShare(probe: Uint8Array, share: Uint8Array): Uint8Array;
91
+ export declare function splitProbeAndShare(opened: Uint8Array): {
92
+ probe: Uint8Array;
93
+ share: Uint8Array;
94
+ };
95
+ /**
96
+ * Open one factor's envelope from the sealed record.
97
+ *
98
+ * Takes the whole record rather than a single box, because the phrase salt
99
+ * lives IN that record and is public — asking every caller to carry it
100
+ * separately meant each one had somewhere to lose it, and losing it surfaces as
101
+ * `atob` complaining about invalid characters rather than as anything about
102
+ * custody.
103
+ */
104
+ export declare function openFactorEnvelope(args: {
105
+ sealed: SealedShare & {
106
+ passkeyEnvelope: SealedToKey;
107
+ phraseEnvelope: SealedToKey;
108
+ };
109
+ custodianKey: string;
110
+ factor: EnvelopeKind;
111
+ passkeyPrfOutput?: Uint8Array;
112
+ phraseWords?: string[];
113
+ }): {
114
+ probe: Uint8Array;
115
+ share: Uint8Array;
116
+ };
117
+ export {};