@forgezero/runtime 0.1.6 → 0.1.7
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 +595 -105
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,155 +1,645 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED FILE — do not edit.
|
|
3
|
+
|
|
4
|
+
Change scripts/generate-guides.ts or its typed sources, run `bun run guides`,
|
|
5
|
+
and commit the generator and rendered files together.
|
|
6
|
+
-->
|
|
7
|
+
|
|
1
8
|
# @forgezero/runtime
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
The machinery behind a request handler — jobs, queues, an outbox, a hash-chained audit trail, mail, backups, schema validation and exact money. 44 public modules, each imported on its own.
|
|
11
|
+
|
|
12
|
+
## Global package root and supported runtimes
|
|
13
|
+
|
|
14
|
+
Anyone running a service, for the work that happens outside a request. Separate from `access` because a Cloudflare Worker wants the request pipeline and cannot run a backup job. Supported runtimes: bun, node. The global base/root import is @forgezero/runtime. Every public import or command is listed below; the documentation inventory is checked in both directions against package.json exports.
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
16
|
+
```text
|
|
17
|
+
import * as root from '@forgezero/runtime';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
bun add @forgezero/runtime — Install runtime contracts; import the required subpath so unused capabilities stay out of the bundle.
|
|
23
|
+
|
|
24
|
+
```text
|
|
12
25
|
bun add @forgezero/runtime
|
|
13
26
|
```
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
## @forgezero/runtime/query
|
|
29
|
+
|
|
30
|
+
Provider-neutral typed function contracts: decode untrusted input, run with caller-supplied services or storage, and strictly validate the result without coupling business logic to ForgeZero or ArangoDB.
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
import * as api from '@forgezero/runtime/query';
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## @forgezero/runtime/jobs
|
|
37
|
+
|
|
38
|
+
Background work that never overlaps itself, advances a cursor only on success, and can be paused and inspected.
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
import * as api from '@forgezero/runtime/jobs';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## @forgezero/runtime/queue
|
|
45
|
+
|
|
46
|
+
Memory-only keyed work queue — awaited results, parallel across keys and strictly sequential within one; clustered callers atomically claim ownership in their own business store before submitting.
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
import * as api from '@forgezero/runtime/queue';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## @forgezero/runtime/outbox
|
|
53
|
+
|
|
54
|
+
Write the event with the record, deliver it after, in order per key with backoff and a dead-letter queue.
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
import * as api from '@forgezero/runtime/outbox';
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## @forgezero/runtime/audit
|
|
61
|
+
|
|
62
|
+
Append-only records chained by hash, with a verifier that names the first altered entry.
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
import * as api from '@forgezero/runtime/audit';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## @forgezero/runtime/backup
|
|
69
|
+
|
|
70
|
+
Encrypted, chunked, verified snapshots to object storage — and the restore that reads them back.
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
import * as api from '@forgezero/runtime/backup';
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## @forgezero/runtime/notify
|
|
77
|
+
|
|
78
|
+
Render a named template to text and HTML, escaped per part, refusing to send with a blank where a value should be.
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
import * as api from '@forgezero/runtime/notify';
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## @forgezero/runtime/notify/templates
|
|
85
|
+
|
|
86
|
+
The six transactional messages ForgeZero sends.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
import * as api from '@forgezero/runtime/notify/templates';
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## @forgezero/runtime/calendar
|
|
93
|
+
|
|
94
|
+
Billing periods computed from an anchor, working days, holidays and due dates.
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
import * as api from '@forgezero/runtime/calendar';
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## @forgezero/runtime/compliance
|
|
101
|
+
|
|
102
|
+
Screening as a decision record — tiers, rules and lists, failing closed when a list is unreachable.
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
import * as api from '@forgezero/runtime/compliance';
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## @forgezero/runtime/totp
|
|
109
|
+
|
|
110
|
+
RFC 6238 TOTP on the existing HMAC — base32, an asymmetric window, and replay left to the caller.
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
import * as api from '@forgezero/runtime/totp';
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## @forgezero/runtime/passkey
|
|
117
|
+
|
|
118
|
+
Passkeys for sites that are not us. A vault-held credential is as unphishable as one in a security chip provided the RP ID check never slips — `evil-example.com` ends with `example.com` and is a different site.
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
import * as api from '@forgezero/runtime/passkey';
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## @forgezero/runtime/phrase
|
|
125
|
+
|
|
126
|
+
BIP-39 recovery phrases, and the salted verifier that proves one without being able to reconstruct it.
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
import * as api from '@forgezero/runtime/phrase';
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## @forgezero/runtime/snp
|
|
133
|
+
|
|
134
|
+
Parse an AMD SEV-SNP attestation report at the firmware ABI offsets, and compare a TCB component by component so a microcode bump cannot mask a firmware downgrade.
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
import * as api from '@forgezero/runtime/snp';
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## @forgezero/runtime/importers
|
|
141
|
+
|
|
142
|
+
Read secrets out of a .env, a CSV, or a Bitwarden or 1Password export — skipping what cannot be understood rather than guessing, and never putting a value in an error.
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
import * as api from '@forgezero/runtime/importers';
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## @forgezero/runtime/openssh
|
|
149
|
+
|
|
150
|
+
OpenSSH wire encoding, so a derived ed25519 key becomes a line that pastes into authorized_keys.
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
import * as api from '@forgezero/runtime/openssh';
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## @forgezero/runtime/ssh-cert
|
|
157
|
+
|
|
158
|
+
OpenSSH certificates, so access expires instead of having to be hunted down. Takes a signing FUNCTION rather than a secret key, which is what lets the CA live in a vault that never hands it out.
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
import * as api from '@forgezero/runtime/ssh-cert';
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## @forgezero/runtime/slip10
|
|
165
|
+
|
|
166
|
+
SLIP-0010 derivation for ed25519, hardened-only — BIP-32 does not work on this curve and produces halves that do not correspond.
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
import * as api from '@forgezero/runtime/slip10';
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## @forgezero/runtime/identity
|
|
173
|
+
|
|
174
|
+
Hybrid Ed25519 + ML-DSA-65 request signing. One canonical string, so the compute agent that signs inside a guest and the API that verifies cannot drift — which two implementations of it certainly would.
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
import * as api from '@forgezero/runtime/identity';
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## @forgezero/runtime/schema
|
|
181
|
+
|
|
182
|
+
Validate against JSON Schema, restrict what a caller may declare, and describe a schema as a form.
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
import * as api from '@forgezero/runtime/schema';
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## @forgezero/runtime/schema/typebox
|
|
189
|
+
|
|
190
|
+
The TypeBox validator behind that interface.
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
import * as api from '@forgezero/runtime/schema/typebox';
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## @forgezero/runtime/finance/discounts
|
|
197
|
+
|
|
198
|
+
Promotions as arithmetic over integer minor units. They never stack — one winner — and a percentage rounds down, because rounding a discount up gives away a unit of currency per invoice forever.
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
import * as api from '@forgezero/runtime/finance/discounts';
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## @forgezero/runtime/finance/money
|
|
205
|
+
|
|
206
|
+
Exact amounts in minor units with the asset attached, so two currencies cannot be added.
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
import * as api from '@forgezero/runtime/finance/money';
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## @forgezero/runtime/finance/venues
|
|
213
|
+
|
|
214
|
+
Trading venues, market types and symbols as data — spot, margin and futures behind one order model.
|
|
215
|
+
|
|
216
|
+
```text
|
|
217
|
+
import * as api from '@forgezero/runtime/finance/venues';
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## @forgezero/runtime/finance/ledger
|
|
221
|
+
|
|
222
|
+
Double-entry postings and derived balances. A hold is a posting, not a lock — the queue does the ordering.
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
import * as api from '@forgezero/runtime/finance/ledger';
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## @forgezero/runtime/finance/commission
|
|
229
|
+
|
|
230
|
+
Profit net of flows, a high-water mark, the tier split and the referral share of our income.
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
import * as api from '@forgezero/runtime/finance/commission';
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## @forgezero/runtime/finance/rates
|
|
237
|
+
|
|
238
|
+
What an asset is worth in USD, and how old that answer is. A peg never ages; a quote always does.
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
import * as api from '@forgezero/runtime/finance/rates';
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## @forgezero/runtime/finance/transfers
|
|
245
|
+
|
|
246
|
+
Deposits and withdrawals as ordered pipelines, with screening reserved at position zero.
|
|
247
|
+
|
|
248
|
+
```text
|
|
249
|
+
import * as api from '@forgezero/runtime/finance/transfers';
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## @forgezero/runtime/finance/chain
|
|
253
|
+
|
|
254
|
+
Which chains exist, their confirmation depth by value band, assets and explorers — as data an admin edits.
|
|
255
|
+
|
|
256
|
+
```text
|
|
257
|
+
import * as api from '@forgezero/runtime/finance/chain';
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## @forgezero/runtime/finance/custody
|
|
261
|
+
|
|
262
|
+
Derive CREATE2 deposit addresses and EIP-712 custody digests — arithmetic over a seed and a salt, needing no credential and no node.
|
|
263
|
+
|
|
264
|
+
```text
|
|
265
|
+
import * as api from '@forgezero/runtime/finance/custody';
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## @forgezero/runtime/finance/derive
|
|
269
|
+
|
|
270
|
+
Declare a field ForgeZero generates rather than the tenant supplying: BIP-44 path arithmetic, and the choice of who is capable of generating the key.
|
|
271
|
+
|
|
272
|
+
```text
|
|
273
|
+
import * as api from '@forgezero/runtime/finance/derive';
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## @forgezero/runtime/finance/tax
|
|
277
|
+
|
|
278
|
+
Which jurisdiction may tax a sale, who accounts for it, and the three ways to be zero that report differently. Rates are data.
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
import * as api from '@forgezero/runtime/finance/tax';
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## @forgezero/runtime/finance/storage
|
|
285
|
+
|
|
286
|
+
Write an amount so it is both exact and sortable: an authoritative string, and a number that is only an index.
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
import * as api from '@forgezero/runtime/finance/storage';
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## @forgezero/runtime/realtime
|
|
293
|
+
|
|
294
|
+
Provider-neutral realtime audience, shard, event and delivery contracts.
|
|
295
|
+
|
|
296
|
+
```text
|
|
297
|
+
import * as api from '@forgezero/runtime/realtime';
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## @forgezero/runtime/passkey-hybrid
|
|
301
|
+
|
|
302
|
+
Versioned WebAuthn PRF plus ML-DSA companion proof construction and verification.
|
|
303
|
+
|
|
304
|
+
```text
|
|
305
|
+
import * as api from '@forgezero/runtime/passkey-hybrid';
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## @forgezero/runtime/otpauth
|
|
309
|
+
|
|
310
|
+
Parse and render otpauth URIs without binding enrolment to a UI framework.
|
|
311
|
+
|
|
312
|
+
```text
|
|
313
|
+
import * as api from '@forgezero/runtime/otpauth';
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## @forgezero/runtime/pipeline
|
|
317
|
+
|
|
318
|
+
Typed ordered application-pipeline execution with explicit evidence.
|
|
319
|
+
|
|
320
|
+
```text
|
|
321
|
+
import * as api from '@forgezero/runtime/pipeline';
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## @forgezero/runtime/finance/chain-addresses
|
|
325
|
+
|
|
326
|
+
Chain-address derivation records and validation independent of a node provider.
|
|
327
|
+
|
|
328
|
+
```text
|
|
329
|
+
import * as api from '@forgezero/runtime/finance/chain-addresses';
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## @forgezero/runtime/finance/chain-deposits
|
|
333
|
+
|
|
334
|
+
Provider-neutral deposit observation, confirmation and credit transitions.
|
|
335
|
+
|
|
336
|
+
```text
|
|
337
|
+
import * as api from '@forgezero/runtime/finance/chain-deposits';
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## @forgezero/runtime/finance/chain-withdrawals
|
|
341
|
+
|
|
342
|
+
Provider-neutral withdrawal approval, broadcast and finality transitions.
|
|
343
|
+
|
|
344
|
+
```text
|
|
345
|
+
import * as api from '@forgezero/runtime/finance/chain-withdrawals';
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## @forgezero/runtime/finance/chain-reconcile
|
|
349
|
+
|
|
350
|
+
Deterministic reconciliation between chain observations and durable transfer state.
|
|
351
|
+
|
|
352
|
+
```text
|
|
353
|
+
import * as api from '@forgezero/runtime/finance/chain-reconcile';
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## @forgezero/runtime/finance/market
|
|
357
|
+
|
|
358
|
+
Market order, fill and quote contracts independent of any exchange adapter.
|
|
359
|
+
|
|
360
|
+
```text
|
|
361
|
+
import * as api from '@forgezero/runtime/finance/market';
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## @forgezero/runtime/custody-share
|
|
365
|
+
|
|
366
|
+
Threshold-share parsing, validation and reconstruction.
|
|
367
|
+
|
|
368
|
+
```text
|
|
369
|
+
import * as api from '@forgezero/runtime/custody-share';
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## @forgezero/runtime/custody-crypto
|
|
373
|
+
|
|
374
|
+
Hybrid ML-KEM-768 plus X25519 custody-share sealing and opening.
|
|
375
|
+
|
|
376
|
+
```text
|
|
377
|
+
import * as api from '@forgezero/runtime/custody-crypto';
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Define and implement a typed query
|
|
381
|
+
|
|
382
|
+
Decode untrusted input and validate output around injected storage/services rather than embedding a database query in the route adapter.
|
|
383
|
+
|
|
384
|
+
```text
|
|
385
|
+
import { defineQuery, implementQuery } from '@forgezero/runtime/query';
|
|
386
|
+
import { T, type Static, typeboxQueryCodec } from '@forgezero/runtime/schema/typebox';
|
|
387
|
+
|
|
388
|
+
const OrderKey = T.Object({ orderKey: T.String({ minLength: 1 }) });
|
|
389
|
+
const Order = T.Object({ orderKey: T.String(), total: T.String() });
|
|
390
|
+
type OrderRow = Static<typeof Order>;
|
|
391
|
+
type Stores = { orders: { find(key: string): Promise<OrderRow | null> } };
|
|
392
|
+
declare const stores: Stores;
|
|
393
|
+
|
|
394
|
+
const findOrder = defineQuery({
|
|
395
|
+
name: 'orders.find',
|
|
396
|
+
input: typeboxQueryCodec(OrderKey),
|
|
397
|
+
output: typeboxQueryCodec(T.Union([Order, T.Null()]))
|
|
398
|
+
});
|
|
399
|
+
export const runFindOrder = implementQuery(findOrder,
|
|
400
|
+
(context: Stores, input) => context.orders.find(input.orderKey)
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const order = await runFindOrder.execute(stores, { orderKey: 'ord_123' });
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## 1. Install, then import a subpath
|
|
407
|
+
|
|
408
|
+
There is no root export, and that is deliberate. `import from "@forgezero/runtime"` is meant to fail rather than resolve to whichever module happened to be listed first — a bare import that silently works is how a project ends up depending on the whole package to use one function. Every module is its own entry point, so a bundler includes what you imported and nothing else.
|
|
409
|
+
|
|
410
|
+
```text
|
|
411
|
+
bun add @forgezero/runtime
|
|
412
|
+
|
|
16
413
|
import { parseAmount } from '@forgezero/runtime/finance/money';
|
|
17
414
|
import { createScheduler } from '@forgezero/runtime/jobs';
|
|
18
415
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
416
|
+
// This throws. It is supposed to.
|
|
417
|
+
// import { anything } from '@forgezero/runtime';
|
|
21
418
|
```
|
|
22
419
|
|
|
23
420
|
## What is in it
|
|
24
421
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
422
|
+
Four groups. The service spine is what a process needs to keep working after the response is sent; the record keeps evidence; the finance modules are the ones that must never be approximated. Nothing here knows what your product does — a tenant binds its own business rules to these, it does not fork them.
|
|
423
|
+
|
|
424
|
+
```text
|
|
425
|
+
SPINE jobs scheduled work submitted through the common queue
|
|
426
|
+
queue awaited result; parallel keys, sequential within one
|
|
427
|
+
outbox business-owned durability, drained through handlers
|
|
428
|
+
|
|
429
|
+
RECORD audit append-only, hash-chained, names the first altered row
|
|
430
|
+
backup encrypted, chunked, verified snapshots + the restore
|
|
431
|
+
compliance screening as a decision record, failing closed
|
|
432
|
+
calendar billing periods, working days, holidays, due dates
|
|
433
|
+
|
|
434
|
+
IDENTITY identity hybrid Ed25519 + ML-DSA-65 request signing
|
|
435
|
+
totp RFC 6238, asymmetric window, replay rejected
|
|
436
|
+
notify named templates to text and HTML, escaped per part
|
|
437
|
+
schema JSON Schema validation, restricted and describable
|
|
438
|
+
|
|
439
|
+
FINANCE money minor units with the asset attached
|
|
440
|
+
storage the same amount, exact AND sortable in a database
|
|
441
|
+
ledger double-entry postings and derived balances
|
|
442
|
+
commission profit net of flows, high-water mark, referral split
|
|
443
|
+
rates what an asset is worth, and how old that answer is
|
|
444
|
+
transfers deposits and withdrawals as ordered pipelines
|
|
445
|
+
tax who may tax a sale, and who accounts for it
|
|
446
|
+
chain chains, assets, confirmation depth by value band
|
|
447
|
+
custody CREATE2 deposit addresses and EIP-712 digests
|
|
448
|
+
derive declare a key ForgeZero generates, not the tenant
|
|
449
|
+
venues trading venues, market types and symbols as data
|
|
450
|
+
binance Binance behind the venue adapter
|
|
451
|
+
```
|
|
31
452
|
|
|
32
|
-
|
|
33
|
-
do not fork them.
|
|
453
|
+
## Optional peers — install only what your subpath needs
|
|
34
454
|
|
|
35
|
-
|
|
455
|
+
Nothing third-party is bundled. Vendoring a crypto library means a fix for it never reaches you until this package is republished, so the imports stay real and the dependency stays yours. Most subpaths need nothing; four of them do, and the failure is a plain module-not-found rather than a silently wrong result.
|
|
36
456
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
wire values; outputs are checked strictly so a wrong handler result cannot be
|
|
40
|
-
coerced into looking correct.
|
|
457
|
+
```text
|
|
458
|
+
# most subpaths: nothing to add
|
|
41
459
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
import { typeboxQueryCodec, T } from '@forgezero/runtime/schema/typebox';
|
|
460
|
+
# finance/custody — CREATE2 and EIP-712 digests
|
|
461
|
+
bun add @noble/hashes
|
|
45
462
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
input: typeboxQueryCodec(T.Object({ reference: T.String() })),
|
|
49
|
-
output: typeboxQueryCodec(T.Object({ total: T.String() }))
|
|
50
|
-
});
|
|
463
|
+
# identity — hybrid post-quantum signing
|
|
464
|
+
bun add @noble/curves @noble/post-quantum
|
|
51
465
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
466
|
+
# schema/typebox — only if you validate with TypeBox
|
|
467
|
+
bun add @sinclair/typebox
|
|
55
468
|
```
|
|
56
469
|
|
|
57
|
-
##
|
|
470
|
+
## Money is never a number
|
|
471
|
+
|
|
472
|
+
An amount is minor units as a bigint with its asset attached, so two currencies cannot be added by accident and a rounding mode is always stated. A database needs a second thing the bigint cannot give it — an ORDER BY that works — so `finance/storage` writes both: the authoritative string, and a lossy double used for sorting and range filters only. One ETH is 10^18 minor units and a signed 64-bit column overflows at about nine ETH, which is why the exact value is never the sortable one.
|
|
473
|
+
|
|
474
|
+
```text
|
|
475
|
+
import { parseAmount, mulRate, formatAmount } from '@forgezero/runtime/finance/money';
|
|
476
|
+
import { toStored, rangeBounds } from '@forgezero/runtime/finance/storage';
|
|
477
|
+
|
|
478
|
+
const fee = mulRate(parseAmount('1250.00', 'USD'), '0.015', 'down');
|
|
479
|
+
formatAmount(fee); // '18.75' — never 18.749999999
|
|
480
|
+
|
|
481
|
+
const row = toStored(fee);
|
|
482
|
+
// { units: '1875', value: '18.75', asset: 'USD', sort: 18.75 }
|
|
483
|
+
// units → what you pay out. sort → what you ORDER BY.
|
|
58
484
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
different keys in parallel. Persistence and cluster ownership remain in the
|
|
62
|
-
business layer that knows what a pending request means; ForgeZero uses an
|
|
63
|
-
ArangoDB unique claim, while another caller may use any database or no database.
|
|
485
|
+
rangeBounds(parseAmount('10', 'USD'), null); // { gte: 10 }
|
|
486
|
+
```
|
|
64
487
|
|
|
65
|
-
|
|
66
|
-
import { createQueue } from '@forgezero/runtime/queue';
|
|
488
|
+
## The contracts ship with it
|
|
67
489
|
|
|
68
|
-
|
|
69
|
-
// Use `width` for an exact ceiling, or an explicit dynamic resource policy:
|
|
70
|
-
const queue = createQueue({ resources: { percent: 60, reserve: 1, max: 32 } });
|
|
71
|
-
const task = queue.run('tenant-a:wallet-7', transfer, amount, destination);
|
|
72
|
-
const receipt = await task.result;
|
|
490
|
+
The Solidity behind `finance/custody` is in the package rather than in a repository you have to go find. `finance/custody` computes the counterfactual address; `DepositFactory` is what makes that address real on first sweep, and `ColdVault` is the M-of-N EIP-712 approval the digests are built for. Address arithmetic without the contract that honours it is half an answer.
|
|
73
491
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
492
|
+
```text
|
|
493
|
+
node_modules/@forgezero/runtime/contracts/src/
|
|
494
|
+
DepositProxy.sol lazily deployed at first sweep — no key, no gas per address
|
|
495
|
+
DepositFactory.sol CREATE2, salt opaque to the contract
|
|
496
|
+
ColdVault.sol M-of-N, signatures ordered by ascending signer
|
|
497
|
+
SafeTransferLib.sol tokens that do not return a bool
|
|
80
498
|
```
|
|
81
499
|
|
|
82
|
-
|
|
83
|
-
multi-core merely by entering a queue: put that handler in Bun/standard Workers
|
|
84
|
-
and await the Worker result from the queue.
|
|
500
|
+
## Jobs — background work that never overlaps
|
|
85
501
|
|
|
86
|
-
|
|
87
|
-
IANA timezone and weekday filter. `overlap: 'wait'` (the default) schedules the
|
|
88
|
-
next run after completion; `overlap: 'skip'` keeps clock cadence and drops a tick
|
|
89
|
-
when the same key is still busy. Same-key overlap is never allowed.
|
|
502
|
+
The lock and the cursor store are interfaces, so this runs against a database, Redis, or nothing at all in a test.
|
|
90
503
|
|
|
91
|
-
```
|
|
92
|
-
defineJob
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
504
|
+
```text
|
|
505
|
+
import { createScheduler, defineJob, cursorJob } from '@forgezero/runtime/jobs';
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
## Never setInterval
|
|
509
|
+
|
|
510
|
+
An interval fires whether or not the previous run finished, so work that takes longer than its period ends up running twice over the same data. The next run is scheduled after the current one completes, which makes the overlap impossible rather than unlikely.
|
|
511
|
+
|
|
512
|
+
```text
|
|
513
|
+
const scheduler = createScheduler({
|
|
514
|
+
jobs: [
|
|
515
|
+
defineJob({ key: 'reap-sessions', label: 'Reap sessions', every: '5m', run: reap }),
|
|
516
|
+
defineJob({ key: 'probe-providers', label: 'Probe providers', every: '30s', run: probe })
|
|
517
|
+
],
|
|
518
|
+
lock: storeLock(lockStore)
|
|
98
519
|
});
|
|
520
|
+
|
|
521
|
+
scheduler.start();
|
|
99
522
|
```
|
|
100
523
|
|
|
101
|
-
##
|
|
524
|
+
## A lease, not a mutex
|
|
102
525
|
|
|
103
|
-
|
|
104
|
-
asset attached, so two currencies cannot be added by accident and a rounding
|
|
105
|
-
mode is always stated. `finance/storage` writes the same amount twice — the
|
|
106
|
-
authoritative string, and a lossy double used for `ORDER BY` only. One ETH is
|
|
107
|
-
10^18 minor units and a signed 64-bit column overflows around nine ETH, which is
|
|
108
|
-
why the exact value is never the sortable one.
|
|
526
|
+
A process that dies holding a mutex blocks its job for ever, and somebody clears it by hand at three in the morning. A lease expires on its own. The fence number rises each time the lock is granted, so a run that stalled past its lease and woke up finds its fence stale and stops before writing.
|
|
109
527
|
|
|
110
|
-
```
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
528
|
+
```text
|
|
529
|
+
const lease = await lock.acquire('scan', 60_000); // undefined if held
|
|
530
|
+
await lock.renew('scan', lease.fence, 60_000); // false once superseded
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## The cursor advances only on a complete batch
|
|
534
|
+
|
|
535
|
+
Fetch a batch, process every item, then write the cursor — never per item and never before. If item three of ten throws, the whole batch is retried from the same position. That means process must be idempotent, and idempotent retries are strictly better than the alternative, which is records nobody ever looks at again.
|
|
536
|
+
|
|
537
|
+
```text
|
|
538
|
+
cursorJob({
|
|
539
|
+
key: 'scan-deposits',
|
|
540
|
+
label: 'Scan deposits',
|
|
541
|
+
every: '15s',
|
|
542
|
+
store: cursors,
|
|
543
|
+
from: '0',
|
|
544
|
+
fetch: (cursor) => fetchBlocks(cursor),
|
|
545
|
+
process: (transfer) => credit(transfer) // keyed, so a retry is free
|
|
546
|
+
});
|
|
114
547
|
```
|
|
115
548
|
|
|
116
|
-
|
|
117
|
-
run finished, so work slower than its period runs twice over the same data. The
|
|
118
|
-
next run is scheduled after the current one completes. The lock is a lease with
|
|
119
|
-
a fence, not a mutex — a process that dies does not block its job for ever.
|
|
549
|
+
## Stop waits
|
|
120
550
|
|
|
121
|
-
|
|
122
|
-
then write the cursor — never per item, never before. Item three of ten throwing
|
|
123
|
-
retries the whole batch from the same position, which means `process` must be
|
|
124
|
-
idempotent, and idempotent retries beat records nobody ever looks at again.
|
|
551
|
+
Returning from stop() before in-flight work settles is how a deploy leaves a record half-written and the next boot finds state nothing explains. Timers are cleared, the abort signal fires so long runs can cut themselves short, and then it awaits what is still running.
|
|
125
552
|
|
|
126
|
-
|
|
553
|
+
```text
|
|
554
|
+
await scheduler.stop(); // clears timers, aborts, awaits in-flight
|
|
555
|
+
scheduler.pause(); // stop scheduling, let in-flight finish
|
|
556
|
+
await scheduler.runNow('scan-deposits');
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Status somebody can read during an incident
|
|
127
560
|
|
|
128
|
-
|
|
129
|
-
never reaches you until this package republishes. Most subpaths need nothing:
|
|
561
|
+
Last run, duration, result, error and consecutive failures per job. Without it a job that has been failing for a week looks exactly like a job that has been succeeding.
|
|
130
562
|
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
563
|
+
```text
|
|
564
|
+
scheduler.status();
|
|
565
|
+
// [{ key: 'scan-deposits', state: 'idle', runs: 412,
|
|
566
|
+
// lastDurationMs: 840, lastResult: { processed: 17, batches: 2 },
|
|
567
|
+
// consecutiveFailures: 0, skippedLocked: 0 }]
|
|
135
568
|
```
|
|
136
569
|
|
|
137
|
-
##
|
|
570
|
+
## Schema — validation you can also render
|
|
138
571
|
|
|
139
|
-
|
|
140
|
-
contracts that make those addresses real are in the tarball, not in a repository
|
|
141
|
-
you have to go find.
|
|
572
|
+
TypeBox is a peer dependency and optional. A project on Zod pulls none of it, because the interface is what the other packages depend on.
|
|
142
573
|
|
|
574
|
+
```text
|
|
575
|
+
import { validate, describeForm } from '@forgezero/runtime/schema';
|
|
576
|
+
|
|
577
|
+
// only if you validate with TypeBox
|
|
578
|
+
bun add @sinclair/typebox
|
|
143
579
|
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
580
|
+
|
|
581
|
+
## 2. Validate
|
|
582
|
+
|
|
583
|
+
Query strings are entirely strings, so values are converted before checking — otherwise ?port=587 fails a schema expecting a number on a perfectly well-formed request.
|
|
584
|
+
|
|
585
|
+
```text
|
|
586
|
+
import { typebox, T } from '@forgezero/runtime/schema/typebox';
|
|
587
|
+
|
|
588
|
+
const Config = T.Object(
|
|
589
|
+
{ host: T.String(), port: T.Integer() },
|
|
590
|
+
{ additionalProperties: false }
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
typebox.validate(Config, { host: 'mail', port: '587' });
|
|
594
|
+
// { ok: true, value: { host: 'mail', port: 587 } }
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
## 3. Render a form from it
|
|
598
|
+
|
|
599
|
+
describeForm turns a schema into flat, framework-agnostic fields. This is what lets an admin screen render provider credentials and security factors with no per-feature UI code.
|
|
600
|
+
|
|
601
|
+
```text
|
|
602
|
+
typebox.describeForm(Config);
|
|
603
|
+
// [{ path: 'host', label: 'Host', kind: 'string', required: true }, ...]
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## The restrictions are a security boundary
|
|
607
|
+
|
|
608
|
+
A schema authored in your own source is trusted. One submitted by a tenant is not, and these limits are the only thing between the two. $ref would let a schema point validation at a document you do not control; unbounded depth is a denial-of-service; and an object accepting unknown properties is one that lets an unvalidated field ride into an envelope.
|
|
609
|
+
|
|
610
|
+
```text
|
|
611
|
+
typebox.restrict(schema)
|
|
612
|
+
|
|
613
|
+
$ref · $id · $defs refused
|
|
614
|
+
depth > 4 refused
|
|
615
|
+
more than 100 fields refused
|
|
616
|
+
larger than 64 KiB refused
|
|
617
|
+
additionalProperties must be false
|
|
148
618
|
```
|
|
149
619
|
|
|
150
|
-
|
|
620
|
+
## writeOnly is the vault boundary
|
|
151
621
|
|
|
152
|
-
|
|
622
|
+
A field marked writeOnly is never returned to a browser. readableFields strips them at any depth, and writeOnlyPaths lists exactly what must be routed to secret storage instead.
|
|
623
|
+
|
|
624
|
+
```text
|
|
625
|
+
const fields = typebox.describeForm(schema);
|
|
626
|
+
|
|
627
|
+
readableFields(fields); // safe to serialise
|
|
628
|
+
writeOnlyPaths(fields); // ['apiKey', 'nested.secret']
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
## Adding another validator
|
|
632
|
+
|
|
633
|
+
Implement four functions. toJsonSchema is what keeps the ecosystem from fragmenting: stored config, the admin UI and the wire all speak JSON Schema, so a Zod project and a TypeBox project produce identical documents and either can read the other.
|
|
634
|
+
|
|
635
|
+
```text
|
|
636
|
+
interface SchemaValidator<S> {
|
|
637
|
+
name: string;
|
|
638
|
+
validate(schema: S, value: unknown): ValidationResult;
|
|
639
|
+
describeForm(schema: S): FormField[];
|
|
640
|
+
toJsonSchema(schema: S): Record<string, unknown>;
|
|
641
|
+
restrict(schema: S, limits?: Restrictions): S;
|
|
642
|
+
}
|
|
643
|
+
```
|
|
153
644
|
|
|
154
|
-
|
|
155
|
-
deploys — and usable entirely on its own, with no ForgeZero account.
|
|
645
|
+
Full rendered documentation: https://www.forgezero.net/docs/runtime
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgezero/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
"prepublishOnly": "bun run check && bun run build"
|
|
192
192
|
},
|
|
193
193
|
"dependencies": {
|
|
194
|
-
"@forgezero/access": "^0.1.
|
|
194
|
+
"@forgezero/access": "^0.1.3"
|
|
195
195
|
},
|
|
196
196
|
"peerDependencies": {
|
|
197
197
|
"@noble/ciphers": "^2.2.0",
|