@sgtbeatdown/pi-tasks 0.1.9 → 0.1.10
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 +48 -100
- package/package.json +2 -2
- package/skills/wolfpack-pi-task-delegation/SKILL.md +6 -4
- package/src/configured-task-core.ts +63 -0
- package/src/extension.ts +112 -35
- package/src/index.ts +6 -7
- package/src/task-core.ts +5 -4
- package/src/task-inbox.ts +18 -4
- package/src/task-protocol.ts +12 -1
- package/src/task-store.ts +177 -426
- package/src/volatile-task-session.ts +283 -0
- package/src/wolfpack-local-auth.ts +20 -0
- package/src/wolfpack-task-relay.ts +35 -358
- package/src/sqlite-database.ts +0 -68
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { createTaskCore } from "./task-core";
|
|
2
|
+
import type { TaskCore } from "./task-core";
|
|
3
|
+
import { TASK_PROTOCOL_VERSION, TaskProtocolError } from "./task-protocol";
|
|
4
|
+
import type { RelayTransportBinding, TaskEndpoint, TaskRelay } from "./task-protocol";
|
|
5
|
+
import type { TaskStore } from "./task-store";
|
|
6
|
+
import { fromWolfpackEnvelope, toWolfpackEnvelope, WOLFPACK_TASK_RELAY_ID } from "./wolfpack-task-relay";
|
|
7
|
+
|
|
8
|
+
export const VOLATILE_PROFILE = "volatile-v1";
|
|
9
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
10
|
+
const CURSOR = /^(0|[1-9][0-9]{0,31})$/;
|
|
11
|
+
const RESET_CODES = new Set(["RELAY_RESET", "REGISTRATION_EXPIRED", "SOURCE_MISMATCH", "CALLER_DEAD", "CALLER_NOT_FOUND"]);
|
|
12
|
+
const RETRY_CODES = new Set(["RELAY_CAPACITY", "RELAY_UNAVAILABLE", "PEER_UNREACHABLE"]);
|
|
13
|
+
|
|
14
|
+
export interface VolatileTaskSessionOptions {
|
|
15
|
+
/** Explicit trusted endpoint ingress URL. No default route or installed server. */
|
|
16
|
+
readonly url: string;
|
|
17
|
+
readonly callerSession: string;
|
|
18
|
+
/** Caller owns this endpoint's RAM state and closes it after session.close(). */
|
|
19
|
+
readonly store: TaskStore;
|
|
20
|
+
readonly fetch?: typeof fetch;
|
|
21
|
+
readonly requestTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface VolatileTaskSession {
|
|
24
|
+
connect(signal?: AbortSignal): Promise<TaskCore>;
|
|
25
|
+
/** Explicitly retire the prior binding; never migrate historical task identities. */
|
|
26
|
+
rebind(signal?: AbortSignal): Promise<TaskCore>;
|
|
27
|
+
status(): { readonly state: "unbound" | "ready" | "reset" | "closed"; readonly binding: RelayTransportBinding | undefined };
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Epoch-bound transport used by the normal extension; caller owns the store. */
|
|
32
|
+
export function createVolatileTaskSession(options: VolatileTaskSessionOptions): VolatileTaskSession {
|
|
33
|
+
const url = new URL(options.url);
|
|
34
|
+
if (url.username || url.password || url.search || url.hash || !(url.protocol === "https:" || (url.protocol === "http:" && ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname)))) throw new TypeError("explicit trusted HTTPS or loopback relay URL required");
|
|
35
|
+
if (!text(options.callerSession)) throw new TypeError("caller session is required");
|
|
36
|
+
const timeoutMs = options.requestTimeoutMs ?? 12_000;
|
|
37
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 15_000) throw new TypeError("invalid relay request deadline");
|
|
38
|
+
const requestFetch = options.fetch ?? fetch, store = options.store;
|
|
39
|
+
let transport: Transport | undefined, core: TaskCore | undefined, busy = false, closed = false;
|
|
40
|
+
let reset = false;
|
|
41
|
+
const saved = () => store.getRelayTransportBinding();
|
|
42
|
+
const validateSaved = (binding: RelayTransportBinding) => {
|
|
43
|
+
if (binding.profile !== VOLATILE_PROFILE || !UUID.test(binding.epoch) || !localEndpoint(binding.endpoint)
|
|
44
|
+
|| !text(binding.generation) || typeof binding.url !== "string" || !text(binding.callerSession) || (binding.reset !== undefined && binding.reset !== true)
|
|
45
|
+
|| !same(store.getEndpointBinding(), binding.endpoint)) throw failure("INVALID_RELAY_METADATA", false);
|
|
46
|
+
};
|
|
47
|
+
const quarantine = (prior: TaskEndpoint | undefined, code: string) => {
|
|
48
|
+
if (!prior) return;
|
|
49
|
+
for (const record of store.outbox("pending")) if (same(record.envelope.source, prior)) store.quarantineOutbox(record.envelope.envelopeId, {
|
|
50
|
+
errorCode: code, reason: "relay binding retired; delivery outcome may be unknown", details: { priorEndpoint: prior, mayHaveBeenDelivered: true }, quarantinedAt: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const onReset = (expected: RelayTransportBinding | undefined) => {
|
|
54
|
+
reset = true;
|
|
55
|
+
const binding = saved();
|
|
56
|
+
// Another controller may already have installed a successor. A stale
|
|
57
|
+
// response must never retire or quarantine that successor's work.
|
|
58
|
+
if (binding && matchesBinding(binding, expected)) store.transaction(() => { store.setRelayTransportBinding({ ...binding, reset: true }); quarantine(binding.endpoint, "RELAY_RESET"); });
|
|
59
|
+
};
|
|
60
|
+
const open = async (replace: boolean, signal?: AbortSignal): Promise<TaskCore> => {
|
|
61
|
+
if (closed) throw failure("RELAY_CLOSED", false);
|
|
62
|
+
if (signal?.aborted) throw failure("ABORTED", true);
|
|
63
|
+
if (busy) throw failure("RELAY_BUSY", true);
|
|
64
|
+
if (!replace && core) { transport!.assertStoredBinding(saved()); await core.connect(signal); return core; }
|
|
65
|
+
busy = true;
|
|
66
|
+
try {
|
|
67
|
+
let prior = saved(), priorEndpoint = store.getEndpointBinding();
|
|
68
|
+
if (prior) validateSaved(prior);
|
|
69
|
+
if (!replace && (reset || prior?.reset || (!prior && priorEndpoint) || (prior && (prior.url !== url.href || prior.callerSession !== options.callerSession)))) { reset = true; throw failure("RELAY_REBIND_REQUIRED", false); }
|
|
70
|
+
if (replace) {
|
|
71
|
+
// Explicit acceptance of loss starts empty. Session history is the only
|
|
72
|
+
// archive; old task identities/ACKs/outbox must never enter the new scope.
|
|
73
|
+
transport?.stop(); core = undefined; reset = true;
|
|
74
|
+
store.clear(); prior = undefined; priorEndpoint = undefined;
|
|
75
|
+
}
|
|
76
|
+
const expectedBinding = saved();
|
|
77
|
+
const generation = replace ? crypto.randomUUID() : prior?.generation ?? store.getEndpointGeneration() ?? crypto.randomUUID();
|
|
78
|
+
store.transaction(() => store.setEndpointGeneration(generation));
|
|
79
|
+
const selected = new Transport(url.href, options.callerSession, generation, replace ? undefined : prior, requestFetch, timeoutMs, onReset);
|
|
80
|
+
transport = selected;
|
|
81
|
+
const binding = await selected.register(signal);
|
|
82
|
+
if (closed) throw failure("RELAY_CLOSED", false);
|
|
83
|
+
store.transaction(() => {
|
|
84
|
+
const current = saved(), currentEndpoint = store.getEndpointBinding();
|
|
85
|
+
const unchanged = expectedBinding ? matchesBinding(current, expectedBinding) && current?.reset === expectedBinding.reset : current === undefined;
|
|
86
|
+
if (!unchanged || store.getEndpointGeneration() !== generation || (priorEndpoint ? !same(currentEndpoint, priorEndpoint) : currentEndpoint !== undefined)) {
|
|
87
|
+
selected.stop(); reset = true; throw failure("RELAY_RESET", false);
|
|
88
|
+
}
|
|
89
|
+
if (replace || !prior) store.setReceiveCursor("0");
|
|
90
|
+
store.setEndpointBinding(binding.endpoint); store.setRelayTransportBinding(binding);
|
|
91
|
+
});
|
|
92
|
+
reset = false;
|
|
93
|
+
// Retained old core handles cannot mutate a successor lifetime even if
|
|
94
|
+
// they resume after await. The RAM store remains controller-owned.
|
|
95
|
+
const guard = () => selected.assertStoredBinding(saved());
|
|
96
|
+
const guarded: TaskStore = { ...store, transaction<T>(operation: () => T): T {
|
|
97
|
+
guard();
|
|
98
|
+
return store.transaction(() => { guard(); const value = operation(); guard(); return value; });
|
|
99
|
+
} };
|
|
100
|
+
const relay = selected.relay();
|
|
101
|
+
const guardedRelay: TaskRelay = { id: relay.id,
|
|
102
|
+
connect: (input, signal) => { guard(); return relay.connect(input, signal); },
|
|
103
|
+
resolve: (input, signal) => { guard(); return relay.resolve(input, signal); },
|
|
104
|
+
send: (input, signal) => { guard(); return relay.send(input, signal); },
|
|
105
|
+
receive: (input, signal) => { guard(); return relay.receive(input, signal); },
|
|
106
|
+
acknowledgeDelivery: (input, signal) => { guard(); return relay.acknowledgeDelivery(input, signal); },
|
|
107
|
+
};
|
|
108
|
+
core = createTaskCore({ endpoint: binding.endpoint, relay: guardedRelay, store: guarded });
|
|
109
|
+
await core.connect(signal);
|
|
110
|
+
return core;
|
|
111
|
+
} finally { busy = false; }
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
connect: signal => open(false, signal), rebind: signal => open(true, signal),
|
|
115
|
+
status: () => ({ state: closed ? "closed" : reset || saved()?.reset ? "reset" : core ? "ready" : "unbound", binding: saved() }),
|
|
116
|
+
close() { closed = true; transport?.stop(); },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class Transport {
|
|
121
|
+
private readonly stopSignal = new AbortController();
|
|
122
|
+
private binding: RelayTransportBinding | undefined;
|
|
123
|
+
private expiresAt = 0;
|
|
124
|
+
private renewing: Promise<RelayTransportBinding> | undefined;
|
|
125
|
+
private active = 0;
|
|
126
|
+
constructor(private url: string, private caller: string, private generation: string, prior: RelayTransportBinding | undefined,
|
|
127
|
+
private fetcher: typeof fetch, private timeoutMs: number, private onReset: (expected: RelayTransportBinding | undefined) => void) { this.binding = prior; }
|
|
128
|
+
stop(): void { this.stopSignal.abort(); }
|
|
129
|
+
assertActive(): void { if (this.stopSignal.signal.aborted) throw failure("RELAY_RESET", false, { mayHaveBeenDelivered: true }); }
|
|
130
|
+
assertStoredBinding(binding: RelayTransportBinding | undefined): void {
|
|
131
|
+
this.assertActive();
|
|
132
|
+
if (!matchesBinding(binding, this.binding) || binding?.reset) this.retire();
|
|
133
|
+
}
|
|
134
|
+
private retire(): never { this.stop(); this.onReset(this.binding); throw failure("RELAY_RESET", false, { mayHaveBeenDelivered: true }); }
|
|
135
|
+
async register(signal?: AbortSignal): Promise<RelayTransportBinding> {
|
|
136
|
+
this.assertActive();
|
|
137
|
+
if (this.binding && this.expiresAt - Date.now() > this.timeoutMs + 10_000) return this.binding;
|
|
138
|
+
if (this.renewing) return this.renewing;
|
|
139
|
+
this.renewing = (async () => {
|
|
140
|
+
const reply = await this.request({ operation: "connect", profile: VOLATILE_PROFILE, callerSession: this.caller, generation: this.generation,
|
|
141
|
+
protocolVersions: [2], leaseMs: 60_000, ...(this.binding && { epoch: this.binding.epoch }) }, signal);
|
|
142
|
+
const value = reply.value;
|
|
143
|
+
if (!record(value) || value.kind !== "connected" || !localEndpoint(value.endpoint) || typeof value.leaseExpiresAt !== "string" || !(Date.parse(value.leaseExpiresAt) > Date.now())) throw failure("INVALID_CONNECTION", true);
|
|
144
|
+
if (this.binding && !same(this.binding.endpoint, value.endpoint)) this.retire();
|
|
145
|
+
this.binding = Object.freeze({ profile: VOLATILE_PROFILE, epoch: reply.epoch, endpoint: Object.freeze({ ...value.endpoint }), generation: this.generation, callerSession: this.caller, url: this.url });
|
|
146
|
+
this.expiresAt = Math.min(Date.parse(value.leaseExpiresAt), Date.now() + 60_000);
|
|
147
|
+
return this.binding;
|
|
148
|
+
})();
|
|
149
|
+
try { return await this.renewing; } finally { this.renewing = undefined; }
|
|
150
|
+
}
|
|
151
|
+
private async operation(operation: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<Record<string, unknown>> {
|
|
152
|
+
const binding = await this.register(signal);
|
|
153
|
+
const result = await this.request({ operation, profile: binding.profile, epoch: binding.epoch, endpoint: binding.endpoint, callerSession: this.caller, ...body }, signal);
|
|
154
|
+
if (!record(result.value)) throw failure("INVALID_RESPONSE", true);
|
|
155
|
+
return result.value;
|
|
156
|
+
}
|
|
157
|
+
relay(): TaskRelay {
|
|
158
|
+
const own = (endpoint: TaskEndpoint) => { this.assertActive(); if (!same(endpoint, this.binding?.endpoint)) throw failure("SOURCE_MISMATCH", false); };
|
|
159
|
+
return {
|
|
160
|
+
id: WOLFPACK_TASK_RELAY_ID,
|
|
161
|
+
connect: async (input, signal) => {
|
|
162
|
+
own(input.endpoint);
|
|
163
|
+
if (input.protocolVersion !== TASK_PROTOCOL_VERSION) throw failure("INCOMPATIBLE_PROTOCOL", false);
|
|
164
|
+
await this.register(signal); return { endpoint: input.endpoint, receiveCursor: input.receiveCursor };
|
|
165
|
+
},
|
|
166
|
+
resolve: async (input, signal) => {
|
|
167
|
+
const value = await this.operation("resolve", { target: { relay: input.relay, id: input.reference } }, signal);
|
|
168
|
+
if (value.kind !== "resolved" || !endpoint(value.endpoint) || value.endpoint.relay !== input.relay || value.endpoint.id !== input.reference) throw failure("INVALID_TARGET", true);
|
|
169
|
+
return value.endpoint;
|
|
170
|
+
},
|
|
171
|
+
send: async (input, signal) => {
|
|
172
|
+
own(input.source);
|
|
173
|
+
const envelope = toWolfpackEnvelope(input); // Preserve original persisted wire content.
|
|
174
|
+
const value = await this.operation("send", { envelope }, signal);
|
|
175
|
+
if (value.kind !== "accepted" || value.envelopeId !== input.envelopeId || !uuid(value.acceptanceId) || typeof value.duplicate !== "boolean" || value.forwarding !== (envelope.target.relay === WOLFPACK_TASK_RELAY_ID ? "local" : "forwarded")) throw failure("INVALID_ACCEPTANCE", true);
|
|
176
|
+
return { envelopeId: input.envelopeId };
|
|
177
|
+
},
|
|
178
|
+
receive: async (input, signal) => {
|
|
179
|
+
own(input.endpoint);
|
|
180
|
+
if (!cursor(input.cursor) || !Number.isInteger(input.limit) || input.limit < 1) throw failure("INVALID_CURSOR", false);
|
|
181
|
+
const limit = Math.min(input.limit, 50);
|
|
182
|
+
const value = await this.operation("receive", { cursor: input.cursor, limit }, signal);
|
|
183
|
+
if (value.kind !== "page" || !Array.isArray(value.deliveries) || value.deliveries.length > limit || !cursor(value.nextCursor) || typeof value.hasMore !== "boolean") throw failure("INVALID_INBOX", true);
|
|
184
|
+
let last = BigInt(input.cursor);
|
|
185
|
+
const ids = new Set<string>();
|
|
186
|
+
const deliveries = value.deliveries.map((item: unknown) => {
|
|
187
|
+
if (!record(item) || !cursor(item.cursor) || BigInt(item.cursor) <= last || !record(item.envelope)) throw failure("INVALID_INBOX", true);
|
|
188
|
+
if (!endpoint(item.envelope.source) || !localEndpoint(item.envelope.target)) throw failure("INVALID_INBOX", true);
|
|
189
|
+
const envelope = fromWolfpackEnvelope(item.envelope as unknown as Parameters<typeof fromWolfpackEnvelope>[0]);
|
|
190
|
+
if (!same(envelope.target, input.endpoint) || ids.has(envelope.envelopeId)) throw failure("INVALID_INBOX", true);
|
|
191
|
+
ids.add(envelope.envelopeId); last = BigInt(item.cursor);
|
|
192
|
+
return { cursor: item.cursor, envelope };
|
|
193
|
+
});
|
|
194
|
+
if (value.nextCursor !== (deliveries.at(-1)?.cursor ?? input.cursor) || (!deliveries.length && value.hasMore)) throw failure("INVALID_INBOX", true);
|
|
195
|
+
return { deliveries, nextCursor: value.nextCursor, hasMore: value.hasMore };
|
|
196
|
+
},
|
|
197
|
+
acknowledgeDelivery: async (input, signal) => {
|
|
198
|
+
own(input.endpoint);
|
|
199
|
+
if (!cursor(input.cursor) || !text(input.envelopeId)) throw failure("INVALID_CURSOR", false);
|
|
200
|
+
const value = await this.operation("acknowledge", { envelopeId: input.envelopeId }, signal);
|
|
201
|
+
if (value.kind !== "acknowledged" || typeof value.duplicate !== "boolean") throw failure("INVALID_RESPONSE", true);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
private async request(body: Record<string, unknown>, signal?: AbortSignal): Promise<{ epoch: string; value: unknown }> {
|
|
206
|
+
this.assertActive();
|
|
207
|
+
const reserved = body.operation === "acknowledge" || body.operation === "connect";
|
|
208
|
+
if (this.active >= (reserved ? 10 : 8)) throw failure("RELAY_CAPACITY", true);
|
|
209
|
+
const encoded = JSON.stringify(body);
|
|
210
|
+
if (Buffer.byteLength(encoded) > 64 * 1024) throw failure("PAYLOAD_TOO_LARGE", false);
|
|
211
|
+
this.active++;
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
let timedOut = false;
|
|
214
|
+
const abort = () => controller.abort();
|
|
215
|
+
signal?.addEventListener("abort", abort, { once: true }); this.stopSignal.signal.addEventListener("abort", abort, { once: true });
|
|
216
|
+
if (signal?.aborted || this.stopSignal.signal.aborted) abort();
|
|
217
|
+
const timer = setTimeout(() => { timedOut = true; abort(); }, this.timeoutMs);
|
|
218
|
+
const abortError = () => failure(this.stopSignal.signal.aborted ? "RELAY_RESET" : timedOut ? "RELAY_TIMEOUT" : "ABORTED", !this.stopSignal.signal.aborted, { mayHaveBeenDelivered: true });
|
|
219
|
+
let rejectAbort!: () => void;
|
|
220
|
+
const cancelled = new Promise<never>((_, reject) => { rejectAbort = () => reject(abortError()); controller.signal.addEventListener("abort", rejectAbort, { once: true }); if (controller.signal.aborted) rejectAbort(); });
|
|
221
|
+
try {
|
|
222
|
+
const result = await Promise.race([cancelled, (async () => {
|
|
223
|
+
if (controller.signal.aborted) throw abortError();
|
|
224
|
+
const response = await this.fetcher(this.url, { method: "POST", headers: { "content-type": "application/json" }, body: encoded, redirect: "error", signal: controller.signal });
|
|
225
|
+
if (controller.signal.aborted) { void response.body?.cancel().catch(() => undefined); throw abortError(); }
|
|
226
|
+
if (response.status === 401) { void response.body?.cancel().catch(() => undefined); throw failure("RELAY_AUTH_REQUIRED", false); }
|
|
227
|
+
const reader = response.body?.getReader();
|
|
228
|
+
if (!reader) throw failure("INVALID_RESPONSE", true);
|
|
229
|
+
const cancel = () => { void reader.cancel().catch(() => undefined); };
|
|
230
|
+
controller.signal.addEventListener("abort", cancel, { once: true });
|
|
231
|
+
const chunks: Uint8Array[] = []; let bytes = 0;
|
|
232
|
+
try {
|
|
233
|
+
while (true) {
|
|
234
|
+
const part = await reader.read();
|
|
235
|
+
if (controller.signal.aborted) throw abortError();
|
|
236
|
+
if (part.done) break;
|
|
237
|
+
bytes += part.value.byteLength;
|
|
238
|
+
if (bytes > 300 * 1024) throw failure("INVALID_RESPONSE", true);
|
|
239
|
+
chunks.push(part.value);
|
|
240
|
+
}
|
|
241
|
+
return { ok: response.ok, value: JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown };
|
|
242
|
+
} finally { controller.signal.removeEventListener("abort", cancel); cancel(); }
|
|
243
|
+
})()]);
|
|
244
|
+
this.assertActive();
|
|
245
|
+
const data = result.value;
|
|
246
|
+
// An explicit permanent profile refusal retires an existing lifetime even
|
|
247
|
+
// when the selected server engine has no volatile epoch to return.
|
|
248
|
+
if (record(data) && (data.profile === VOLATILE_PROFILE || data.profile === "durable-v2")
|
|
249
|
+
&& data.ok === false && record(data.error) && data.error.code === "RELAY_PROFILE_REQUIRED" && data.error.retryable === false) {
|
|
250
|
+
if (this.binding) this.retire();
|
|
251
|
+
throw failure("RELAY_PROFILE_REQUIRED", false);
|
|
252
|
+
}
|
|
253
|
+
if (!record(data) || data.profile !== VOLATILE_PROFILE) throw failure("RELAY_PROFILE_REQUIRED", false);
|
|
254
|
+
if (this.binding && uuid(data.epoch) && data.epoch !== this.binding.epoch) this.retire();
|
|
255
|
+
if (data.ok === false && record(data.error) && text(data.error.code) && typeof data.error.retryable === "boolean") {
|
|
256
|
+
if (RESET_CODES.has(data.error.code)) this.retire();
|
|
257
|
+
const epochlessRejection = data.epoch === undefined && ["RELAY_CAPACITY", "RELAY_UNAVAILABLE", "INVALID_REQUEST", "PEER_POLICY_REQUIRED"].includes(data.error.code);
|
|
258
|
+
if ((!uuid(data.epoch) && !epochlessRejection) || (this.binding && uuid(data.epoch) && data.epoch !== this.binding.epoch)) throw failure("INVALID_RESPONSE", true);
|
|
259
|
+
const details = { ...((data.error.mayHaveBeenDelivered === true || data.error.code === "RELAY_UNAVAILABLE") && { mayHaveBeenDelivered: true }),
|
|
260
|
+
...(typeof data.error.retryAfterMs === "number" && Number.isFinite(data.error.retryAfterMs) && data.error.retryAfterMs >= 0 && { retryAfterMs: data.error.retryAfterMs }) };
|
|
261
|
+
throw failure(data.error.code, data.error.retryable || RETRY_CODES.has(data.error.code), details);
|
|
262
|
+
}
|
|
263
|
+
if (!result.ok || data.ok !== true || !uuid(data.epoch)) throw failure("INVALID_RESPONSE", true);
|
|
264
|
+
return { epoch: data.epoch, value: data.value };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error instanceof TaskProtocolError) throw error;
|
|
267
|
+
throw failure("RELAY_UNAVAILABLE", true, { mayHaveBeenDelivered: true });
|
|
268
|
+
} finally {
|
|
269
|
+
clearTimeout(timer); this.active--; signal?.removeEventListener("abort", abort); this.stopSignal.signal.removeEventListener("abort", abort); controller.signal.removeEventListener("abort", rejectAbort);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function failure(code: string, retryable: boolean, details?: Record<string, unknown>): TaskProtocolError { return new TaskProtocolError(code, code, { retryable, details }); }
|
|
274
|
+
function record(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
275
|
+
function text(value: unknown): value is string { return typeof value === "string" && value.length > 0 && value.length <= 512; }
|
|
276
|
+
function uuid(value: unknown): value is string { return typeof value === "string" && UUID.test(value); }
|
|
277
|
+
function endpoint(value: unknown): value is TaskEndpoint { return record(value) && text(value.relay) && uuid(value.id); }
|
|
278
|
+
function localEndpoint(value: unknown): value is TaskEndpoint { return endpoint(value) && value.relay === WOLFPACK_TASK_RELAY_ID; }
|
|
279
|
+
function same(a: TaskEndpoint | undefined, b: TaskEndpoint | undefined): boolean { return !!a && !!b && a.relay === b.relay && a.id === b.id; }
|
|
280
|
+
function matchesBinding(a: RelayTransportBinding | undefined, b: RelayTransportBinding | undefined): boolean {
|
|
281
|
+
return !!a && !!b && a.profile === b.profile && a.epoch === b.epoch && a.generation === b.generation && a.callerSession === b.callerSession && a.url === b.url && same(a.endpoint, b.endpoint);
|
|
282
|
+
}
|
|
283
|
+
function cursor(value: unknown): value is string { return typeof value === "string" && CURSOR.test(value); }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { TaskProtocolError } from "./task-protocol";
|
|
3
|
+
|
|
4
|
+
/** Existing owner-provided JWT configuration, only for the local control server.
|
|
5
|
+
* Never copy this credential to an arbitrary programmatic HTTPS relay URL. */
|
|
6
|
+
export function wolfpackLocalHeaders(url: string, input?: RequestInit["headers"], env: Readonly<Record<string, string | undefined>> = process.env): Headers {
|
|
7
|
+
const headers = new Headers(input), target = new URL(url);
|
|
8
|
+
if (headers.has("authorization") || target.protocol !== "http:" || !["127.0.0.1", "[::1]", "localhost"].includes(target.hostname)) return headers;
|
|
9
|
+
const secret = env.WOLFPACK_JWT_SECRET?.trim();
|
|
10
|
+
if (!secret) return headers;
|
|
11
|
+
if (secret.length < 32) throw new TaskProtocolError("RELAY_AUTH_REQUIRED", "configured Wolfpack JWT secret must contain at least 32 characters", { retryable: false });
|
|
12
|
+
const now = Math.floor(Date.now() / 1000);
|
|
13
|
+
const payload = { iat: now, exp: now + 60, jti: randomBytes(8).toString("hex"),
|
|
14
|
+
...(env.WOLFPACK_JWT_ISSUER?.trim() && { iss: env.WOLFPACK_JWT_ISSUER.trim() }),
|
|
15
|
+
...(env.WOLFPACK_JWT_AUDIENCE?.trim() && { aud: env.WOLFPACK_JWT_AUDIENCE.trim() }) };
|
|
16
|
+
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
17
|
+
const data = `${encode({ alg: "HS256", typ: "JWT" })}.${encode(payload)}`;
|
|
18
|
+
headers.set("authorization", `Bearer ${data}.${createHmac("sha256", secret).update(data).digest("base64url")}`);
|
|
19
|
+
return headers;
|
|
20
|
+
}
|