@forgezero/runtime 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/queue.js CHANGED
@@ -7,240 +7,276 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
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]];
10
+ class QueueStoppedError extends Error {
11
+ constructor() {
12
+ super("queue: stopped before this task could run");
13
+ this.name = "QueueStoppedError";
14
+ }
19
15
  }
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
- };
16
+
17
+ class QueueKeyStoppedError extends Error {
18
+ key;
19
+ constructor(key) {
20
+ super(`queue: key ${key} is stopped`);
21
+ this.key = key;
22
+ this.name = "QueueKeyStoppedError";
23
+ }
24
+ }
25
+
26
+ class TaskCancelledError extends Error {
27
+ constructor() {
28
+ super("queue: task cancelled");
29
+ this.name = "TaskCancelledError";
30
+ }
80
31
  }
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.");
32
+ var DEFAULT_RETRY = {
33
+ attempts: 1,
34
+ backoffMs: (attempt) => Math.min(30000, 2 ** attempt * 100)
35
+ };
36
+ function createQueue(options = {}) {
37
+ const width = options.width ?? 8;
38
+ const retry = { ...DEFAULT_RETRY, ...options.retry };
39
+ if (!Number.isSafeInteger(width) || width < 1) {
40
+ throw new RangeError("queue: width must be a positive integer");
41
+ }
42
+ if (!Number.isSafeInteger(retry.attempts) || retry.attempts < 1) {
43
+ throw new RangeError("queue: retry attempts must be a positive integer");
98
44
  }
45
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
46
+ const lanes = new Map;
47
+ const running = new Set;
48
+ const paused = new Set;
49
+ const stoppedKeys = new Set;
99
50
  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 };
51
+ let globallyPaused = false;
52
+ let accepting = true;
53
+ let aborted = false;
54
+ let completed = 0;
55
+ let failed = 0;
56
+ const idle = [];
57
+ const announceIdle = () => {
58
+ if (running.size > 0)
59
+ return;
60
+ for (const lane of lanes.values())
61
+ if (lane.length > 0)
62
+ return;
63
+ while (idle.length > 0)
64
+ idle.shift()();
65
+ };
66
+ function pump() {
67
+ if (globallyPaused || aborted) {
68
+ announceIdle();
69
+ return;
112
70
  }
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 };
71
+ for (const [key, lane] of lanes) {
72
+ if (running.size >= width)
73
+ break;
74
+ if (running.has(key) || paused.has(key) || lane.length === 0)
75
+ continue;
76
+ execute(key);
77
+ }
78
+ announceIdle();
127
79
  }
128
- async function drainKey(lease) {
80
+ async function execute(key) {
81
+ running.add(key);
129
82
  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
- }
83
+ const lane = lanes.get(key);
84
+ const entry = lane?.[0];
85
+ if (entry && !globallyPaused && !paused.has(key) && !aborted) {
86
+ lane.shift();
87
+ await attempt(entry);
160
88
  }
161
89
  } finally {
162
- await store.releaseKey(name, lease);
163
- held.delete(lease.key);
90
+ running.delete(key);
91
+ const remaining = lanes.get(key);
92
+ if (remaining?.length === 0)
93
+ lanes.delete(key);
94
+ else if (remaining) {
95
+ lanes.delete(key);
96
+ lanes.set(key, remaining);
97
+ }
98
+ pump();
164
99
  }
165
100
  }
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);
101
+ async function attempt(entry) {
102
+ if (entry.cancelled) {
103
+ entry.reject(new TaskCancelledError);
104
+ return;
176
105
  }
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;
106
+ for (;; ) {
107
+ entry.attempt += 1;
108
+ try {
109
+ const value = await entry.run();
110
+ completed += 1;
111
+ entry.resolve(value);
112
+ return;
113
+ } catch (cause) {
114
+ if (entry.attempt >= retry.attempts || entry.cancelled || aborted) {
115
+ failed += 1;
116
+ entry.reject(cause);
117
+ return;
118
+ }
119
+ const delay = retry.backoffMs(entry.attempt);
120
+ if (!Number.isFinite(delay) || delay < 0) {
121
+ failed += 1;
122
+ entry.reject(new RangeError("queue: retry backoff must be a non-negative finite number"));
123
+ return;
124
+ }
125
+ await sleep(delay);
126
+ }
184
127
  }
185
- return claimed.length;
186
128
  }
187
129
  return {
188
- enqueue,
189
- tick,
190
- async drain(maxPasses = 100) {
191
- for (let pass = 0;pass < maxPasses; pass += 1) {
192
- if (await tick() === 0)
130
+ run(key, handler, ...args) {
131
+ const id = `q_${++sequence}`;
132
+ if (!accepting || stoppedKeys.has(key)) {
133
+ const refused = Promise.reject(accepting ? new QueueKeyStoppedError(key) : new QueueStoppedError);
134
+ refused.catch(() => {
193
135
  return;
136
+ });
137
+ return { id, key, result: refused };
194
138
  }
139
+ let resolve;
140
+ let reject;
141
+ const result = new Promise((ok, no) => {
142
+ resolve = ok;
143
+ reject = no;
144
+ });
145
+ const entry = {
146
+ id,
147
+ key,
148
+ run: async () => handler(...args),
149
+ resolve,
150
+ reject,
151
+ attempt: 0,
152
+ cancelled: false
153
+ };
154
+ const lane = lanes.get(key);
155
+ if (lane)
156
+ lane.push(entry);
157
+ else
158
+ lanes.set(key, [entry]);
159
+ pump();
160
+ return { id, key, result };
195
161
  },
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
- }
162
+ cancel(id) {
163
+ for (const [key, lane] of lanes) {
164
+ const index = lane.findIndex((entry2) => entry2.id === id);
165
+ if (index === -1)
166
+ continue;
167
+ const [entry] = lane.splice(index, 1);
168
+ entry.cancelled = true;
169
+ entry.reject(new TaskCancelledError);
170
+ if (lane.length === 0 && !running.has(key))
171
+ lanes.delete(key);
172
+ announceIdle();
173
+ return true;
174
+ }
175
+ return false;
176
+ },
177
+ pauseKey(key) {
178
+ paused.add(key);
179
+ },
180
+ resumeKey(key) {
181
+ paused.delete(key);
182
+ pump();
183
+ },
184
+ stopKey(key) {
185
+ stoppedKeys.add(key);
186
+ paused.delete(key);
187
+ const lane = lanes.get(key);
188
+ if (!lane)
189
+ return 0;
190
+ let removed = 0;
191
+ for (const entry of lane.splice(0)) {
192
+ removed += 1;
193
+ entry.cancelled = true;
194
+ entry.reject(new QueueKeyStoppedError(key));
195
+ }
196
+ if (!running.has(key))
197
+ lanes.delete(key);
198
+ announceIdle();
199
+ return removed;
200
+ },
201
+ startKey(key) {
202
+ const changed = stoppedKeys.delete(key);
203
+ pump();
204
+ return changed;
205
+ },
206
+ pause() {
207
+ globallyPaused = true;
208
+ },
209
+ resume() {
210
+ globallyPaused = false;
211
+ pump();
212
+ },
213
+ snapshot() {
214
+ let queued = 0;
215
+ for (const lane of lanes.values())
216
+ queued += lane.length;
217
+ return {
218
+ running: running.size,
219
+ queued,
220
+ keys: lanes.size,
221
+ paused: globallyPaused,
222
+ pausedKeys: [...paused],
223
+ stoppedKeys: [...stoppedKeys],
224
+ completed,
225
+ failed
209
226
  };
210
- loop();
211
227
  },
212
- stop() {
213
- running = false;
228
+ whenIdle() {
229
+ if (running.size === 0 && [...lanes.values()].every((lane) => lane.length === 0)) {
230
+ return Promise.resolve();
231
+ }
232
+ return new Promise((resolve) => idle.push(resolve));
214
233
  },
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() });
234
+ async stop(deadlineMs = 30000) {
235
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs < 0) {
236
+ throw new RangeError("queue: stop deadline must be a non-negative integer");
237
+ }
238
+ accepting = false;
239
+ const before = { completed, failed };
240
+ globallyPaused = false;
241
+ paused.clear();
242
+ pump();
243
+ let timedOut = false;
244
+ let deadlineHandle;
245
+ const deadline = new Promise((resolve) => {
246
+ deadlineHandle = setTimeout(() => {
247
+ timedOut = true;
248
+ resolve();
249
+ }, deadlineMs);
250
+ });
251
+ await Promise.race([this.whenIdle(), deadline]);
252
+ if (!timedOut && deadlineHandle !== undefined)
253
+ clearTimeout(deadlineHandle);
254
+ if (timedOut)
255
+ aborted = true;
256
+ let abandoned = 0;
257
+ for (const lane of lanes.values()) {
258
+ abandoned += lane.length;
259
+ for (const entry of lane.splice(0))
260
+ entry.reject(new QueueStoppedError);
261
+ }
262
+ abandoned += running.size;
263
+ for (const [key, lane] of lanes) {
264
+ if (lane.length === 0 && !running.has(key))
265
+ lanes.delete(key);
266
+ }
267
+ announceIdle();
268
+ return {
269
+ completed: completed - before.completed,
270
+ failed: failed - before.failed,
271
+ abandoned,
272
+ timedOut
273
+ };
220
274
  }
221
275
  };
222
276
  }
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
277
  export {
239
- systemClock,
240
- memoryStore,
241
- durationMs,
242
278
  createQueue,
243
- clusterStore,
244
- VERSION,
245
- MODES
279
+ TaskCancelledError,
280
+ QueueStoppedError,
281
+ QueueKeyStoppedError
246
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),
@@ -46,6 +46,19 @@ export declare const AGENT_TIMEOUT_MS = 3000;
46
46
  export declare function listIdentities(socketPath?: string): Promise<AgentIdentity[]>;
47
47
  /** Ed25519 only. See the module note — determinism is the whole mechanism. */
48
48
  export declare function listCustodyIdentities(socketPath?: string): Promise<AgentIdentity[]>;
49
+ /**
50
+ * Sign arbitrary bytes with an identity the agent holds.
51
+ *
52
+ * Exported because a fresh proof from a terminal needs it: the server issues a
53
+ * nonce and this is what turns it into something the server can verify against
54
+ * a registered public key. `deriveCustodyKey` signs a FIXED challenge and hashes
55
+ * the result — that is a key-derivation, not a proof, and using it as one would
56
+ * replay.
57
+ *
58
+ * Returns the raw ed25519 signature, unwrapped from the agent's blob, because
59
+ * that is what a verifier takes.
60
+ */
61
+ export declare function signWithIdentity(identity: AgentIdentity, data: Uint8Array, socketPath?: string): Promise<Uint8Array>;
49
62
  /**
50
63
  * Derive the 32-byte custody key for an identity.
51
64
  *
package/dist/ssh-agent.js CHANGED
@@ -100,6 +100,11 @@ async function listIdentities(socketPath) {
100
100
  async function listCustodyIdentities(socketPath) {
101
101
  return (await listIdentities(socketPath)).filter((id) => id.type === "ssh-ed25519");
102
102
  }
103
+ async function signWithIdentity(identity, data, socketPath) {
104
+ const wrapped = await sign(identity.blob, Buffer.from(data), socketPath);
105
+ const [raw] = readString(wrapped, readString(wrapped, 0)[1]);
106
+ return new Uint8Array(raw);
107
+ }
103
108
  async function sign(blob, data, socketPath) {
104
109
  const payload = Buffer.concat([
105
110
  Buffer.from([SSH_AGENTC_SIGN_REQUEST]),
@@ -132,6 +137,7 @@ async function assertDeterministic(identity, socketPath) {
132
137
  return first;
133
138
  }
134
139
  export {
140
+ signWithIdentity,
135
141
  listIdentities,
136
142
  listCustodyIdentities,
137
143
  deriveCustodyKey,