@forgezero/runtime 0.1.0 → 0.1.1
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/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/outbox.d.ts +10 -1
- package/dist/outbox.js +8 -3
- package/dist/queue.d.ts +88 -229
- package/dist/queue.js +180 -211
- package/dist/ssh-agent.d.ts +13 -0
- package/dist/ssh-agent.js +6 -0
- package/package.json +258 -246
package/dist/queue.js
CHANGED
|
@@ -7,240 +7,209 @@ 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]];
|
|
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
|
-
};
|
|
10
|
+
class QueueStoppedError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("queue: stopped before this task could run");
|
|
13
|
+
this.name = "QueueStoppedError";
|
|
14
|
+
}
|
|
80
15
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.");
|
|
16
|
+
|
|
17
|
+
class TaskCancelledError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("queue: task cancelled");
|
|
20
|
+
this.name = "TaskCancelledError";
|
|
98
21
|
}
|
|
22
|
+
}
|
|
23
|
+
var DEFAULT_RETRY = {
|
|
24
|
+
attempts: 1,
|
|
25
|
+
backoffMs: (attempt) => Math.min(30000, 2 ** attempt * 100)
|
|
26
|
+
};
|
|
27
|
+
function createQueue(options = {}) {
|
|
28
|
+
const width = Math.max(1, options.width ?? 8);
|
|
29
|
+
const retry = { ...DEFAULT_RETRY, ...options.retry };
|
|
30
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
31
|
+
const lanes = new Map;
|
|
32
|
+
const running = new Set;
|
|
33
|
+
const paused = new Set;
|
|
99
34
|
let sequence = 0;
|
|
100
|
-
let
|
|
101
|
-
let
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
return
|
|
35
|
+
let globallyPaused = false;
|
|
36
|
+
let accepting = true;
|
|
37
|
+
let aborted = false;
|
|
38
|
+
let completed = 0;
|
|
39
|
+
let failed = 0;
|
|
40
|
+
const idle = [];
|
|
41
|
+
const announceIdle = () => {
|
|
42
|
+
if (running.size > 0)
|
|
43
|
+
return;
|
|
44
|
+
for (const lane of lanes.values())
|
|
45
|
+
if (lane.length > 0)
|
|
46
|
+
return;
|
|
47
|
+
while (idle.length > 0)
|
|
48
|
+
idle.shift()();
|
|
49
|
+
};
|
|
50
|
+
function pump() {
|
|
51
|
+
if (globallyPaused || aborted)
|
|
52
|
+
return;
|
|
53
|
+
for (const [key, lane] of lanes) {
|
|
54
|
+
if (running.size >= width)
|
|
55
|
+
break;
|
|
56
|
+
if (running.has(key) || paused.has(key) || lane.length === 0)
|
|
57
|
+
continue;
|
|
58
|
+
execute(key);
|
|
112
59
|
}
|
|
113
|
-
|
|
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 };
|
|
60
|
+
announceIdle();
|
|
127
61
|
}
|
|
128
|
-
async function
|
|
62
|
+
async function execute(key) {
|
|
63
|
+
running.add(key);
|
|
129
64
|
try {
|
|
130
|
-
for (
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
}
|
|
65
|
+
for (;; ) {
|
|
66
|
+
const lane = lanes.get(key);
|
|
67
|
+
const entry = lane?.[0];
|
|
68
|
+
if (!entry || globallyPaused || paused.has(key) || aborted)
|
|
69
|
+
break;
|
|
70
|
+
lane.shift();
|
|
71
|
+
await attempt(entry);
|
|
160
72
|
}
|
|
161
73
|
} finally {
|
|
162
|
-
|
|
163
|
-
|
|
74
|
+
running.delete(key);
|
|
75
|
+
if (lanes.get(key)?.length === 0)
|
|
76
|
+
lanes.delete(key);
|
|
77
|
+
pump();
|
|
164
78
|
}
|
|
165
79
|
}
|
|
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);
|
|
80
|
+
async function attempt(entry) {
|
|
81
|
+
if (entry.cancelled) {
|
|
82
|
+
entry.reject(new TaskCancelledError);
|
|
83
|
+
return;
|
|
176
84
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
85
|
+
for (;; ) {
|
|
86
|
+
entry.attempt += 1;
|
|
87
|
+
try {
|
|
88
|
+
const value = await entry.run();
|
|
89
|
+
completed += 1;
|
|
90
|
+
entry.resolve(value);
|
|
91
|
+
return;
|
|
92
|
+
} catch (cause) {
|
|
93
|
+
if (entry.attempt >= retry.attempts || entry.cancelled || aborted) {
|
|
94
|
+
failed += 1;
|
|
95
|
+
entry.reject(cause);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await sleep(retry.backoffMs(entry.attempt));
|
|
99
|
+
}
|
|
184
100
|
}
|
|
185
|
-
return claimed.length;
|
|
186
101
|
}
|
|
187
102
|
return {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
103
|
+
run(key, handler) {
|
|
104
|
+
const id = `q_${++sequence}`;
|
|
105
|
+
if (!accepting) {
|
|
106
|
+
const refused = Promise.reject(new QueueStoppedError);
|
|
107
|
+
refused.catch(() => {
|
|
193
108
|
return;
|
|
109
|
+
});
|
|
110
|
+
return { id, key, result: refused };
|
|
194
111
|
}
|
|
112
|
+
let resolve;
|
|
113
|
+
let reject;
|
|
114
|
+
const result = new Promise((ok, no) => {
|
|
115
|
+
resolve = ok;
|
|
116
|
+
reject = no;
|
|
117
|
+
});
|
|
118
|
+
const entry = {
|
|
119
|
+
id,
|
|
120
|
+
key,
|
|
121
|
+
run: async () => handler(),
|
|
122
|
+
resolve,
|
|
123
|
+
reject,
|
|
124
|
+
attempt: 0,
|
|
125
|
+
cancelled: false
|
|
126
|
+
};
|
|
127
|
+
const lane = lanes.get(key);
|
|
128
|
+
if (lane)
|
|
129
|
+
lane.push(entry);
|
|
130
|
+
else
|
|
131
|
+
lanes.set(key, [entry]);
|
|
132
|
+
pump();
|
|
133
|
+
return { id, key, result };
|
|
195
134
|
},
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
135
|
+
cancel(id) {
|
|
136
|
+
for (const [key, lane] of lanes) {
|
|
137
|
+
const index = lane.findIndex((entry2) => entry2.id === id);
|
|
138
|
+
if (index === -1)
|
|
139
|
+
continue;
|
|
140
|
+
const [entry] = lane.splice(index, 1);
|
|
141
|
+
entry.cancelled = true;
|
|
142
|
+
entry.reject(new TaskCancelledError);
|
|
143
|
+
if (lane.length === 0 && !running.has(key))
|
|
144
|
+
lanes.delete(key);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
},
|
|
149
|
+
pauseKey(key) {
|
|
150
|
+
paused.add(key);
|
|
151
|
+
},
|
|
152
|
+
resumeKey(key) {
|
|
153
|
+
paused.delete(key);
|
|
154
|
+
pump();
|
|
155
|
+
},
|
|
156
|
+
pause() {
|
|
157
|
+
globallyPaused = true;
|
|
158
|
+
},
|
|
159
|
+
resume() {
|
|
160
|
+
globallyPaused = false;
|
|
161
|
+
pump();
|
|
162
|
+
},
|
|
163
|
+
snapshot() {
|
|
164
|
+
let queued = 0;
|
|
165
|
+
for (const lane of lanes.values())
|
|
166
|
+
queued += lane.length;
|
|
167
|
+
return {
|
|
168
|
+
running: running.size,
|
|
169
|
+
queued,
|
|
170
|
+
keys: lanes.size,
|
|
171
|
+
paused: globallyPaused,
|
|
172
|
+
pausedKeys: [...paused],
|
|
173
|
+
completed,
|
|
174
|
+
failed
|
|
209
175
|
};
|
|
210
|
-
loop();
|
|
211
176
|
},
|
|
212
|
-
|
|
213
|
-
running
|
|
177
|
+
whenIdle() {
|
|
178
|
+
if (running.size === 0 && [...lanes.values()].every((lane) => lane.length === 0)) {
|
|
179
|
+
return Promise.resolve();
|
|
180
|
+
}
|
|
181
|
+
return new Promise((resolve) => idle.push(resolve));
|
|
214
182
|
},
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
await
|
|
183
|
+
async stop(deadlineMs = 30000) {
|
|
184
|
+
accepting = false;
|
|
185
|
+
const before = { completed, failed };
|
|
186
|
+
let timedOut = false;
|
|
187
|
+
await Promise.race([
|
|
188
|
+
this.whenIdle(),
|
|
189
|
+
sleep(deadlineMs).then(() => {
|
|
190
|
+
timedOut = true;
|
|
191
|
+
})
|
|
192
|
+
]);
|
|
193
|
+
if (timedOut)
|
|
194
|
+
aborted = true;
|
|
195
|
+
let abandoned = 0;
|
|
196
|
+
for (const lane of lanes.values()) {
|
|
197
|
+
abandoned += lane.length;
|
|
198
|
+
for (const entry of lane.splice(0))
|
|
199
|
+
entry.reject(new QueueStoppedError);
|
|
200
|
+
}
|
|
201
|
+
abandoned += running.size;
|
|
202
|
+
return {
|
|
203
|
+
completed: completed - before.completed,
|
|
204
|
+
failed: failed - before.failed,
|
|
205
|
+
abandoned,
|
|
206
|
+
timedOut
|
|
207
|
+
};
|
|
220
208
|
}
|
|
221
209
|
};
|
|
222
210
|
}
|
|
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
211
|
export {
|
|
239
|
-
systemClock,
|
|
240
|
-
memoryStore,
|
|
241
|
-
durationMs,
|
|
242
212
|
createQueue,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
MODES
|
|
213
|
+
TaskCancelledError,
|
|
214
|
+
QueueStoppedError
|
|
246
215
|
};
|
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,
|