@forgezero/runtime 0.1.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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -0
  3. package/contracts/foundry.toml +9 -0
  4. package/contracts/src/ColdVault.sol +206 -0
  5. package/contracts/src/DepositFactory.sol +202 -0
  6. package/contracts/src/DepositProxy.sol +72 -0
  7. package/contracts/src/IERC20.sol +7 -0
  8. package/contracts/src/MockTokens.sol +32 -0
  9. package/contracts/src/SafeTransferLib.sol +31 -0
  10. package/contracts/test/Custody.t.sol +361 -0
  11. package/contracts/test/Vectors.t.sol +45 -0
  12. package/dist/audit.d.ts +265 -0
  13. package/dist/audit.js +291 -0
  14. package/dist/backup.d.ts +243 -0
  15. package/dist/backup.js +302 -0
  16. package/dist/calendar.d.ts +136 -0
  17. package/dist/calendar.js +129 -0
  18. package/dist/compliance.d.ts +172 -0
  19. package/dist/compliance.js +168 -0
  20. package/dist/finance/binance.d.ts +27 -0
  21. package/dist/finance/binance.js +452 -0
  22. package/dist/finance/chain-addresses.d.ts +130 -0
  23. package/dist/finance/chain-addresses.js +462 -0
  24. package/dist/finance/chain-deposits.d.ts +193 -0
  25. package/dist/finance/chain-deposits.js +596 -0
  26. package/dist/finance/chain-reconcile.d.ts +112 -0
  27. package/dist/finance/chain-reconcile.js +76 -0
  28. package/dist/finance/chain-withdrawals.d.ts +223 -0
  29. package/dist/finance/chain-withdrawals.js +631 -0
  30. package/dist/finance/chain.d.ts +116 -0
  31. package/dist/finance/chain.js +316 -0
  32. package/dist/finance/commission.d.ts +155 -0
  33. package/dist/finance/commission.js +419 -0
  34. package/dist/finance/custody.d.ts +68 -0
  35. package/dist/finance/custody.js +107 -0
  36. package/dist/finance/derive.d.ts +115 -0
  37. package/dist/finance/derive.js +116 -0
  38. package/dist/finance/discounts.d.ts +98 -0
  39. package/dist/finance/discounts.js +90 -0
  40. package/dist/finance/ledger.d.ts +221 -0
  41. package/dist/finance/ledger.js +308 -0
  42. package/dist/finance/market.d.ts +209 -0
  43. package/dist/finance/market.js +112 -0
  44. package/dist/finance/money.d.ts +118 -0
  45. package/dist/finance/money.js +176 -0
  46. package/dist/finance/rates.d.ts +178 -0
  47. package/dist/finance/rates.js +292 -0
  48. package/dist/finance/storage.d.ts +113 -0
  49. package/dist/finance/storage.js +226 -0
  50. package/dist/finance/tax.d.ts +132 -0
  51. package/dist/finance/tax.js +291 -0
  52. package/dist/finance/transfers.d.ts +153 -0
  53. package/dist/finance/transfers.js +292 -0
  54. package/dist/finance/venues.d.ts +190 -0
  55. package/dist/finance/venues.js +251 -0
  56. package/dist/identity.d.ts +115 -0
  57. package/dist/identity.js +111 -0
  58. package/dist/importers.d.ts +87 -0
  59. package/dist/importers.js +250 -0
  60. package/dist/jobs.d.ts +171 -0
  61. package/dist/jobs.js +250 -0
  62. package/dist/notify-templates.d.ts +11 -0
  63. package/dist/notify-templates.js +254 -0
  64. package/dist/notify.d.ts +172 -0
  65. package/dist/notify.js +122 -0
  66. package/dist/openssh.d.ts +36 -0
  67. package/dist/openssh.js +106 -0
  68. package/dist/otpauth.d.ts +57 -0
  69. package/dist/otpauth.js +223 -0
  70. package/dist/outbox.d.ts +234 -0
  71. package/dist/outbox.js +236 -0
  72. package/dist/passkey.d.ts +120 -0
  73. package/dist/passkey.js +105 -0
  74. package/dist/phrase.d.ts +87 -0
  75. package/dist/phrase.js +87 -0
  76. package/dist/pipeline.d.ts +137 -0
  77. package/dist/pipeline.js +121 -0
  78. package/dist/queue.d.ts +243 -0
  79. package/dist/queue.js +246 -0
  80. package/dist/schema-typebox.d.ts +24 -0
  81. package/dist/schema-typebox.js +201 -0
  82. package/dist/schema.d.ts +134 -0
  83. package/dist/schema.js +169 -0
  84. package/dist/serial.d.ts +54 -0
  85. package/dist/serial.js +40 -0
  86. package/dist/slip10.d.ts +37 -0
  87. package/dist/slip10.js +74 -0
  88. package/dist/snp.d.ts +115 -0
  89. package/dist/snp.js +109 -0
  90. package/dist/ssh-agent.d.ts +70 -0
  91. package/dist/ssh-agent.js +141 -0
  92. package/dist/ssh-cert.d.ts +73 -0
  93. package/dist/ssh-cert.js +111 -0
  94. package/dist/totp.d.ts +104 -0
  95. package/dist/totp.js +143 -0
  96. package/package.json +248 -0
@@ -0,0 +1,106 @@
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/openssh.ts
10
+ import { sha256 } from "@noble/hashes/sha2.js";
11
+ import { toBase64Url, fromBase64Url } from "@forgezero/access/security";
12
+ var ALGORITHM = "ssh-ed25519";
13
+ var ENCODER = new TextEncoder;
14
+ function sshString(value) {
15
+ const bytes = typeof value === "string" ? ENCODER.encode(value) : value;
16
+ const out = new Uint8Array(4 + bytes.length);
17
+ new DataView(out.buffer).setUint32(0, bytes.length, false);
18
+ out.set(bytes, 4);
19
+ return out;
20
+ }
21
+ var concat = (...parts) => {
22
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
23
+ const out = new Uint8Array(total);
24
+ let offset = 0;
25
+ for (const part of parts) {
26
+ out.set(part, offset);
27
+ offset += part.length;
28
+ }
29
+ return out;
30
+ };
31
+ function base64(bytes) {
32
+ const url = toBase64Url(bytes);
33
+ const standard = url.replaceAll("-", "+").replaceAll("_", "/");
34
+ const padding = (4 - standard.length % 4) % 4;
35
+ return standard + "=".repeat(padding);
36
+ }
37
+ var unbase64 = (value) => fromBase64Url(value.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""));
38
+
39
+ class OpenSshError extends Error {
40
+ code;
41
+ constructor(code, message) {
42
+ super(message);
43
+ this.code = code;
44
+ this.name = "OpenSshError";
45
+ }
46
+ }
47
+ function publicKeyBlob(publicKey) {
48
+ if (publicKey.length !== 32) {
49
+ throw new OpenSshError("BAD_KEY", "An ed25519 public key is 32 bytes.");
50
+ }
51
+ return concat(sshString(ALGORITHM), sshString(publicKey));
52
+ }
53
+ function authorizedKey(publicKey, comment = "") {
54
+ if (/[\r\n]/.test(comment)) {
55
+ throw new OpenSshError("BAD_COMMENT", "A comment cannot contain a newline — in authorized_keys that ends the line and starts another entry.");
56
+ }
57
+ const line = `${ALGORITHM} ${base64(publicKeyBlob(publicKey))}`;
58
+ return comment.trim() ? `${line} ${comment.trim()}` : line;
59
+ }
60
+ function fingerprint(publicKey) {
61
+ return `SHA256:${base64(sha256(publicKeyBlob(publicKey))).replace(/=+$/, "")}`;
62
+ }
63
+ function signatureBlob(signature) {
64
+ if (signature.length !== 64) {
65
+ throw new OpenSshError("BAD_KEY", "An ed25519 signature is 64 bytes.");
66
+ }
67
+ return concat(sshString(ALGORITHM), sshString(signature));
68
+ }
69
+ function parseAuthorizedKey(line) {
70
+ const parts = line.trim().split(/\s+/);
71
+ if (parts.length < 2)
72
+ throw new OpenSshError("BAD_BLOB", "That is not an authorized_keys line.");
73
+ const [prefix, encoded, ...rest] = parts;
74
+ if (prefix !== ALGORITHM) {
75
+ throw new OpenSshError("BAD_BLOB", `Only ${ALGORITHM} is supported here, not ${prefix}.`);
76
+ }
77
+ let blob;
78
+ try {
79
+ blob = unbase64(encoded);
80
+ } catch {
81
+ throw new OpenSshError("BAD_BLOB", "The key body is not valid base64.");
82
+ }
83
+ if (blob.length !== 51)
84
+ throw new OpenSshError("BAD_BLOB", "That is not an ed25519 key blob.");
85
+ const view = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
86
+ const nameLength = view.getUint32(0, false);
87
+ const name = new TextDecoder().decode(blob.slice(4, 4 + nameLength));
88
+ if (name !== ALGORITHM) {
89
+ throw new OpenSshError("BAD_BLOB", `The blob says "${name}" while the line says "${prefix}". sshd reads the blob, so this key would be refused at login.`);
90
+ }
91
+ const keyLength = view.getUint32(4 + nameLength, false);
92
+ if (keyLength !== 32)
93
+ throw new OpenSshError("BAD_BLOB", "The embedded key is not 32 bytes.");
94
+ return {
95
+ publicKey: blob.slice(4 + nameLength + 4),
96
+ comment: rest.join(" ")
97
+ };
98
+ }
99
+ export {
100
+ signatureBlob,
101
+ publicKeyBlob,
102
+ parseAuthorizedKey,
103
+ fingerprint,
104
+ authorizedKey,
105
+ OpenSshError
106
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The `otpauth://` URI, which is what a QR code actually contains.
3
+ *
4
+ * Every site that offers "scan this" encodes the same thing: a secret, who it
5
+ * belongs to, and the parameters needed to reproduce a code. Nothing about it
6
+ * is standardised beyond a Google wiki page, so implementations disagree at the
7
+ * edges — and each disagreement is a code that comes out wrong for one issuer
8
+ * and right for every other, which is the hardest kind of bug to be told about.
9
+ *
10
+ * So this parses strictly and refuses what it cannot reproduce, rather than
11
+ * guessing a default and generating six digits that will never be accepted.
12
+ *
13
+ * ## The label is not the issuer, except when it is
14
+ *
15
+ * otpauth://totp/GitHub:alice@example.com?issuer=GitHub&secret=...
16
+ *
17
+ * The prefix before the colon and the `issuer` parameter are the same claim
18
+ * made twice, and they DISAGREE in the wild. The parameter wins — it is
19
+ * unambiguous, while the label prefix cannot be told apart from an account name
20
+ * that happens to contain a colon.
21
+ */
22
+ export interface OtpAuth {
23
+ /** `totp`. `hotp` is counter-based and deliberately unsupported. */
24
+ type: 'totp';
25
+ /** Who issued it — "GitHub", "AWS". */
26
+ issuer: string;
27
+ /** The account at that issuer. May be empty; not every issuer sets one. */
28
+ account: string;
29
+ /** Base32, no padding, upper case. */
30
+ secret: string;
31
+ digits: number;
32
+ stepSeconds: number;
33
+ algorithm: 'SHA-1' | 'SHA-256' | 'SHA-512';
34
+ }
35
+ /**
36
+ * Parse, or throw.
37
+ *
38
+ * There is no partial success. A URI missing its secret, or carrying digits we
39
+ * cannot produce, yields nothing usable — and storing it would mean an entry
40
+ * that looks enrolled and can never sign anybody in.
41
+ */
42
+ export declare function parseOtpAuth(raw: string): OtpAuth;
43
+ /**
44
+ * Back to a URI, for handing to another authenticator.
45
+ *
46
+ * A vault somebody cannot leave is a vault they should not have joined, and for
47
+ * TOTP that means being able to export what was imported.
48
+ *
49
+ * The issuer appears TWICE — as the label prefix and as the parameter — because
50
+ * older authenticators read only one of the two, and which one is not knowable
51
+ * from here.
52
+ */
53
+ export declare function formatOtpAuth(entry: OtpAuth): string;
54
+ /** A secret somebody typed by hand, rather than scanned. */
55
+ export declare function fromManualSecret(secret: string, issuer?: string, account?: string): OtpAuth;
56
+ /** Round-trip a secret we generated ourselves, for enrolling a NEW account. */
57
+ export declare const newOtpAuth: (secretBytes: Uint8Array, issuer: string, account: string) => OtpAuth;
@@ -0,0 +1,223 @@
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/totp.ts
10
+ import { timingSafeEqual } from "@forgezero/access/security";
11
+
12
+ class TotpError extends Error {
13
+ code;
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.code = code;
17
+ this.name = "TotpError";
18
+ }
19
+ }
20
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
21
+ function toBase32(bytes) {
22
+ let bits = 0;
23
+ let value = 0;
24
+ let output = "";
25
+ for (const byte of bytes) {
26
+ value = value << 8 | byte;
27
+ bits += 8;
28
+ while (bits >= 5) {
29
+ output += ALPHABET[value >>> bits - 5 & 31];
30
+ bits -= 5;
31
+ }
32
+ }
33
+ if (bits > 0)
34
+ output += ALPHABET[value << 5 - bits & 31];
35
+ return output;
36
+ }
37
+ function fromBase32(text) {
38
+ const cleaned = text.toUpperCase().replace(/[\s=-]/g, "");
39
+ if (cleaned.length === 0)
40
+ throw new TotpError("BAD_SECRET", "The secret is empty.");
41
+ let bits = 0;
42
+ let value = 0;
43
+ const bytes = [];
44
+ for (const character of cleaned) {
45
+ const index = ALPHABET.indexOf(character);
46
+ if (index === -1) {
47
+ throw new TotpError("BAD_SECRET", `"${character}" is not a base32 character.`);
48
+ }
49
+ value = value << 5 | index;
50
+ bits += 5;
51
+ if (bits >= 8) {
52
+ bytes.push(value >>> bits - 8 & 255);
53
+ bits -= 8;
54
+ }
55
+ }
56
+ return new Uint8Array(bytes);
57
+ }
58
+ function generateSecret(bytes = 20) {
59
+ const buffer = new Uint8Array(bytes);
60
+ crypto.getRandomValues(buffer);
61
+ return toBase32(buffer);
62
+ }
63
+ var DEFAULT_STEP_SECONDS = 30;
64
+ var DEFAULT_DIGITS = 6;
65
+ var counterAt = (atMs, stepSeconds = DEFAULT_STEP_SECONDS) => Math.floor(atMs / 1000 / stepSeconds);
66
+ async function hmac(secret, counter, algorithm) {
67
+ const message = new Uint8Array(8);
68
+ let remaining = counter;
69
+ for (let index = 7;index >= 0; index -= 1) {
70
+ message[index] = remaining & 255;
71
+ remaining = Math.floor(remaining / 256);
72
+ }
73
+ const key = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: algorithm }, false, ["sign"]);
74
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, message));
75
+ }
76
+ async function codeFor(secret, counter, options = {}) {
77
+ const digits = options.digits ?? DEFAULT_DIGITS;
78
+ if (digits < 6 || digits > 8) {
79
+ throw new TotpError("BAD_DIGITS", "A TOTP code is between six and eight digits.");
80
+ }
81
+ const mac = await hmac(fromBase32(secret), counter, options.algorithm ?? "SHA-1");
82
+ const offset = mac[mac.length - 1] & 15;
83
+ const binary = (mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255;
84
+ return String(binary % 10 ** digits).padStart(digits, "0");
85
+ }
86
+ var codeAt = (secret, atMs, options = {}) => codeFor(secret, counterAt(atMs, options.stepSeconds ?? DEFAULT_STEP_SECONDS), options);
87
+ async function verifyCode(secret, code, options = {}) {
88
+ const digits = options.digits ?? DEFAULT_DIGITS;
89
+ const cleaned = code.replace(/\s/g, "");
90
+ if (!new RegExp(`^\\d{${digits}}$`).test(cleaned)) {
91
+ return { ok: false, reason: "BAD_CODE" };
92
+ }
93
+ const step = options.stepSeconds ?? DEFAULT_STEP_SECONDS;
94
+ const window = options.window ?? 1;
95
+ const current = counterAt(options.atMs ?? Date.now(), step);
96
+ let matched;
97
+ for (let drift = -window;drift <= window; drift += 1) {
98
+ const counter = current + drift;
99
+ const expected = await codeFor(secret, counter, options);
100
+ if (timingSafeEqual(expected, cleaned))
101
+ matched = counter;
102
+ }
103
+ if (matched === undefined)
104
+ return { ok: false, reason: "BAD_CODE" };
105
+ if (options.lastCounter !== undefined && matched <= options.lastCounter) {
106
+ return { ok: false, counter: matched, reason: "REPLAYED" };
107
+ }
108
+ return { ok: true, counter: matched };
109
+ }
110
+ async function assertCode(secret, code, options = {}) {
111
+ const result = await verifyCode(secret, code, options);
112
+ if (!result.ok) {
113
+ throw new TotpError(result.reason ?? "BAD_CODE", result.reason === "REPLAYED" ? "That code has already been used. Wait for the next one." : "That code is not right. Check your authenticator and try again.");
114
+ }
115
+ return result.counter;
116
+ }
117
+ function enrolmentUri(args) {
118
+ const label = `${encodeURIComponent(args.issuer)}:${encodeURIComponent(args.account)}`;
119
+ const parameters = new URLSearchParams({
120
+ secret: args.secret,
121
+ issuer: args.issuer,
122
+ algorithm: (args.options?.algorithm ?? "SHA-1").replace("-", ""),
123
+ digits: String(args.options?.digits ?? DEFAULT_DIGITS),
124
+ period: String(args.options?.stepSeconds ?? DEFAULT_STEP_SECONDS)
125
+ });
126
+ return `otpauth://totp/${label}?${parameters.toString()}`;
127
+ }
128
+ var secondsRemaining = (atMs = Date.now(), stepSeconds = DEFAULT_STEP_SECONDS) => stepSeconds - Math.floor(atMs / 1000) % stepSeconds;
129
+
130
+ // src/otpauth.ts
131
+ var ALGORITHMS = {
132
+ SHA1: "SHA-1",
133
+ SHA256: "SHA-256",
134
+ SHA512: "SHA-512"
135
+ };
136
+ function parseOtpAuth(raw) {
137
+ let url;
138
+ try {
139
+ url = new URL(raw.trim());
140
+ } catch {
141
+ throw new TotpError("OTPAUTH_MALFORMED", "That is not an otpauth:// URI.");
142
+ }
143
+ if (url.protocol !== "otpauth:") {
144
+ throw new TotpError("OTPAUTH_MALFORMED", "That is not an otpauth:// URI.");
145
+ }
146
+ const afterScheme = raw.trim().slice("otpauth://".length);
147
+ const [typeAndLabel = "", ...rest] = afterScheme.split("?");
148
+ const query = new URLSearchParams(rest.join("?"));
149
+ const slash = typeAndLabel.indexOf("/");
150
+ const type = (slash === -1 ? typeAndLabel : typeAndLabel.slice(0, slash)).toLowerCase();
151
+ const label = decodeURIComponent(slash === -1 ? "" : typeAndLabel.slice(slash + 1));
152
+ if (type === "hotp") {
153
+ throw new TotpError("OTPAUTH_UNSUPPORTED", "Counter-based (hotp) codes are not supported.");
154
+ }
155
+ if (type !== "totp") {
156
+ throw new TotpError("OTPAUTH_UNSUPPORTED", `"${type}" is not a kind of code we generate.`);
157
+ }
158
+ const secret = (query.get("secret") ?? "").replace(/\s|=/g, "").toUpperCase();
159
+ if (!secret)
160
+ throw new TotpError("OTPAUTH_NO_SECRET", "The URI carries no secret.");
161
+ fromBase32(secret);
162
+ const colon = label.indexOf(":");
163
+ const labelIssuer = colon === -1 ? "" : label.slice(0, colon).trim();
164
+ const issuer = (query.get("issuer") ?? labelIssuer).trim();
165
+ const account = (colon === -1 ? label : label.slice(colon + 1)).trim();
166
+ const digits = Number(query.get("digits") ?? DEFAULT_DIGITS);
167
+ if (![6, 7, 8].includes(digits)) {
168
+ throw new TotpError("OTPAUTH_UNSUPPORTED", `${digits}-digit codes are not supported.`);
169
+ }
170
+ const stepSeconds = Number(query.get("period") ?? DEFAULT_STEP_SECONDS);
171
+ if (!Number.isInteger(stepSeconds) || stepSeconds < 15 || stepSeconds > 300) {
172
+ throw new TotpError("OTPAUTH_UNSUPPORTED", `A ${stepSeconds}s period is out of range.`);
173
+ }
174
+ const rawAlgorithm = (query.get("algorithm") ?? "SHA1").toUpperCase().replace("-", "");
175
+ const algorithm = ALGORITHMS[rawAlgorithm];
176
+ if (!algorithm) {
177
+ throw new TotpError("OTPAUTH_UNSUPPORTED", `${rawAlgorithm} is not an algorithm we support.`);
178
+ }
179
+ return { type: "totp", issuer, account, secret, digits, stepSeconds, algorithm };
180
+ }
181
+ function formatOtpAuth(entry) {
182
+ const label = entry.issuer ? `${encodeURIComponent(entry.issuer)}:${encodeURIComponent(entry.account)}` : encodeURIComponent(entry.account);
183
+ const query = new URLSearchParams({ secret: entry.secret });
184
+ if (entry.issuer)
185
+ query.set("issuer", entry.issuer);
186
+ if (entry.digits !== DEFAULT_DIGITS)
187
+ query.set("digits", String(entry.digits));
188
+ if (entry.stepSeconds !== DEFAULT_STEP_SECONDS)
189
+ query.set("period", String(entry.stepSeconds));
190
+ if (entry.algorithm !== "SHA-1")
191
+ query.set("algorithm", entry.algorithm.replace("-", ""));
192
+ return `otpauth://totp/${label}?${query.toString()}`;
193
+ }
194
+ function fromManualSecret(secret, issuer = "", account = "") {
195
+ const cleaned = secret.replace(/\s|=/g, "").toUpperCase();
196
+ if (!cleaned)
197
+ throw new TotpError("OTPAUTH_NO_SECRET", "Enter the secret the site showed you.");
198
+ fromBase32(cleaned);
199
+ return {
200
+ type: "totp",
201
+ issuer: issuer.trim(),
202
+ account: account.trim(),
203
+ secret: cleaned,
204
+ digits: DEFAULT_DIGITS,
205
+ stepSeconds: DEFAULT_STEP_SECONDS,
206
+ algorithm: "SHA-1"
207
+ };
208
+ }
209
+ var newOtpAuth = (secretBytes, issuer, account) => ({
210
+ type: "totp",
211
+ issuer,
212
+ account,
213
+ secret: toBase32(secretBytes),
214
+ digits: DEFAULT_DIGITS,
215
+ stepSeconds: DEFAULT_STEP_SECONDS,
216
+ algorithm: "SHA-1"
217
+ });
218
+ export {
219
+ parseOtpAuth,
220
+ newOtpAuth,
221
+ fromManualSecret,
222
+ formatOtpAuth
223
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Write the event with the record; deliver it afterwards.
3
+ *
4
+ * `emit` in `@forgezero/access/effects` is only as reliable as the thing it
5
+ * publishes into. Sending straight from the handler loses the event whenever the
6
+ * process dies between the commit and the send — and that window is not
7
+ * theoretical: it is exactly where a deploy, an OOM kill or a failover lands,
8
+ * which is to say the moments somebody is watching.
9
+ *
10
+ * The inversion is the whole idea. Instead of
11
+ *
12
+ * commit the record → send the event (event lost if we die here)
13
+ *
14
+ * do
15
+ *
16
+ * commit the record AND the event together → send later, retrying
17
+ *
18
+ * The event is a row written by the same transaction that wrote the record, so
19
+ * either both exist or neither does. Delivery becomes a separate, restartable
20
+ * problem rather than a thing that had one chance to work.
21
+ *
22
+ * ## What this guarantees, exactly
23
+ *
24
+ * **At least once.** Not exactly once. A delivery can succeed and the write that
25
+ * marks it delivered can fail immediately afterwards, and the event is then sent
26
+ * again. There is no arrangement of two systems that avoids this — the
27
+ * acknowledgement is itself a message that can be lost.
28
+ *
29
+ * So consumers must be idempotent, and every event carries an `id` stable across
30
+ * redeliveries to make that possible. A package claiming exactly-once here would
31
+ * be claiming to have solved the two-generals problem, and the practical result
32
+ * of believing it is a double credit.
33
+ *
34
+ * ## Order is per key, not global
35
+ *
36
+ * A global order means one delivery at a time and throughput collapses. No order
37
+ * at all means `entry.updated` can overtake `entry.created`, and a consumer sees
38
+ * a change to something it has never heard of. Events therefore carry an
39
+ * optional `key` — an aggregate id — and delivery is strictly sequential within
40
+ * one key and fully parallel across keys. Same shape as `@forgezero/runtime/queue`, for
41
+ * the same reason.
42
+ */
43
+ export declare class OutboxError extends Error {
44
+ readonly code: 'NO_TRANSPORT' | 'NOT_CLAIMED' | 'UNKNOWN_EVENT';
45
+ constructor(code: 'NO_TRANSPORT' | 'NOT_CLAIMED' | 'UNKNOWN_EVENT', message: string);
46
+ }
47
+ export declare const EVENT_STATES: readonly ["pending", "delivering", "delivered", "dead"];
48
+ export type EventState = (typeof EVENT_STATES)[number];
49
+ export interface OutboxEvent {
50
+ id: string;
51
+ type: string;
52
+ /** The aggregate this concerns. Events sharing one are delivered in order. */
53
+ key?: string;
54
+ route?: string;
55
+ realm?: string;
56
+ payload: Record<string, unknown>;
57
+ state: EventState;
58
+ attempts: number;
59
+ /** Not eligible for delivery before this. Set by backoff after a failure. */
60
+ nextAttemptAtMs: number;
61
+ /** Set while a drainer holds it. A stale claim is reclaimable — see `claim`. */
62
+ claimedBy?: string;
63
+ claimedUntilMs?: number;
64
+ lastError?: string;
65
+ createdAtMs: number;
66
+ deliveredAtMs?: number;
67
+ }
68
+ /** What a caller publishes. Everything else is filled in. */
69
+ export interface PublishInput {
70
+ type: string;
71
+ key?: string;
72
+ route?: string;
73
+ realm?: string;
74
+ payload: Record<string, unknown>;
75
+ /**
76
+ * Supply this to make publishing idempotent.
77
+ *
78
+ * A handler that retries its own transaction would otherwise insert the event
79
+ * twice. With a stable id the second insert collides and is ignored, which is
80
+ * the behaviour every caller assumes they already have.
81
+ */
82
+ id?: string;
83
+ /** Hold the event back until a time — a reminder, a scheduled notice. */
84
+ notBeforeMs?: number;
85
+ }
86
+ export interface DeliveryPolicy {
87
+ /** Attempts before the event is dead-lettered. */
88
+ maxAttempts: number;
89
+ /** First retry delay. Doubles each attempt, capped. */
90
+ baseDelayMs: number;
91
+ maxDelayMs: number;
92
+ /**
93
+ * Random fraction added to each delay, 0–1.
94
+ *
95
+ * Without it, a relay that was down for a minute gets every event that failed
96
+ * during the outage retried in the same millisecond, and goes down again. The
97
+ * failure mode is self-inflicted and completely avoidable.
98
+ */
99
+ jitter: number;
100
+ /** How long a claim is honoured before another drainer may take the event. */
101
+ claimTtlMs: number;
102
+ }
103
+ export declare const DEFAULT_POLICY: DeliveryPolicy;
104
+ /**
105
+ * Exponential backoff with jitter.
106
+ *
107
+ * `random` is injectable so a test can assert the shape of the curve rather than
108
+ * a range — a bounds-only assertion passes for an implementation that returns
109
+ * the cap every time.
110
+ */
111
+ export declare function backoffMs(attempts: number, policy?: DeliveryPolicy, random?: () => number): number;
112
+ /**
113
+ * Persistence.
114
+ *
115
+ * `insert` is the load-bearing method: it must run inside the SAME transaction
116
+ * as the business write. That is why the store is injected rather than opened
117
+ * here — only the caller knows what transaction is open, and an outbox that
118
+ * opens its own connection has thrown away the one property it exists for.
119
+ */
120
+ export interface OutboxStore {
121
+ /** Returns false if an event with this id already exists. Never throws on collision. */
122
+ insert(event: OutboxEvent): Promise<boolean>;
123
+ /**
124
+ * Take up to `limit` deliverable events and mark them claimed.
125
+ *
126
+ * Must not return two events sharing a `key` in one batch, and must not
127
+ * return an event whose key has an older undelivered event — that is what
128
+ * makes per-key order hold. Must reclaim events whose `claimedUntilMs` has
129
+ * passed, or a drainer that crashes mid-batch strands them forever.
130
+ */
131
+ claim(args: {
132
+ drainerId: string;
133
+ limit: number;
134
+ nowMs: number;
135
+ claimTtlMs: number;
136
+ }): Promise<OutboxEvent[]>;
137
+ settle(id: string, patch: Partial<OutboxEvent>): Promise<void>;
138
+ byState(state: EventState, limit?: number): Promise<OutboxEvent[]>;
139
+ get(id: string): Promise<OutboxEvent | null>;
140
+ }
141
+ export interface DeliveryResult {
142
+ ok: boolean;
143
+ /** A failure that will never succeed — a malformed payload, a 400. */
144
+ terminal?: boolean;
145
+ error?: string;
146
+ }
147
+ export interface Transport {
148
+ deliver(event: OutboxEvent): Promise<DeliveryResult>;
149
+ }
150
+ export interface OutboxOptions {
151
+ store: OutboxStore;
152
+ transport: Transport;
153
+ policy?: Partial<DeliveryPolicy>;
154
+ /** Identifies this drainer in a claim. Must differ per process. */
155
+ drainerId?: string;
156
+ now?: () => number;
157
+ random?: () => number;
158
+ /** Called when an event exhausts its attempts. The one hook worth wiring. */
159
+ onDeadLetter?: (event: OutboxEvent) => void;
160
+ }
161
+ export interface DrainReport {
162
+ claimed: number;
163
+ delivered: number;
164
+ failed: number;
165
+ dead: number;
166
+ }
167
+ export declare function createOutbox(options: OutboxOptions): {
168
+ /**
169
+ * Record an event. Call this INSIDE the transaction that writes the record.
170
+ *
171
+ * Nothing is sent here. That is the point — the send is what can fail, and
172
+ * moving it out of the request means the request cannot fail because of it.
173
+ */
174
+ publish(input: PublishInput): Promise<OutboxEvent>;
175
+ /**
176
+ * Deliver a batch.
177
+ *
178
+ * Events are delivered concurrently because the store guarantees no two in
179
+ * one batch share a key — so parallelism here cannot reorder anything. That
180
+ * guarantee is the store's, and `memoryStore` below implements it exactly
181
+ * so a test cannot pass against a weaker one.
182
+ */
183
+ drain(limit?: number): Promise<DrainReport>;
184
+ /** Everything that gave up. The queue somebody has to actually look at. */
185
+ deadLetter: (limit?: number) => Promise<OutboxEvent[]>;
186
+ /**
187
+ * Put a dead event back in the queue.
188
+ *
189
+ * Attempts reset, because the reason it died has usually been fixed by hand
190
+ * and starting at attempt eight would dead-letter it again on the first
191
+ * hiccup.
192
+ */
193
+ retry(id: string): Promise<void>;
194
+ pending: (limit?: number) => Promise<OutboxEvent[]>;
195
+ drainerId: string;
196
+ };
197
+ export type Outbox = ReturnType<typeof createOutbox>;
198
+ /**
199
+ * The outbox as a job, for `@forgezero/runtime/jobs`.
200
+ *
201
+ * Returned as a plain spec rather than importing `defineJob`, so this package
202
+ * does not take a dependency on `jobs` for one object literal — and so a caller
203
+ * running its own loop is not forced to adopt a scheduler.
204
+ */
205
+ export declare function outboxJob(outbox: Outbox, options?: {
206
+ key?: string;
207
+ everyMs?: number;
208
+ batch?: number;
209
+ }): {
210
+ key: string;
211
+ everyMs: number;
212
+ run: () => Promise<{
213
+ ok: boolean;
214
+ detail: {
215
+ claimed: number;
216
+ delivered: number;
217
+ failed: number;
218
+ dead: number;
219
+ };
220
+ }>;
221
+ };
222
+ /**
223
+ * An in-memory store that enforces every constraint a real one must.
224
+ *
225
+ * Specifically the per-key ordering rule: a test that passes against a store
226
+ * which happily returns two events for one key would pass here and reorder in
227
+ * production. Encoding the constraint in the reference implementation is the
228
+ * only way the interface documentation is load-bearing.
229
+ */
230
+ export declare function memoryStore(): OutboxStore & {
231
+ all(): OutboxEvent[];
232
+ clear(): void;
233
+ };
234
+ export declare const VERSION = "0.1.0";