@forgezero/runtime 0.1.1 → 0.1.3

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/dist/queue.js CHANGED
@@ -14,6 +14,15 @@ class QueueStoppedError extends Error {
14
14
  }
15
15
  }
16
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
+
17
26
  class TaskCancelledError extends Error {
18
27
  constructor() {
19
28
  super("queue: task cancelled");
@@ -25,12 +34,19 @@ var DEFAULT_RETRY = {
25
34
  backoffMs: (attempt) => Math.min(30000, 2 ** attempt * 100)
26
35
  };
27
36
  function createQueue(options = {}) {
28
- const width = Math.max(1, options.width ?? 8);
37
+ const width = options.width ?? 8;
29
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
+ }
30
45
  const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
31
46
  const lanes = new Map;
32
47
  const running = new Set;
33
48
  const paused = new Set;
49
+ const stoppedKeys = new Set;
34
50
  let sequence = 0;
35
51
  let globallyPaused = false;
36
52
  let accepting = true;
@@ -48,8 +64,10 @@ function createQueue(options = {}) {
48
64
  idle.shift()();
49
65
  };
50
66
  function pump() {
51
- if (globallyPaused || aborted)
67
+ if (globallyPaused || aborted) {
68
+ announceIdle();
52
69
  return;
70
+ }
53
71
  for (const [key, lane] of lanes) {
54
72
  if (running.size >= width)
55
73
  break;
@@ -62,18 +80,21 @@ function createQueue(options = {}) {
62
80
  async function execute(key) {
63
81
  running.add(key);
64
82
  try {
65
- for (;; ) {
66
- const lane = lanes.get(key);
67
- const entry = lane?.[0];
68
- if (!entry || globallyPaused || paused.has(key) || aborted)
69
- break;
83
+ const lane = lanes.get(key);
84
+ const entry = lane?.[0];
85
+ if (entry && !globallyPaused && !paused.has(key) && !aborted) {
70
86
  lane.shift();
71
87
  await attempt(entry);
72
88
  }
73
89
  } finally {
74
90
  running.delete(key);
75
- if (lanes.get(key)?.length === 0)
91
+ const remaining = lanes.get(key);
92
+ if (remaining?.length === 0)
76
93
  lanes.delete(key);
94
+ else if (remaining) {
95
+ lanes.delete(key);
96
+ lanes.set(key, remaining);
97
+ }
77
98
  pump();
78
99
  }
79
100
  }
@@ -95,15 +116,21 @@ function createQueue(options = {}) {
95
116
  entry.reject(cause);
96
117
  return;
97
118
  }
98
- await sleep(retry.backoffMs(entry.attempt));
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);
99
126
  }
100
127
  }
101
128
  }
102
129
  return {
103
- run(key, handler) {
130
+ run(key, handler, ...args) {
104
131
  const id = `q_${++sequence}`;
105
- if (!accepting) {
106
- const refused = Promise.reject(new QueueStoppedError);
132
+ if (!accepting || stoppedKeys.has(key)) {
133
+ const refused = Promise.reject(accepting ? new QueueKeyStoppedError(key) : new QueueStoppedError);
107
134
  refused.catch(() => {
108
135
  return;
109
136
  });
@@ -118,7 +145,7 @@ function createQueue(options = {}) {
118
145
  const entry = {
119
146
  id,
120
147
  key,
121
- run: async () => handler(),
148
+ run: async () => handler(...args),
122
149
  resolve,
123
150
  reject,
124
151
  attempt: 0,
@@ -142,6 +169,7 @@ function createQueue(options = {}) {
142
169
  entry.reject(new TaskCancelledError);
143
170
  if (lane.length === 0 && !running.has(key))
144
171
  lanes.delete(key);
172
+ announceIdle();
145
173
  return true;
146
174
  }
147
175
  return false;
@@ -153,6 +181,28 @@ function createQueue(options = {}) {
153
181
  paused.delete(key);
154
182
  pump();
155
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
+ },
156
206
  pause() {
157
207
  globallyPaused = true;
158
208
  },
@@ -170,6 +220,7 @@ function createQueue(options = {}) {
170
220
  keys: lanes.size,
171
221
  paused: globallyPaused,
172
222
  pausedKeys: [...paused],
223
+ stoppedKeys: [...stoppedKeys],
173
224
  completed,
174
225
  failed
175
226
  };
@@ -181,15 +232,25 @@ function createQueue(options = {}) {
181
232
  return new Promise((resolve) => idle.push(resolve));
182
233
  },
183
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
+ }
184
238
  accepting = false;
185
239
  const before = { completed, failed };
240
+ globallyPaused = false;
241
+ paused.clear();
242
+ pump();
186
243
  let timedOut = false;
187
- await Promise.race([
188
- this.whenIdle(),
189
- sleep(deadlineMs).then(() => {
244
+ let deadlineHandle;
245
+ const deadline = new Promise((resolve) => {
246
+ deadlineHandle = setTimeout(() => {
190
247
  timedOut = true;
191
- })
192
- ]);
248
+ resolve();
249
+ }, deadlineMs);
250
+ });
251
+ await Promise.race([this.whenIdle(), deadline]);
252
+ if (!timedOut && deadlineHandle !== undefined)
253
+ clearTimeout(deadlineHandle);
193
254
  if (timedOut)
194
255
  aborted = true;
195
256
  let abandoned = 0;
@@ -199,6 +260,11 @@ function createQueue(options = {}) {
199
260
  entry.reject(new QueueStoppedError);
200
261
  }
201
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();
202
268
  return {
203
269
  completed: completed - before.completed,
204
270
  failed: failed - before.failed,
@@ -211,5 +277,6 @@ function createQueue(options = {}) {
211
277
  export {
212
278
  createQueue,
213
279
  TaskCancelledError,
214
- QueueStoppedError
280
+ QueueStoppedError,
281
+ QueueKeyStoppedError
215
282
  };
package/dist/snp.d.ts CHANGED
@@ -17,10 +17,9 @@
17
17
  * ## What this does NOT do
18
18
  *
19
19
  * It does not verify the signature. Chaining a report to AMD's root needs the
20
- * VCEK certificate for that specific chip at that specific TCB, fetched from
21
- * AMD's KDS a network dependency with its own failure modes, and a
22
- * hand-rolled ECDSA-P384 verification here would be worse than none because it
23
- * would produce `verified: true` for a report nobody signed.
20
+ * VCEK certificate for that specific chip at that specific TCB and is performed
21
+ * by the API's pinned KDS verifier. Keeping network trust out of this parser
22
+ * preserves a total byte-to-fields function.
24
23
  *
25
24
  * So `parseSnpReport` returns the signature bytes and says nothing about them.
26
25
  * The caller supplies a verifier, which is the same shape every other trust
@@ -29,8 +28,8 @@
29
28
  /** The structure is exactly this long. Anything else is not a report. */
30
29
  export declare const REPORT_BYTES = 1184;
31
30
  export declare class SnpError extends Error {
32
- readonly code: 'BAD_LENGTH' | 'BAD_VERSION' | 'BAD_VMPL';
33
- constructor(code: 'BAD_LENGTH' | 'BAD_VERSION' | 'BAD_VMPL', message: string);
31
+ readonly code: 'BAD_LENGTH' | 'BAD_VERSION' | 'BAD_VMPL' | 'BAD_SIGNATURE_ALGORITHM';
32
+ constructor(code: 'BAD_LENGTH' | 'BAD_VERSION' | 'BAD_VMPL' | 'BAD_SIGNATURE_ALGORITHM', message: string);
34
33
  }
35
34
  /**
36
35
  * Guest policy bits.
@@ -61,6 +60,8 @@ export interface SnpReport {
61
60
  guestSvn: number;
62
61
  policy: GuestPolicy;
63
62
  vmpl: number;
63
+ /** 1 is ECDSA P-384 with SHA-384. */
64
+ signatureAlgorithm: number;
64
65
  /** 48 bytes of hex. What the guest actually booted. */
65
66
  measurement: string;
66
67
  /** 64 bytes of hex. Whatever the guest asked the PSP to bind in — our nonce. */
package/dist/snp.js CHANGED
@@ -69,19 +69,24 @@ function parseSnpReport(bytes) {
69
69
  }
70
70
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
71
71
  const version = view.getUint32(OFFSET.version, true);
72
- if (version !== 2 && version !== 3) {
72
+ if (version !== 2 && version !== 3 && version !== 5) {
73
73
  throw new SnpError("BAD_VERSION", `Report version ${version} is not one this parser understands.`);
74
74
  }
75
75
  const vmpl = view.getUint32(OFFSET.vmpl, true);
76
76
  if (vmpl > 3) {
77
77
  throw new SnpError("BAD_VMPL", `VMPL ${vmpl} is outside the defined range.`);
78
78
  }
79
+ const signatureAlgorithm = view.getUint32(OFFSET.signatureAlgo, true);
80
+ if (signatureAlgorithm !== 1) {
81
+ throw new SnpError("BAD_SIGNATURE_ALGORITHM", `Signature algorithm ${signatureAlgorithm} is not ECDSA P-384 with SHA-384.`);
82
+ }
79
83
  const slice = (offset, length) => hex(bytes.subarray(offset, offset + length));
80
84
  return {
81
85
  version,
82
86
  guestSvn: view.getUint32(OFFSET.guestSvn, true),
83
87
  policy: readPolicy(view, OFFSET.policy),
84
88
  vmpl,
89
+ signatureAlgorithm,
85
90
  measurement: slice(OFFSET.measurement, 48),
86
91
  reportData: slice(OFFSET.reportData, 64),
87
92
  hostData: slice(OFFSET.hostData, 32),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Publishing happens from an operator's machine, not CI \u2014 CLAUDE.md records that the absence of CI is deliberate. npm's `provenance` attests a tarball was built by a recognised CI provider from a named commit, so it cannot be produced here: it was set, and the first publish failed with `Automatic provenance generation not supported for provider: null`. A setting that can never be satisfied is worse than none, because it reads as a guarantee nobody is getting. Restore it the day this publishes from CI, and not before.",
3
3
  "name": "@forgezero/runtime",
4
- "version": "0.1.1",
4
+ "version": "0.1.3",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -59,10 +59,6 @@
59
59
  "types": "./dist/otpauth.d.ts",
60
60
  "default": "./dist/otpauth.js"
61
61
  },
62
- "./serial": {
63
- "types": "./dist/serial.d.ts",
64
- "default": "./dist/serial.js"
65
- },
66
62
  "./pipeline": {
67
63
  "types": "./dist/pipeline.d.ts",
68
64
  "default": "./dist/pipeline.js"
@@ -182,7 +178,8 @@
182
178
  },
183
179
  "scripts": {
184
180
  "check": "tsc --noEmit",
185
- "build": "bun build src/jobs.ts src/queue.ts src/serial.ts src/outbox.ts src/audit.ts src/backup.ts src/notify.ts src/notify-templates.ts src/calendar.ts src/compliance.ts src/pipeline.ts src/totp.ts src/otpauth.ts src/identity.ts src/slip10.ts src/openssh.ts src/ssh-cert.ts src/importers.ts src/snp.ts src/passkey.ts src/custody-crypto.ts src/custody-share.ts src/phrase.ts src/ssh-agent.ts src/schema.ts src/schema-typebox.ts src/finance/discounts.ts src/finance/money.ts src/finance/storage.ts src/finance/custody.ts src/finance/tax.ts src/finance/derive.ts src/finance/venues.ts src/finance/ledger.ts src/finance/rates.ts src/finance/transfers.ts src/finance/chain.ts src/finance/chain-addresses.ts src/finance/chain-deposits.ts src/finance/chain-withdrawals.ts src/finance/chain-reconcile.ts src/finance/market.ts src/finance/commission.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
181
+ "prebuild": "rm -rf dist",
182
+ "build": "bun build src/jobs.ts src/queue.ts src/outbox.ts src/audit.ts src/backup.ts src/notify.ts src/notify-templates.ts src/calendar.ts src/compliance.ts src/pipeline.ts src/totp.ts src/otpauth.ts src/identity.ts src/slip10.ts src/openssh.ts src/ssh-cert.ts src/importers.ts src/snp.ts src/passkey.ts src/custody-crypto.ts src/custody-share.ts src/phrase.ts src/ssh-agent.ts src/schema.ts src/schema-typebox.ts src/finance/discounts.ts src/finance/money.ts src/finance/storage.ts src/finance/custody.ts src/finance/tax.ts src/finance/derive.ts src/finance/venues.ts src/finance/ledger.ts src/finance/rates.ts src/finance/transfers.ts src/finance/chain.ts src/finance/chain-addresses.ts src/finance/chain-deposits.ts src/finance/chain-withdrawals.ts src/finance/chain-reconcile.ts src/finance/market.ts src/finance/commission.ts --root src --outdir dist --target browser --format esm --packages external && tsc --emitDeclarationOnly --declaration --noEmit false --outDir dist",
186
183
  "prepublishOnly": "bun run check && bun run build"
187
184
  },
188
185
  "dependencies": {
@@ -241,13 +238,13 @@
241
238
  "working-days"
242
239
  ],
243
240
  "license": "MIT",
244
- "homepage": "https://forgezero.net/docs/runtime",
241
+ "homepage": "https://www.forgezero.net/docs/runtime",
245
242
  "repository": {
246
243
  "type": "git",
247
- "url": "git+https://github.com/axxra/forgezero.git",
244
+ "url": "git+https://github.com/forgezero-net/packages.git",
248
245
  "directory": "packages/runtime"
249
246
  },
250
- "bugs": "https://github.com/axxra/forgezero/issues",
247
+ "bugs": "https://github.com/forgezero-net/packages/issues",
251
248
  "sideEffects": false,
252
249
  "files": [
253
250
  "dist",
@@ -1,27 +0,0 @@
1
- import { VenueError, type VenueAdapter, type MarketType, type OrderStatus } from './venues';
2
- export interface BinanceCredentials {
3
- apiKey: string;
4
- apiSecret: string;
5
- }
6
- export interface BinanceOptions {
7
- credentials: BinanceCredentials;
8
- /** Override for testnet, or for a test. */
9
- hosts?: Partial<Record<MarketType, string>>;
10
- fetch?: typeof globalThis.fetch;
11
- /**
12
- * How far a request may be delayed before Binance refuses it.
13
- *
14
- * 5s rather than the 60s maximum. A signed order that arrives a minute late
15
- * is an order placed into a market that has moved, and accepting it is worse
16
- * than being told to retry.
17
- */
18
- recvWindowMs?: number;
19
- now?: () => number;
20
- }
21
- /** Binance spells `BTC/USDT` as `BTCUSDT`. Denormalised here and nowhere else. */
22
- export declare const binanceSymbol: (symbol: string) => string;
23
- /** Binance statuses → ours. An unknown one is `rejected`, never silently `accepted`. */
24
- export declare function toOrderStatus(status: string): OrderStatus;
25
- export declare function createBinanceAdapter(options: BinanceOptions): VenueAdapter;
26
- /** Turn a Binance error into something that names the actual cause. */
27
- export declare function readBinanceError(error: unknown): VenueError | undefined;