pi-crew 0.10.2 → 0.10.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/CHANGELOG.md +249 -0
- package/dist/index.mjs +98 -307
- package/package.json +2 -1
- package/schema.json +11 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +6 -2
- package/skills/real-test-pi-crew/SKILL.md +278 -79
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +40 -1
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +35 -10
- package/src/config/env-vars.ts +27 -2
- package/src/config/types.ts +36 -0
- package/src/extension/registration/lifecycle-handlers.ts +40 -9
- package/src/extension/registration/team-tool.ts +53 -5
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +19 -0
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/status.ts +7 -0
- package/src/extension/team-tool.ts +35 -2
- package/src/hooks/registry.ts +59 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +13 -7
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +261 -41
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +9 -1
- package/src/runtime/child-pi/child-pi.ts +353 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +4 -0
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/model/pi-args.ts +54 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +47 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +101 -0
- package/src/schema/config-schema.ts +24 -0
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/state-store.ts +98 -6
- package/src/state/types.ts +51 -0
- package/src/ui/inline-panel/agent-pane.ts +3 -0
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +32 -8
- package/src/utils/visual.ts +43 -0
- package/src/worktree/worktree-manager.ts +65 -4
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* informational only — the scheduler never derives liveness from them.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { appendFileSync,
|
|
29
|
+
import { appendFileSync, closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
30
30
|
|
|
31
31
|
export const WORKER_EVENT_TYPE_PATTERN = /^worker\.[a-z0-9_.-]{1,63}$/;
|
|
32
32
|
|
|
@@ -46,6 +46,8 @@ export interface WorkerEventsChannelOptions {
|
|
|
46
46
|
export interface WorkerEventsChannel {
|
|
47
47
|
/** Emit a worker.* event. false = dropped (schema/rate/buffer reasons). */
|
|
48
48
|
emit(type: string, data: Record<string, unknown>): boolean;
|
|
49
|
+
/** Emit a terminal worker.* event (bypasses rate-limit; schema/buffer still apply). */
|
|
50
|
+
emitTerminal(type: string, data: Record<string, unknown>): boolean;
|
|
49
51
|
/** Retry queued appends (called on the next emit internally). */
|
|
50
52
|
flush(): void;
|
|
51
53
|
/** Test introspection. */
|
|
@@ -67,8 +69,21 @@ export function createWorkerEventsChannel(options: WorkerEventsChannelOptions =
|
|
|
67
69
|
// which the orchestrator reader skips).
|
|
68
70
|
let prefix = "";
|
|
69
71
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
// PERF (2026-08-24): 1-byte tail read (the comment above always
|
|
73
|
+
// described this; the implementation used to readFileSync the
|
|
74
|
+
// WHOLE file — O(file) reads up to 300x/min per worker for a
|
|
75
|
+
// single byte of information).
|
|
76
|
+
const fd = openSync(eventsPath, "r");
|
|
77
|
+
try {
|
|
78
|
+
const size = fstatSync(fd).size;
|
|
79
|
+
if (size > 0) {
|
|
80
|
+
const tail = Buffer.alloc(1);
|
|
81
|
+
readSync(fd, tail, 0, 1, size - 1);
|
|
82
|
+
if (tail[0] !== 0x0a) prefix = "\n";
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
closeSync(fd);
|
|
86
|
+
}
|
|
72
87
|
} catch {
|
|
73
88
|
/* absent file — nothing to separate */
|
|
74
89
|
}
|
|
@@ -163,6 +178,37 @@ export function createWorkerEventsChannel(options: WorkerEventsChannelOptions =
|
|
|
163
178
|
suppressedSinceLast = 0;
|
|
164
179
|
return true;
|
|
165
180
|
},
|
|
181
|
+
emitTerminal(type: string, data: Record<string, unknown>): boolean {
|
|
182
|
+
if (!eventsPath || !runId) return false; // non-team context — no-op
|
|
183
|
+
if (!WORKER_EVENT_TYPE_PATTERN.test(type)) {
|
|
184
|
+
counters.droppedSchema++;
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
// NO sliding-window rate limit — terminal events bypass.
|
|
188
|
+
// Drain any queued items first (FIFO — oldest first), then the new one.
|
|
189
|
+
if (pending.length > 0) {
|
|
190
|
+
const carry = flushInternal(0);
|
|
191
|
+
if (carry > 0) {
|
|
192
|
+
// Writer failing: queue the new item under the FIFO cap.
|
|
193
|
+
if (pending.length >= bufferCap) {
|
|
194
|
+
pending.shift();
|
|
195
|
+
counters.droppedBuffer++;
|
|
196
|
+
}
|
|
197
|
+
pending.push({ type, data, droppedSinceLast: 0 });
|
|
198
|
+
return true; // accepted (queued)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (write({ type, data, droppedSinceLast: 0 })) {
|
|
202
|
+
counters.accepted++;
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (pending.length >= bufferCap) {
|
|
206
|
+
pending.shift();
|
|
207
|
+
counters.droppedBuffer++;
|
|
208
|
+
}
|
|
209
|
+
pending.push({ type, data, droppedSinceLast: 0 });
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
166
212
|
flush(): void {
|
|
167
213
|
flushInternal(0);
|
|
168
214
|
},
|
|
@@ -226,8 +226,36 @@ export const BACKGROUND_RUNNER_ENV_ALLOWLIST: string[] = [
|
|
|
226
226
|
// Phase 1.5 #3: V8 diagnostic report on fatal error (RFC 17 — investigation).
|
|
227
227
|
"PI_CREW_BG_REPORT_ON_FATAL",
|
|
228
228
|
"PI_TEAMS_BG_REPORT_ON_FATAL",
|
|
229
|
+
// MuxSurface: multiplexer env the detached runner needs to DETECT and
|
|
230
|
+
// REACH the host's tmux/herdr — surface follows env + config for async runs
|
|
231
|
+
// too (2026-08-30 Finding 2: without these, every async run gates to no-mux
|
|
232
|
+
// and stays headless). None of these are secret-named (sanitizeEnvSecrets
|
|
233
|
+
// would reject them from the allowlist otherwise).
|
|
234
|
+
"TMUX",
|
|
235
|
+
"TMUX_PANE",
|
|
236
|
+
"TMUX_TMPDIR",
|
|
237
|
+
"HERDR_ENV",
|
|
238
|
+
"HERDR_SESSION",
|
|
239
|
+
"HERDR_PANE_ID",
|
|
240
|
+
"HERDR_SOCKET_PATH",
|
|
241
|
+
"HERDR_WORKSPACE_ID",
|
|
242
|
+
"HERDR_PING_TIMEOUT_MS",
|
|
229
243
|
];
|
|
230
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Stamp the async telemetry marker onto the background runner's env.
|
|
247
|
+
* (MuxSurface async policy, 2026-08-30): `PI_CREW_ASYNC_RUN` is NOT a surface
|
|
248
|
+
* gate — surface follows env + `runtime.surface.*` config, same as sync runs.
|
|
249
|
+
* The marker stays for telemetry (`SurfaceGateEnvSnapshot.asyncRun`). Pane
|
|
250
|
+
* attach from a detached process is safe: parent-guard (PI_CREW_PARENT_PID)
|
|
251
|
+
* still reaps panes when the host dies.
|
|
252
|
+
*
|
|
253
|
+
* Exported for the unit test asserting the invariant.
|
|
254
|
+
*/
|
|
255
|
+
export function buildBackgroundRunnerEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
256
|
+
return { ...env, PI_CREW_ASYNC_RUN: "1" };
|
|
257
|
+
}
|
|
258
|
+
|
|
231
259
|
export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise<SpawnBackgroundTeamRunResult> {
|
|
232
260
|
// FIX (2026-07-02, perf review F-critical): use packageRoot() instead of
|
|
233
261
|
// import.meta.url-relative path. The previous path.resolve walks
|
|
@@ -257,7 +285,7 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
|
|
|
257
285
|
// `npm root -g` probe. No-op when pi-crew and pi are co-located. See
|
|
258
286
|
// src/runtime/peer-dep.ts.
|
|
259
287
|
const peerDepDir = resolvePeerDepDir();
|
|
260
|
-
const childEnv = peerDepDir ? { ...filteredEnv, [PEER_DEP_DIR_ENV]: peerDepDir } : filteredEnv;
|
|
288
|
+
const childEnv = buildBackgroundRunnerEnv(peerDepDir ? { ...filteredEnv, [PEER_DEP_DIR_ENV]: peerDepDir } : filteredEnv);
|
|
261
289
|
|
|
262
290
|
const loader = resolveTypeScriptLoader();
|
|
263
291
|
if (!loader) {
|
|
@@ -214,13 +214,19 @@ export function startInterruptGuard(
|
|
|
214
214
|
): () => void {
|
|
215
215
|
const controlPath = path.join(manifest.stateRoot, "foreground-control.json");
|
|
216
216
|
// FIX: Made configurable via PI_CREW_INTERRUPT_GUARD_INTERVAL_MS env var.
|
|
217
|
-
//
|
|
218
|
-
|
|
217
|
+
// PERF (2026-08-24): default 1000ms. Each tick does existsSync + readFileSync
|
|
218
|
+
// + JSON.parse for the whole background run, and the guard is best-effort
|
|
219
|
+
// interrupt LATENCY, not a correctness deadline — verified by reading the
|
|
220
|
+
// trigger body below: a late trigger merely delays the SIGTERM (children are
|
|
221
|
+
// SIGKILLed after HARD_KILL_MS=3s regardless) and no state machine aborts on
|
|
222
|
+
// a deadline when the guard fires late. Nothing downstream requires the old
|
|
223
|
+
// 250ms cadence, so trade up to 4×/s of fs+parse churn for 1×/s.
|
|
224
|
+
const interruptGuardInterval = Number(getCrewEnv("PI_CREW_INTERRUPT_GUARD_INTERVAL_MS")) || 1000;
|
|
219
225
|
// RT-4 FIX: Module-local gate so the interrupt body runs only once per
|
|
220
226
|
// interrupt request. Without this, the guard re-fires every
|
|
221
|
-
// interruptGuardInterval
|
|
222
|
-
// terminateActiveChildPiProcesses sweep + sync appendEvent
|
|
223
|
-
//
|
|
227
|
+
// interruptGuardInterval — each tick does a full
|
|
228
|
+
// terminateActiveChildPiProcesses sweep + sync appendEvent. The ack write
|
|
229
|
+
// stops the re-fire; this gate is defense-in-depth if
|
|
224
230
|
// the ack write fails (e.g. transient fs error).
|
|
225
231
|
let interruptHandled = false;
|
|
226
232
|
const interval = setInterval(() => {
|
|
@@ -238,8 +244,8 @@ export function startInterruptGuard(
|
|
|
238
244
|
|
|
239
245
|
// RT-4 FIX: Write acknowledged:true back to foreground-control.json
|
|
240
246
|
// SYNCHRONOUSLY. This stops the guard from re-firing on the next tick
|
|
241
|
-
// (
|
|
242
|
-
// — we cannot await in a polling callback.
|
|
247
|
+
// (interruptGuardInterval). Must be sync because this is a setInterval
|
|
248
|
+
// polling callback — we cannot await in a polling callback.
|
|
243
249
|
try {
|
|
244
250
|
const reqs = parsed.requests ?? [];
|
|
245
251
|
if (reqs.length > 0) {
|
|
@@ -28,8 +28,9 @@ export interface BrokerSpawnCredentials {
|
|
|
28
28
|
* ADR-5 §4 (governed nesting): `childDepth` carries the DEPTH-2+ grandchild's
|
|
29
29
|
* depth when the root-side delegate handler spawns it. The issuer mints only
|
|
30
30
|
* for children that may themselves delegate (childDepth < resolved
|
|
31
|
-
* PI_CREW_MAX_DEPTH) — at the default maxDepth=
|
|
32
|
-
* credentials (env containment AC: no PI_CREW_BROKER_SOCKET/TOKEN at
|
|
31
|
+
* PI_CREW_MAX_DEPTH) — at the default maxDepth=4 a depth-4 grandchild gets NO
|
|
32
|
+
* credentials (env containment AC: no PI_CREW_BROKER_SOCKET/TOKEN at the cap
|
|
33
|
+
* depth).
|
|
33
34
|
* Undefined childDepth = legacy worker spawn (depth 1) — unchanged behavior. */
|
|
34
35
|
export type BrokerIssuer = (runId: string, taskId?: string, childDepth?: number) => Promise<BrokerSpawnCredentials | undefined>;
|
|
35
36
|
|
|
@@ -44,3 +45,27 @@ export function setActiveBrokerIssuer(issuer: BrokerIssuer | undefined): void {
|
|
|
44
45
|
export function getActiveBrokerIssuer(): BrokerIssuer | undefined {
|
|
45
46
|
return activeIssuer;
|
|
46
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* MuxSurface A1 (spec §7 D3 step 2): process-local revoker for per-task tokens.
|
|
51
|
+
* The team-runner's surface-degrade controller calls it when a pane is lost so
|
|
52
|
+
* the zombie worker left in that pane can no longer authenticate to the broker;
|
|
53
|
+
* the headless respawn mints a FRESH token (T10 re-issue).
|
|
54
|
+
*
|
|
55
|
+
* Same registration model as the issuer: the lifecycle controller publishes a
|
|
56
|
+
* closure on start and clears it on stop, so the degrade path needs no wiring
|
|
57
|
+
* through every runner layer. A function reference only — never a token.
|
|
58
|
+
*/
|
|
59
|
+
export type BrokerRevoker = (taskId: string) => void;
|
|
60
|
+
|
|
61
|
+
let activeRevoker: BrokerRevoker | undefined;
|
|
62
|
+
|
|
63
|
+
/** Register the active revoker (called by the lifecycle controller on start). */
|
|
64
|
+
export function setActiveBrokerRevoker(revoker: BrokerRevoker | undefined): void {
|
|
65
|
+
activeRevoker = revoker;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read the active revoker, if any. Returns undefined when no broker is wired. */
|
|
69
|
+
export function getActiveBrokerRevoker(): BrokerRevoker | undefined {
|
|
70
|
+
return activeRevoker;
|
|
71
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* independently testable.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { randomUUID, timingSafeEqual } from "node:crypto";
|
|
13
|
+
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
14
14
|
|
|
15
15
|
/** Length guard: tokens are 128-bit-class (UUID v4). */
|
|
16
16
|
export type BrokerToken = string;
|
|
@@ -37,6 +37,21 @@ export function newBrokerToken(): BrokerToken {
|
|
|
37
37
|
*/
|
|
38
38
|
export class BrokerTokenRegistry {
|
|
39
39
|
private readonly map = new Map<string, BrokerToken>();
|
|
40
|
+
/** Task 10 (mux-surface A1 §5.2): sha256 hashes of explicitly revoked
|
|
41
|
+
* tokens. Heap-only like the map; hashing keeps the registry from holding
|
|
42
|
+
* a second plaintext copy of a secret. Revocation is per-SECRET, not
|
|
43
|
+
* per-key: `issue()` mints a fresh token when the registered one is in
|
|
44
|
+
* this set (fix round 1 BUG #1), so the A2 re-issue remedy always
|
|
45
|
+
* produces a live token while the old secret stays dead. */
|
|
46
|
+
private readonly revokedTokenHashes = new Set<string>();
|
|
47
|
+
|
|
48
|
+
/** Public so the broker can hash the hello secret ONCE at auth time and
|
|
49
|
+
* keep only the digest on the connection (fix round 2 BUG #3: the frame
|
|
50
|
+
* revocation check is secret-based — a connection authenticated with a
|
|
51
|
+
* revoked secret stays dead even after the key is re-issued). */
|
|
52
|
+
static hashToken(token: BrokerToken): string {
|
|
53
|
+
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
54
|
+
}
|
|
40
55
|
|
|
41
56
|
/** Compute the registry key. Compound when taskId is present, bare
|
|
42
57
|
* runId otherwise (backward-compat with the original per-run model). */
|
|
@@ -72,8 +87,12 @@ export class BrokerTokenRegistry {
|
|
|
72
87
|
/** Issue a token for `runId` (+optional `taskId`). Idempotent per key:
|
|
73
88
|
* if a token already exists for the computed key, the existing token is
|
|
74
89
|
* returned unchanged so that concurrent sibling tasks sharing a key all
|
|
75
|
-
* authenticate with the same token
|
|
76
|
-
*
|
|
90
|
+
* authenticate with the same token — UNLESS that token was revoked (fix
|
|
91
|
+
* round 1, BUG #1): a revoked secret is never resurrected, a fresh one
|
|
92
|
+
* is minted and overwrites the entry (the degrade respawn flow re-issues
|
|
93
|
+
* after revokeTaskToken and the new worker must get a live token).
|
|
94
|
+
* Pass an explicit `token` only in tests that need a deterministic
|
|
95
|
+
* value. */
|
|
77
96
|
issue(runId: string, taskId?: string, token?: BrokerToken): BrokerToken {
|
|
78
97
|
if (typeof runId !== "string" || runId.length === 0) {
|
|
79
98
|
throw new Error("BrokerTokenRegistry.issue: runId must be a non-empty string");
|
|
@@ -81,7 +100,9 @@ export class BrokerTokenRegistry {
|
|
|
81
100
|
const k = this.key(runId, taskId);
|
|
82
101
|
if (token === undefined) {
|
|
83
102
|
const existing = this.map.get(k);
|
|
84
|
-
if (existing !== undefined
|
|
103
|
+
if (existing !== undefined && !this.revokedTokenHashes.has(BrokerTokenRegistry.hashToken(existing))) {
|
|
104
|
+
return existing;
|
|
105
|
+
}
|
|
85
106
|
const fresh = newBrokerToken();
|
|
86
107
|
this.map.set(k, fresh);
|
|
87
108
|
return fresh;
|
|
@@ -184,9 +205,40 @@ export class BrokerTokenRegistry {
|
|
|
184
205
|
if (!taskId) this.map.delete(this.orchestratorKey(runId));
|
|
185
206
|
}
|
|
186
207
|
|
|
208
|
+
/** Task 10 (mux-surface A1 §5.2): mark `token` revoked. The next hello
|
|
209
|
+
* presenting it — and every subsequent frame on a connection already
|
|
210
|
+
* authenticated with it — is rejected with code "revoked". No-op on a
|
|
211
|
+
* non-string/empty input. */
|
|
212
|
+
revokeToken(token: BrokerToken): void {
|
|
213
|
+
if (typeof token !== "string" || token.length === 0) return;
|
|
214
|
+
this.revokedTokenHashes.add(BrokerTokenRegistry.hashToken(token));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Task 10: whether the token currently registered for the compound
|
|
218
|
+
* (runId, taskId) key has been revoked. False when no such key exists
|
|
219
|
+
* (undefined ids, orchestrator connections, legacy bare-runId fallbacks)
|
|
220
|
+
* and short-circuits while nothing was ever revoked. This is the HELLO
|
|
221
|
+
* path check — at hello time the candidate must still match the map, so
|
|
222
|
+
* key resolution is equivalent to secret resolution. */
|
|
223
|
+
isTaskTokenRevoked(runId: string | undefined, taskId: string | undefined): boolean {
|
|
224
|
+
if (this.revokedTokenHashes.size === 0 || !runId || !taskId) return false;
|
|
225
|
+
const token = this.map.get(this.key(runId, taskId));
|
|
226
|
+
return token !== undefined && this.revokedTokenHashes.has(BrokerTokenRegistry.hashToken(token));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Fix round 2 (BUG #3): whether a specific SECRET digest has been
|
|
230
|
+
* revoked. The post-hello frame check uses this — it must evaluate the
|
|
231
|
+
* secret the connection ACTUALLY authenticated with, not whatever token
|
|
232
|
+
* the key currently holds (a revoke → re-issue window must not let an
|
|
233
|
+
* old connection ride a freshly issued token). */
|
|
234
|
+
isSecretRevoked(hash: string): boolean {
|
|
235
|
+
return this.revokedTokenHashes.has(hash);
|
|
236
|
+
}
|
|
237
|
+
|
|
187
238
|
/** Wipe every token. Called from CrewBroker.stop(). */
|
|
188
239
|
clear(): void {
|
|
189
240
|
this.map.clear();
|
|
241
|
+
this.revokedTokenHashes.clear();
|
|
190
242
|
}
|
|
191
243
|
|
|
192
244
|
/** Diagnostic — count of registered tokens. Never returns the tokens. */
|