@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
package/dist/queue.js ADDED
@@ -0,0 +1,246 @@
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/queue.ts
10
+ var systemClock = { now: () => Date.now() };
11
+ var UNITS = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
12
+ function durationMs(value) {
13
+ if (typeof value === "number")
14
+ return value;
15
+ const match = /^(\d+)([smhd])$/.exec(value);
16
+ if (!match)
17
+ throw new Error(`"${value}" is not a duration like 30s, 5m, 1h, 1d.`);
18
+ return Number(match[1]) * UNITS[match[2]];
19
+ }
20
+ var MODES = ["standalone", "cluster"];
21
+ function memoryStore(clock = systemClock) {
22
+ const messages = new Map;
23
+ const leases = new Map;
24
+ let fences = 0;
25
+ const all = (queue) => messages.get(queue) ?? [];
26
+ return {
27
+ mode: "standalone",
28
+ async append(queue, message) {
29
+ messages.set(queue, [...all(queue), message]);
30
+ },
31
+ async findByDedupe(queue, dedupeKey, sinceMs) {
32
+ return all(queue).find((message) => message.dedupeKey === dedupeKey && message.enqueuedAtMs >= sinceMs) ?? null;
33
+ },
34
+ async claimKey(queue, owner, ttlMs, nowMs) {
35
+ const ready = all(queue).filter((message) => message.status === "ready" && message.availableAtMs <= nowMs);
36
+ for (const message of ready.sort((a, b) => a.sequence - b.sequence)) {
37
+ const held = leases.get(`${queue}\x00${message.key}`);
38
+ if (held && held.untilMs > nowMs)
39
+ continue;
40
+ fences += 1;
41
+ const lease = { key: message.key, fence: fences, untilMs: nowMs + ttlMs, owner };
42
+ leases.set(`${queue}\x00${message.key}`, lease);
43
+ return lease;
44
+ }
45
+ return null;
46
+ },
47
+ async renewKey(queue, lease, ttlMs, nowMs) {
48
+ const held = leases.get(`${queue}\x00${lease.key}`);
49
+ if (!held || held.fence !== lease.fence)
50
+ return false;
51
+ held.untilMs = nowMs + ttlMs;
52
+ return true;
53
+ },
54
+ async releaseKey(queue, lease) {
55
+ const held = leases.get(`${queue}\x00${lease.key}`);
56
+ if (held?.fence === lease.fence)
57
+ leases.delete(`${queue}\x00${lease.key}`);
58
+ },
59
+ async readKey(queue, key, nowMs, limit) {
60
+ return all(queue).filter((message) => message.key === key && message.status === "ready" && message.availableAtMs <= nowMs).sort((a, b) => a.sequence - b.sequence).slice(0, limit);
61
+ },
62
+ async update(queue, id, patch) {
63
+ const message = all(queue).find((entry) => entry.id === id);
64
+ if (message)
65
+ Object.assign(message, patch);
66
+ },
67
+ async stats(queue) {
68
+ const list = all(queue);
69
+ return {
70
+ ready: list.filter((message) => message.status === "ready").length,
71
+ leased: list.filter((message) => message.status === "leased").length,
72
+ dead: list.filter((message) => message.status === "dead").length,
73
+ keys: new Set(list.filter((message) => message.status === "ready").map((m) => m.key)).size
74
+ };
75
+ },
76
+ async dead(queue, limit) {
77
+ return all(queue).filter((message) => message.status === "dead").sort((a, b) => b.sequence - a.sequence).slice(0, limit);
78
+ }
79
+ };
80
+ }
81
+ function createQueue(options) {
82
+ const {
83
+ name,
84
+ store,
85
+ handler,
86
+ concurrency = 4,
87
+ leaseMs = 30000,
88
+ maxAttempts = 5,
89
+ backoffMs = 1000,
90
+ maxBackoffMs = 60000,
91
+ dedupeWindowMs = 300000,
92
+ batch = 32,
93
+ clock = systemClock,
94
+ owner = `worker-${Math.trunc(clock.now())}`
95
+ } = options;
96
+ if (options.require && store.mode !== options.require) {
97
+ throw new Error(`Queue "${name}" requires a ${options.require} store but was given a ${store.mode} one. ` + "A memory store loses everything on restart.");
98
+ }
99
+ let sequence = 0;
100
+ let running = false;
101
+ let inFlight = 0;
102
+ const held = new Set;
103
+ const backoffFor = (attempt) => Math.min(maxBackoffMs, backoffMs * 2 ** Math.max(0, attempt - 1));
104
+ async function enqueue(args) {
105
+ if (!args.key)
106
+ throw new Error("Every message needs a key — it is what orders the work.");
107
+ const nowMs = clock.now();
108
+ if (args.dedupeKey) {
109
+ const existing = await store.findByDedupe(name, args.dedupeKey, nowMs - dedupeWindowMs);
110
+ if (existing)
111
+ return { id: existing.id, duplicate: true };
112
+ }
113
+ sequence += 1;
114
+ const message = {
115
+ id: `${name}-${nowMs}-${sequence}`,
116
+ key: args.key,
117
+ body: args.body,
118
+ sequence,
119
+ status: "ready",
120
+ attempts: 0,
121
+ availableAtMs: nowMs + (args.delayMs ?? 0),
122
+ dedupeKey: args.dedupeKey,
123
+ enqueuedAtMs: nowMs
124
+ };
125
+ await store.append(name, message);
126
+ return { id: message.id, duplicate: false };
127
+ }
128
+ async function drainKey(lease) {
129
+ try {
130
+ for (let taken = 0;taken < batch; taken += 1) {
131
+ const nowMs = clock.now();
132
+ if (!await store.renewKey(name, lease, leaseMs, nowMs))
133
+ return;
134
+ const [message] = await store.readKey(name, lease.key, nowMs, 1);
135
+ if (!message)
136
+ return;
137
+ await store.update(name, message.id, { status: "leased", attempts: message.attempts + 1 });
138
+ try {
139
+ await handler({ ...message, attempts: message.attempts + 1 }, {
140
+ holdsKey: () => store.renewKey(name, lease, leaseMs, clock.now()),
141
+ log: (line) => options.onError?.({ ...message }, line),
142
+ attempt: message.attempts + 1
143
+ });
144
+ await store.update(name, message.id, { status: "done", completedAtMs: clock.now() });
145
+ } catch (cause) {
146
+ const attempts = message.attempts + 1;
147
+ const reason = cause instanceof Error ? cause.message : String(cause);
148
+ options.onError?.(message, cause);
149
+ if (attempts >= maxAttempts) {
150
+ await store.update(name, message.id, { status: "dead", lastError: reason });
151
+ continue;
152
+ }
153
+ await store.update(name, message.id, {
154
+ status: "ready",
155
+ lastError: reason,
156
+ availableAtMs: clock.now() + backoffFor(attempts)
157
+ });
158
+ return;
159
+ }
160
+ }
161
+ } finally {
162
+ await store.releaseKey(name, lease);
163
+ held.delete(lease.key);
164
+ }
165
+ }
166
+ async function tick() {
167
+ const claimed = [];
168
+ while (inFlight + claimed.length < concurrency) {
169
+ const lease = await store.claimKey(name, owner, leaseMs, clock.now());
170
+ if (!lease)
171
+ break;
172
+ if (held.has(lease.key))
173
+ break;
174
+ held.add(lease.key);
175
+ claimed.push(lease);
176
+ }
177
+ if (claimed.length === 0)
178
+ return 0;
179
+ inFlight += claimed.length;
180
+ try {
181
+ await Promise.all(claimed.map((lease) => drainKey(lease)));
182
+ } finally {
183
+ inFlight -= claimed.length;
184
+ }
185
+ return claimed.length;
186
+ }
187
+ return {
188
+ enqueue,
189
+ tick,
190
+ async drain(maxPasses = 100) {
191
+ for (let pass = 0;pass < maxPasses; pass += 1) {
192
+ if (await tick() === 0)
193
+ return;
194
+ }
195
+ },
196
+ start(intervalMs = 200) {
197
+ if (running)
198
+ return;
199
+ running = true;
200
+ const loop = async () => {
201
+ while (running) {
202
+ try {
203
+ if (await tick() === 0)
204
+ await new Promise((r) => setTimeout(r, intervalMs));
205
+ } catch {
206
+ await new Promise((r) => setTimeout(r, intervalMs));
207
+ }
208
+ }
209
+ };
210
+ loop();
211
+ },
212
+ stop() {
213
+ running = false;
214
+ },
215
+ mode: store.mode,
216
+ stats: () => store.stats(name),
217
+ dead: (limit = 50) => store.dead(name, limit),
218
+ async revive(id) {
219
+ await store.update(name, id, { status: "ready", attempts: 0, availableAtMs: clock.now() });
220
+ }
221
+ };
222
+ }
223
+ var VERSION = "0.1.0";
224
+ function clusterStore(adapter) {
225
+ return {
226
+ mode: "cluster",
227
+ append: (queue, message) => adapter.insert(queue, message),
228
+ findByDedupe: (queue, dedupeKey, sinceMs) => adapter.findDuplicate(queue, dedupeKey, sinceMs),
229
+ claimKey: (queue, owner, ttlMs, nowMs) => adapter.takeKey({ queue, owner, untilMs: nowMs + ttlMs, nowMs }),
230
+ renewKey: (queue, lease, ttlMs, nowMs) => adapter.extendKey(queue, lease.key, lease.fence, nowMs + ttlMs),
231
+ releaseKey: (queue, lease) => adapter.dropKey(queue, lease.key, lease.fence),
232
+ readKey: (queue, key, nowMs, limit) => adapter.readReady(queue, key, nowMs, limit),
233
+ update: (queue, id, patch) => adapter.patch(queue, id, patch),
234
+ stats: (queue) => adapter.counts(queue),
235
+ dead: (queue, limit) => adapter.deadLetter(queue, limit)
236
+ };
237
+ }
238
+ export {
239
+ systemClock,
240
+ memoryStore,
241
+ durationMs,
242
+ createQueue,
243
+ clusterStore,
244
+ VERSION,
245
+ MODES
246
+ };
@@ -0,0 +1,24 @@
1
+ import type { TSchema, Static } from '@sinclair/typebox';
2
+ import { type SchemaValidator } from './schema';
3
+ /**
4
+ * TypeBox implementation of `SchemaValidator`.
5
+ *
6
+ * TypeBox is a peer dependency and optional: the interface is what
7
+ * `@forgezero/access` depends on, so a project using Zod pulls no TypeBox at
8
+ * all. Import this module only if you want it.
9
+ *
10
+ * TypeBox schemas ARE JSON Schema, so `toJsonSchema` is the identity and the
11
+ * shared restriction and form logic applies without translation. That is why it
12
+ * ships first — anything needing a conversion step gets to prove the interface
13
+ * survives one.
14
+ */
15
+ export declare const typebox: SchemaValidator<TSchema>;
16
+ /**
17
+ * Parse-or-throw, for call sites where a failure is a bug rather than user
18
+ * input. Route request validation uses `validate` and answers 422; this is for
19
+ * config read at boot, where continuing with a bad value is worse than
20
+ * refusing to start.
21
+ */
22
+ export declare function parse<T extends TSchema>(schema: T, value: unknown): Static<T>;
23
+ export { Type as T } from '@sinclair/typebox';
24
+ export type { TSchema, Static } from '@sinclair/typebox';
@@ -0,0 +1,201 @@
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/schema.ts
10
+ class SchemaError extends Error {
11
+ code;
12
+ path;
13
+ constructor(code, message, path) {
14
+ super(message);
15
+ this.code = code;
16
+ this.path = path;
17
+ this.name = "SchemaError";
18
+ }
19
+ }
20
+ var DEFAULT_RESTRICTIONS = {
21
+ maxDepth: 4,
22
+ maxFields: 100,
23
+ maxBytes: 64 * 1024,
24
+ forbidden: ["$ref", "$id", "$dynamicRef", "$dynamicAnchor", "$schema", "definitions", "$defs"]
25
+ };
26
+ function restrictJsonSchema(schema, limits = DEFAULT_RESTRICTIONS) {
27
+ const bytes = JSON.stringify(schema).length;
28
+ if (bytes > limits.maxBytes) {
29
+ throw new SchemaError("SCHEMA_TOO_LARGE", `Schema is ${bytes} bytes; the limit is ${limits.maxBytes}.`);
30
+ }
31
+ let fields = 0;
32
+ const walk = (node, depth, path) => {
33
+ if (node === null || typeof node !== "object")
34
+ return;
35
+ if (Array.isArray(node)) {
36
+ node.forEach((child, index) => walk(child, depth, `${path}[${index}]`));
37
+ return;
38
+ }
39
+ const record = node;
40
+ for (const keyword of limits.forbidden) {
41
+ if (keyword in record) {
42
+ throw new SchemaError("SCHEMA_FORBIDDEN_KEYWORD", `"${keyword}" is not permitted: it can point validation at a document we do not control.`, path || "(root)");
43
+ }
44
+ }
45
+ if (depth > limits.maxDepth) {
46
+ throw new SchemaError("SCHEMA_TOO_DEEP", `Nesting exceeds ${limits.maxDepth} levels.`, path || "(root)");
47
+ }
48
+ if (record.type === "object") {
49
+ const properties = record.properties ?? {};
50
+ const names = Object.keys(properties);
51
+ fields += names.length;
52
+ if (fields > limits.maxFields) {
53
+ throw new SchemaError("SCHEMA_TOO_WIDE", `Schema declares more than ${limits.maxFields} fields.`, path || "(root)");
54
+ }
55
+ if (record.additionalProperties !== false) {
56
+ throw new SchemaError("SCHEMA_OPEN_OBJECT", "Objects must set additionalProperties:false, or unvalidated fields pass through.", path || "(root)");
57
+ }
58
+ for (const name of names) {
59
+ walk(properties[name], depth + 1, path ? `${path}.${name}` : name);
60
+ }
61
+ }
62
+ if (record.type === "array" && record.items) {
63
+ walk(record.items, depth + 1, `${path}[]`);
64
+ }
65
+ };
66
+ walk(schema, 1, "");
67
+ return schema;
68
+ }
69
+ function deriveAnnotation(property) {
70
+ const raw = property["x-fz-derive"];
71
+ if (typeof raw !== "object" || raw === null)
72
+ return;
73
+ const spec = raw;
74
+ const scheme = typeof spec.scheme === "string" ? spec.scheme : undefined;
75
+ if (!scheme)
76
+ return;
77
+ return {
78
+ custody: spec.custody === "supplied" ? "supplied" : "derived",
79
+ scheme,
80
+ expose: typeof spec.expose === "string" ? spec.expose : undefined
81
+ };
82
+ }
83
+ function describeJsonSchema(schema, prefix = "") {
84
+ if (schema.type !== "object")
85
+ return [];
86
+ const properties = schema.properties ?? {};
87
+ const required = new Set(schema.required ?? []);
88
+ return Object.entries(properties).map(([name, property]) => {
89
+ const path = prefix ? `${prefix}.${name}` : name;
90
+ const kind = fieldKind(property);
91
+ const field = {
92
+ path,
93
+ label: property.title ?? humanise(name),
94
+ kind,
95
+ required: required.has(name),
96
+ description: property.description,
97
+ writeOnly: property.writeOnly === true ? true : undefined,
98
+ default: property.default,
99
+ format: property.format,
100
+ minLength: property.minLength,
101
+ maxLength: property.maxLength,
102
+ minimum: property.minimum,
103
+ maximum: property.maximum,
104
+ pattern: property.pattern,
105
+ derive: deriveAnnotation(property)
106
+ };
107
+ if (kind === "enum") {
108
+ return { ...field, options: property.enum ?? property.anyOf };
109
+ }
110
+ if (kind === "object") {
111
+ return { ...field, items: describeJsonSchema(property, path) };
112
+ }
113
+ if (kind === "array" && property.items) {
114
+ return {
115
+ ...field,
116
+ items: describeJsonSchema(property.items, path)
117
+ };
118
+ }
119
+ return field;
120
+ });
121
+ }
122
+ function fieldKind(property) {
123
+ if (Array.isArray(property.enum))
124
+ return "enum";
125
+ if (Array.isArray(property.anyOf) && property.anyOf.every((m) => typeof m === "object" && m !== null && ("const" in m))) {
126
+ return "enum";
127
+ }
128
+ switch (property.type) {
129
+ case "string":
130
+ return "string";
131
+ case "number":
132
+ return "number";
133
+ case "integer":
134
+ return "integer";
135
+ case "boolean":
136
+ return "boolean";
137
+ case "array":
138
+ return "array";
139
+ case "object":
140
+ return "object";
141
+ default:
142
+ return "unknown";
143
+ }
144
+ }
145
+ function humanise(name) {
146
+ const spaced = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim().toLowerCase();
147
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
148
+ }
149
+ function readableFields(fields) {
150
+ return fields.filter((field) => !field.writeOnly).map((field) => field.items ? { ...field, items: readableFields(field.items) } : field);
151
+ }
152
+ function writeOnlyPaths(fields) {
153
+ return fields.flatMap((field) => [
154
+ ...field.writeOnly ? [field.path] : [],
155
+ ...field.items ? writeOnlyPaths(field.items) : []
156
+ ]);
157
+ }
158
+ var SCHEMA_VERSION = 1;
159
+ var VERSION = "0.1.0";
160
+
161
+ // src/schema-typebox.ts
162
+ import { Value } from "@sinclair/typebox/value";
163
+ import { Type } from "@sinclair/typebox";
164
+ var typebox = {
165
+ name: "typebox",
166
+ validate(schema, value) {
167
+ const converted = Value.Convert(schema, value);
168
+ if (Value.Check(schema, converted)) {
169
+ return { ok: true, value: Value.Clean(schema, converted) };
170
+ }
171
+ return {
172
+ ok: false,
173
+ errors: [...Value.Errors(schema, converted)].map((error) => ({
174
+ path: error.path || "(root)",
175
+ message: error.message
176
+ }))
177
+ };
178
+ },
179
+ describeForm(schema) {
180
+ return describeJsonSchema(schema);
181
+ },
182
+ toJsonSchema(schema) {
183
+ return structuredClone(schema);
184
+ },
185
+ restrict(schema, limits) {
186
+ restrictJsonSchema(schema, limits);
187
+ return schema;
188
+ }
189
+ };
190
+ function parse(schema, value) {
191
+ const result = typebox.validate(schema, value);
192
+ if (result.ok)
193
+ return result.value;
194
+ const first = result.errors[0];
195
+ throw new SchemaError("SCHEMA_INVALID", first?.message ?? "Value does not match schema", first?.path);
196
+ }
197
+ export {
198
+ typebox,
199
+ parse,
200
+ Type as T
201
+ };
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @forgezero/runtime/schema — one validation interface, pluggable implementations.
3
+ *
4
+ * The core of `@forgezero/access` accepts JSON Schema and depends on nothing.
5
+ * This package is what turns a real type system into something it can use, and
6
+ * what turns a schema into an admin form without per-field UI code.
7
+ *
8
+ * Three jobs, and the third is a security boundary rather than a convenience:
9
+ *
10
+ * validate does this value match
11
+ * describeForm what fields does an admin see, and which are write-only
12
+ * restrict refuse schemas that are unsafe to accept from a tenant
13
+ *
14
+ * TypeBox ships as `@forgezero/runtime/schema/typebox`. Zod and Valibot are new files,
15
+ * never edits here — that is the whole reason the interface exists.
16
+ */
17
+ export declare class SchemaError extends Error {
18
+ readonly code: string;
19
+ readonly path?: string | undefined;
20
+ constructor(code: string, message: string, path?: string | undefined);
21
+ }
22
+ /**
23
+ * Limits on any schema accepted from outside our own source.
24
+ *
25
+ * These are the security boundary, not style:
26
+ *
27
+ * `$ref`/`$id` would let a tenant point the validator at a remote document
28
+ * depth unbounded nesting is a denial-of-service on validation
29
+ * field count the same, by width instead of depth
30
+ * byte size a schema is stored and re-parsed on every read
31
+ * additionalProperties:false
32
+ * is what stops an unvalidated field riding into an envelope
33
+ *
34
+ * A schema authored in our own code is trusted and skips this. A schema a
35
+ * tenant submits never does.
36
+ */
37
+ export interface Restrictions {
38
+ maxDepth: number;
39
+ maxFields: number;
40
+ maxBytes: number;
41
+ /** Keywords refused outright, wherever they appear. */
42
+ forbidden: readonly string[];
43
+ }
44
+ export declare const DEFAULT_RESTRICTIONS: Restrictions;
45
+ export type FieldKind = 'string' | 'number' | 'integer' | 'boolean' | 'enum' | 'array' | 'object' | 'unknown';
46
+ /**
47
+ * One rendered field. Deliberately flat and framework-agnostic: the admin UI
48
+ * maps this to inputs, and nothing here knows what a component is.
49
+ */
50
+ export interface FormField {
51
+ /** Dotted path, so nested objects address unambiguously. */
52
+ path: string;
53
+ label: string;
54
+ kind: FieldKind;
55
+ required: boolean;
56
+ description?: string;
57
+ /**
58
+ * Never rendered with a value and never serialised back to a browser. This
59
+ * is what routes a credential into the vault instead of onto a page.
60
+ */
61
+ writeOnly?: boolean;
62
+ default?: unknown;
63
+ /** `enum` only. */
64
+ options?: readonly string[];
65
+ format?: string;
66
+ minLength?: number;
67
+ maxLength?: number;
68
+ minimum?: number;
69
+ maximum?: number;
70
+ pattern?: string;
71
+ /** `array` and `object` only — the shape of each child. */
72
+ items?: readonly FormField[];
73
+ /**
74
+ * ForgeZero generates this value; the tenant never types it and never holds
75
+ * it. Carried through from the schema's `x-fz-derive` annotation.
76
+ *
77
+ * A form MUST render this differently from an ordinary field, and the
78
+ * difference is not cosmetic. `derived` means the key material is generated
79
+ * from the master seed and exists in plaintext only inside a signing call —
80
+ * so there is no input to draw, and drawing an empty one invites somebody to
81
+ * paste a key that will be silently ignored. `supplied` means the tenant
82
+ * generates it and the vault only seals it, which IS an input, and a
83
+ * write-only one.
84
+ *
85
+ * Without this the two are indistinguishable in a form, and the failure is
86
+ * quiet in the worst direction: a user believes they have configured a
87
+ * signing key when the platform is using a different one entirely.
88
+ */
89
+ derive?: {
90
+ custody: 'derived' | 'supplied';
91
+ scheme: string;
92
+ expose?: string;
93
+ };
94
+ }
95
+ export type ValidationResult<T = unknown> = {
96
+ ok: true;
97
+ value: T;
98
+ } | {
99
+ ok: false;
100
+ errors: readonly {
101
+ path: string;
102
+ message: string;
103
+ }[];
104
+ };
105
+ /**
106
+ * What a validator must provide. Four functions.
107
+ *
108
+ * `toJsonSchema` is what stops the ecosystem fragmenting: stored config, the
109
+ * admin UI and the wire all speak JSON Schema, so a Zod tenant and a TypeBox
110
+ * tenant produce identical documents and either can read the other's.
111
+ */
112
+ export interface SchemaValidator<S = unknown> {
113
+ readonly name: string;
114
+ validate<T = unknown>(schema: S, value: unknown): ValidationResult<T>;
115
+ describeForm(schema: S): readonly FormField[];
116
+ toJsonSchema(schema: S): Record<string, unknown>;
117
+ /** Throws `SchemaError` on violation. Returns the schema when acceptable. */
118
+ restrict(schema: S, limits?: Restrictions): S;
119
+ }
120
+ /**
121
+ * Restriction checking against JSON Schema, shared by every validator.
122
+ *
123
+ * Implemented once here rather than per-validator: this is the security
124
+ * boundary, and three implementations of it would be three chances to differ.
125
+ * A validator converts to JSON Schema and calls this.
126
+ */
127
+ export declare function restrictJsonSchema(schema: Record<string, unknown>, limits?: Restrictions): Record<string, unknown>;
128
+ export declare function describeJsonSchema(schema: Record<string, unknown>, prefix?: string): readonly FormField[];
129
+ /** Fields safe to return to a browser — everything not marked `writeOnly`. */
130
+ export declare function readableFields(fields: readonly FormField[]): readonly FormField[];
131
+ /** Paths that must never leave the server. Used to strip before serialising. */
132
+ export declare function writeOnlyPaths(fields: readonly FormField[]): readonly string[];
133
+ export declare const SCHEMA_VERSION: 1;
134
+ export declare const VERSION = "0.1.0";