@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/README.md +27 -4
- package/dist/audit.js +262 -23
- package/dist/custody-crypto.d.ts +53 -0
- package/dist/custody-crypto.js +89 -0
- package/dist/custody-share.d.ts +117 -0
- package/dist/custody-share.js +313 -0
- package/dist/jobs.d.ts +3 -2
- package/dist/jobs.js +288 -13
- package/dist/outbox.d.ts +10 -1
- package/dist/outbox.js +8 -3
- package/dist/pipeline.d.ts +11 -37
- package/dist/pipeline.js +0 -27
- package/dist/queue.d.ts +101 -228
- package/dist/queue.js +246 -210
- package/dist/snp.d.ts +7 -6
- package/dist/snp.js +6 -1
- package/dist/ssh-agent.d.ts +13 -0
- package/dist/ssh-agent.js +6 -0
- package/package.json +254 -246
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
return
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
key
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
80
|
+
async function execute(key) {
|
|
81
|
+
running.add(key);
|
|
129
82
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
running
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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/dist/ssh-agent.d.ts
CHANGED
|
@@ -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,
|