@decibelsystems/tools 3.0.0 → 3.1.0
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 +66 -0
- package/dist/agentCommands.d.ts +0 -1
- package/dist/agentCommands.d.ts.map +1 -1
- package/dist/agentCommands.js +19 -8
- package/dist/agentCommands.js.map +1 -1
- package/dist/agentPresence.d.ts +5 -3
- package/dist/agentPresence.d.ts.map +1 -1
- package/dist/agentPresence.js +20 -29
- package/dist/agentPresence.js.map +1 -1
- package/dist/architectAdrs.d.ts +13 -1
- package/dist/architectAdrs.d.ts.map +1 -1
- package/dist/architectAdrs.js +83 -1
- package/dist/architectAdrs.js.map +1 -1
- package/dist/artifacts.d.ts +60 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +208 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/asks.d.ts +93 -0
- package/dist/asks.d.ts.map +1 -0
- package/dist/asks.js +383 -0
- package/dist/asks.js.map +1 -0
- package/dist/chat.d.ts +47 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +288 -0
- package/dist/chat.js.map +1 -0
- package/dist/daemon.d.ts +14 -3
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +44 -14
- package/dist/daemon.js.map +1 -1
- package/dist/daemonConfig.d.ts +22 -0
- package/dist/daemonConfig.d.ts.map +1 -1
- package/dist/daemonConfig.js +11 -0
- package/dist/daemonConfig.js.map +1 -1
- package/dist/domain/issue.d.ts +19 -15
- package/dist/domain/issue.d.ts.map +1 -1
- package/dist/domain/issue.js +4 -32
- package/dist/domain/issue.js.map +1 -1
- package/dist/domain/issueCodec.d.ts.map +1 -1
- package/dist/domain/issueCodec.js +10 -0
- package/dist/domain/issueCodec.js.map +1 -1
- package/dist/domain/issueRepository.d.ts +35 -2
- package/dist/domain/issueRepository.d.ts.map +1 -1
- package/dist/domain/issueRepository.js +40 -1
- package/dist/domain/issueRepository.js.map +1 -1
- package/dist/domain/uid.d.ts +16 -0
- package/dist/domain/uid.d.ts.map +1 -0
- package/dist/domain/uid.js +43 -0
- package/dist/domain/uid.js.map +1 -0
- package/dist/facades/definitions.d.ts.map +1 -1
- package/dist/facades/definitions.js +46 -3
- package/dist/facades/definitions.js.map +1 -1
- package/dist/hq/defineAgent.d.ts.map +1 -1
- package/dist/hq/defineAgent.js +7 -11
- package/dist/hq/defineAgent.js.map +1 -1
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +201 -48
- package/dist/httpServer.js.map +1 -1
- package/dist/identity.d.ts +123 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +312 -0
- package/dist/identity.js.map +1 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +24 -1
- package/dist/kernel.js.map +1 -1
- package/dist/killSwitch.d.ts +117 -0
- package/dist/killSwitch.d.ts.map +1 -0
- package/dist/killSwitch.js +161 -0
- package/dist/killSwitch.js.map +1 -0
- package/dist/lib/atomicWrite.d.ts +23 -0
- package/dist/lib/atomicWrite.d.ts.map +1 -1
- package/dist/lib/atomicWrite.js +84 -0
- package/dist/lib/atomicWrite.js.map +1 -1
- package/dist/lib/recordIdAllocator.d.ts +21 -5
- package/dist/lib/recordIdAllocator.d.ts.map +1 -1
- package/dist/lib/recordIdAllocator.js +25 -7
- package/dist/lib/recordIdAllocator.js.map +1 -1
- package/dist/orgId.d.ts +23 -0
- package/dist/orgId.d.ts.map +1 -0
- package/dist/orgId.js +73 -0
- package/dist/orgId.js.map +1 -0
- package/dist/pairCli.d.ts +2 -0
- package/dist/pairCli.d.ts.map +1 -0
- package/dist/pairCli.js +97 -0
- package/dist/pairCli.js.map +1 -0
- package/dist/peerBroker.d.ts +40 -0
- package/dist/peerBroker.d.ts.map +1 -0
- package/dist/peerBroker.js +70 -0
- package/dist/peerBroker.js.map +1 -0
- package/dist/peerBrokerSupervisor.d.ts +21 -0
- package/dist/peerBrokerSupervisor.d.ts.map +1 -0
- package/dist/peerBrokerSupervisor.js +131 -0
- package/dist/peerBrokerSupervisor.js.map +1 -0
- package/dist/projectPaths.d.ts.map +1 -1
- package/dist/projectPaths.js +1 -19
- package/dist/projectPaths.js.map +1 -1
- package/dist/projectRegistry.d.ts +22 -0
- package/dist/projectRegistry.d.ts.map +1 -1
- package/dist/projectRegistry.js +58 -2
- package/dist/projectRegistry.js.map +1 -1
- package/dist/pty/boundary.d.ts +39 -0
- package/dist/pty/boundary.d.ts.map +1 -0
- package/dist/pty/boundary.js +100 -0
- package/dist/pty/boundary.js.map +1 -0
- package/dist/pty/errors.d.ts +6 -0
- package/dist/pty/errors.d.ts.map +1 -0
- package/dist/pty/errors.js +12 -0
- package/dist/pty/errors.js.map +1 -0
- package/dist/pty/fleet.d.ts +159 -0
- package/dist/pty/fleet.d.ts.map +1 -0
- package/dist/pty/fleet.js +543 -0
- package/dist/pty/fleet.js.map +1 -0
- package/dist/pty/launchProfile.d.ts +45 -0
- package/dist/pty/launchProfile.d.ts.map +1 -0
- package/dist/pty/launchProfile.js +149 -0
- package/dist/pty/launchProfile.js.map +1 -0
- package/dist/pty/openTerminal.d.ts +48 -0
- package/dist/pty/openTerminal.d.ts.map +1 -0
- package/dist/pty/openTerminal.js +185 -0
- package/dist/pty/openTerminal.js.map +1 -0
- package/dist/pty/pairDialog.d.ts +16 -0
- package/dist/pty/pairDialog.d.ts.map +1 -0
- package/dist/pty/pairDialog.js +121 -0
- package/dist/pty/pairDialog.js.map +1 -0
- package/dist/pty/pairing.d.ts +86 -0
- package/dist/pty/pairing.d.ts.map +1 -0
- package/dist/pty/pairing.js +351 -0
- package/dist/pty/pairing.js.map +1 -0
- package/dist/pty/routes.d.ts +49 -0
- package/dist/pty/routes.d.ts.map +1 -0
- package/dist/pty/routes.js +624 -0
- package/dist/pty/routes.js.map +1 -0
- package/dist/pty/sessions.d.ts +167 -0
- package/dist/pty/sessions.d.ts.map +1 -0
- package/dist/pty/sessions.js +513 -0
- package/dist/pty/sessions.js.map +1 -0
- package/dist/pty/tmux.d.ts +109 -0
- package/dist/pty/tmux.d.ts.map +1 -0
- package/dist/pty/tmux.js +271 -0
- package/dist/pty/tmux.js.map +1 -0
- package/dist/roster.d.ts +37 -0
- package/dist/roster.d.ts.map +1 -0
- package/dist/roster.js +126 -0
- package/dist/roster.js.map +1 -0
- package/dist/runtime/daemonMeta.d.ts +53 -0
- package/dist/runtime/daemonMeta.d.ts.map +1 -0
- package/dist/runtime/daemonMeta.js +121 -0
- package/dist/runtime/daemonMeta.js.map +1 -0
- package/dist/runtime/ensureRuntime.d.ts.map +1 -1
- package/dist/runtime/ensureRuntime.js +5 -10
- package/dist/runtime/ensureRuntime.js.map +1 -1
- package/dist/sentinelIssues.d.ts +16 -0
- package/dist/sentinelIssues.d.ts.map +1 -1
- package/dist/sentinelIssues.js +42 -1
- package/dist/sentinelIssues.js.map +1 -1
- package/dist/server.js +66 -5
- package/dist/server.js.map +1 -1
- package/dist/toolConfig.d.ts.map +1 -1
- package/dist/toolConfig.js +12 -0
- package/dist/toolConfig.js.map +1 -1
- package/dist/tools/agentic/agentQueue.d.ts.map +1 -1
- package/dist/tools/agentic/agentQueue.js +4 -0
- package/dist/tools/agentic/agentQueue.js.map +1 -1
- package/dist/tools/architect/index.d.ts +1 -0
- package/dist/tools/architect/index.d.ts.map +1 -1
- package/dist/tools/architect/index.js +45 -1
- package/dist/tools/architect/index.js.map +1 -1
- package/dist/tools/architect.d.ts.map +1 -1
- package/dist/tools/architect.js +59 -8
- package/dist/tools/architect.js.map +1 -1
- package/dist/tools/artifacts.d.ts +5 -0
- package/dist/tools/artifacts.d.ts.map +1 -0
- package/dist/tools/artifacts.js +89 -0
- package/dist/tools/artifacts.js.map +1 -0
- package/dist/tools/asks.d.ts +6 -0
- package/dist/tools/asks.d.ts.map +1 -0
- package/dist/tools/asks.js +104 -0
- package/dist/tools/asks.js.map +1 -0
- package/dist/tools/coordinator/index.d.ts +1 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +42 -2
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/data-inspector.d.ts +1 -1
- package/dist/tools/data-inspector.d.ts.map +1 -1
- package/dist/tools/data-inspector.js +1 -1
- package/dist/tools/data-inspector.js.map +1 -1
- package/dist/tools/dojo/index.d.ts.map +1 -1
- package/dist/tools/dojo/index.js +52 -3
- package/dist/tools/dojo/index.js.map +1 -1
- package/dist/tools/dojo.d.ts +62 -1
- package/dist/tools/dojo.d.ts.map +1 -1
- package/dist/tools/dojo.js +84 -1
- package/dist/tools/dojo.js.map +1 -1
- package/dist/tools/guardian.d.ts +7 -0
- package/dist/tools/guardian.d.ts.map +1 -1
- package/dist/tools/guardian.js +146 -9
- package/dist/tools/guardian.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/killswitch/index.d.ts +13 -0
- package/dist/tools/killswitch/index.d.ts.map +1 -0
- package/dist/tools/killswitch/index.js +138 -0
- package/dist/tools/killswitch/index.js.map +1 -0
- package/dist/tools/oracle/index.d.ts.map +1 -1
- package/dist/tools/oracle/index.js +8 -0
- package/dist/tools/oracle/index.js.map +1 -1
- package/dist/tools/oracle.d.ts +7 -0
- package/dist/tools/oracle.d.ts.map +1 -1
- package/dist/tools/oracle.js +22 -6
- package/dist/tools/oracle.js.map +1 -1
- package/dist/tools/provenance.d.ts +11 -2
- package/dist/tools/provenance.d.ts.map +1 -1
- package/dist/tools/provenance.js +26 -24
- package/dist/tools/provenance.js.map +1 -1
- package/dist/tools/registry/index.d.ts.map +1 -1
- package/dist/tools/registry/index.js +24 -0
- package/dist/tools/registry/index.js.map +1 -1
- package/dist/tools/sentinel/index.d.ts.map +1 -1
- package/dist/tools/sentinel/index.js +54 -2
- package/dist/tools/sentinel/index.js.map +1 -1
- package/dist/tools/sentinel.d.ts +50 -0
- package/dist/tools/sentinel.d.ts.map +1 -1
- package/dist/tools/sentinel.js +72 -7
- package/dist/tools/sentinel.js.map +1 -1
- package/dist/tools/shared/runTracker.d.ts +2 -0
- package/dist/tools/shared/runTracker.d.ts.map +1 -1
- package/dist/tools/shared/runTracker.js +38 -0
- package/dist/tools/shared/runTracker.js.map +1 -1
- package/dist/tools/swarm.d.ts +3 -1
- package/dist/tools/swarm.d.ts.map +1 -1
- package/dist/tools/swarm.js +6 -12
- package/dist/tools/swarm.js.map +1 -1
- package/dist/tools/voice/index.d.ts +1 -0
- package/dist/tools/voice/index.d.ts.map +1 -1
- package/dist/tools/voice/index.js +33 -2
- package/dist/tools/voice/index.js.map +1 -1
- package/dist/tools/voice.d.ts +48 -1
- package/dist/tools/voice.d.ts.map +1 -1
- package/dist/tools/voice.js +63 -3
- package/dist/tools/voice.js.map +1 -1
- package/dist/transports/bridge.d.ts.map +1 -1
- package/dist/transports/bridge.js +2 -1
- package/dist/transports/bridge.js.map +1 -1
- package/dist/transports/mcp.d.ts.map +1 -1
- package/dist/transports/mcp.js +2 -1
- package/dist/transports/mcp.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +13 -2
- package/templates/AGENT.md +3 -0
- package/templates/CLAUDE-decibel.md +11 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kill switch — one durable STOP for unattended work (ISS-0178).
|
|
3
|
+
*
|
|
4
|
+
* WHAT IT STOPS, AND WHAT IT DELIBERATELY DOES NOT. The metaphor is a machine
|
|
5
|
+
* tool's big red button: it halts the automation, it does not power down the
|
|
6
|
+
* panel. Unattended work — the agentic queue replaying remote writes, the dojo
|
|
7
|
+
* experiment runner spawning a script — refuses while stopped. Interactive
|
|
8
|
+
* calls keep working on purpose, because the first thing anyone does after
|
|
9
|
+
* hitting stop is ask what went wrong, and that takes sentinel, oracle and this
|
|
10
|
+
* module itself.
|
|
11
|
+
*
|
|
12
|
+
* THE SAFETY INVARIANT. You can never stop yourself out of resuming. That holds
|
|
13
|
+
* BY CONSTRUCTION rather than by an exception list: nothing in this file gates
|
|
14
|
+
* itself, and enforcement lives at the call sites of the work being stopped.
|
|
15
|
+
* There is no path where `resume` consults the flag it is trying to clear.
|
|
16
|
+
*
|
|
17
|
+
* WHY A FILE, AND WHY THIS FILE. State lives at ~/.decibel/killswitch.json:
|
|
18
|
+
*
|
|
19
|
+
* - machine-global, because a button on a desk is machine-global. Stopping
|
|
20
|
+
* "the project you happen to be in" is not what a red button means.
|
|
21
|
+
* - a file, so it survives a daemon restart, a crash, and a reboot. A stop
|
|
22
|
+
* that forgets itself is worse than no stop, because it teaches people the
|
|
23
|
+
* switch is unreliable.
|
|
24
|
+
* - plain JSON with no schema tricks, so a process that is not this one — a
|
|
25
|
+
* shell script, a microcontroller, a future physical button — can read and
|
|
26
|
+
* write it without linking anything.
|
|
27
|
+
*
|
|
28
|
+
* WHAT IT IS NOT. It is not a security control. Anything that can write to
|
|
29
|
+
* ~/.decibel can clear it. It is an operational brake for work you started, in
|
|
30
|
+
* the same spirit as the daemon's own PID file.
|
|
31
|
+
*/
|
|
32
|
+
import fs from 'fs';
|
|
33
|
+
import path from 'path';
|
|
34
|
+
import os from 'os';
|
|
35
|
+
/** Machine-global, deliberately — see the header. */
|
|
36
|
+
export const KILL_SWITCH_PATH = path.join(os.homedir(), '.decibel', 'killswitch.json');
|
|
37
|
+
const RUNNING = { stopped: false };
|
|
38
|
+
/**
|
|
39
|
+
* Read the current state, treating every failure as RUNNING.
|
|
40
|
+
*
|
|
41
|
+
* FAIL-OPEN IS THE RIGHT CHOICE HERE and it is worth saying why, because the
|
|
42
|
+
* opposite is defensible and wrong for this case. A corrupt or unreadable
|
|
43
|
+
* killswitch file would, under fail-closed, halt all unattended work on the
|
|
44
|
+
* machine with no way to diagnose it except editing the very file that is
|
|
45
|
+
* broken — a brake that jams on is worse than one that does not engage. The
|
|
46
|
+
* stop is an operational convenience, not a safety interlock, and it says so in
|
|
47
|
+
* the header.
|
|
48
|
+
*
|
|
49
|
+
* Note this is the one place the module is deliberately quiet: a missing file
|
|
50
|
+
* is the overwhelmingly common case (nobody has ever pressed stop) and must not
|
|
51
|
+
* be noise.
|
|
52
|
+
*/
|
|
53
|
+
export function readKillSwitch() {
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(KILL_SWITCH_PATH, 'utf-8');
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
// Anything other than an explicit `true` is running. A file containing
|
|
58
|
+
// `{"stopped":"yes"}` from a well-meaning script must not read as stopped,
|
|
59
|
+
// because a truthy-string check would make the brake depend on spelling.
|
|
60
|
+
return parsed.stopped === true ? parsed : RUNNING;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return RUNNING;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** True when unattended work should refuse. */
|
|
67
|
+
export function isStopped() {
|
|
68
|
+
return readKillSwitch().stopped;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Engage the stop. Idempotent: a second press reports the FIRST stop rather
|
|
72
|
+
* than overwriting it, because a button will be pressed twice and the original
|
|
73
|
+
* reason and time are the ones worth keeping.
|
|
74
|
+
*
|
|
75
|
+
* @returns the state now in force, and whether this call is what engaged it
|
|
76
|
+
*/
|
|
77
|
+
export function engageStop(opts = {}) {
|
|
78
|
+
const current = readKillSwitch();
|
|
79
|
+
if (current.stopped) {
|
|
80
|
+
return { state: current, already: true };
|
|
81
|
+
}
|
|
82
|
+
const state = {
|
|
83
|
+
stopped: true,
|
|
84
|
+
stopped_at: new Date().toISOString(),
|
|
85
|
+
...(opts.reason ? { reason: opts.reason } : {}),
|
|
86
|
+
// An unattributed stop is allowed — a physical button cannot type — but it
|
|
87
|
+
// is recorded as unattributed rather than silently attributed to whoever
|
|
88
|
+
// happened to be running.
|
|
89
|
+
actor: opts.actor || 'unattributed',
|
|
90
|
+
...(opts.source ? { source: opts.source } : {}),
|
|
91
|
+
};
|
|
92
|
+
writeState(state);
|
|
93
|
+
return { state, already: false };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Release the stop. Idempotent, and never gated — see the safety invariant.
|
|
97
|
+
*
|
|
98
|
+
* @returns the state that was cleared, when there was one
|
|
99
|
+
*/
|
|
100
|
+
export function releaseStop() {
|
|
101
|
+
const current = readKillSwitch();
|
|
102
|
+
if (!current.stopped)
|
|
103
|
+
return { released: null };
|
|
104
|
+
writeState(RUNNING);
|
|
105
|
+
return { released: current };
|
|
106
|
+
}
|
|
107
|
+
function writeState(state) {
|
|
108
|
+
const dir = path.dirname(KILL_SWITCH_PATH);
|
|
109
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
110
|
+
// Write-then-rename: a physical button may be pressed while something else is
|
|
111
|
+
// reading, and a half-written file must never be observable as either state.
|
|
112
|
+
const tmp = `${KILL_SWITCH_PATH}.${process.pid}.tmp`;
|
|
113
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
114
|
+
fs.renameSync(tmp, KILL_SWITCH_PATH);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Thrown at the gate. Carries the stop's own reason and time so the refusal can
|
|
118
|
+
* say WHY work stopped rather than merely that it did — the difference between
|
|
119
|
+
* a legible brake and a mystery.
|
|
120
|
+
*/
|
|
121
|
+
export class KillSwitchEngagedError extends Error {
|
|
122
|
+
code = 'KILL_SWITCH_ENGAGED';
|
|
123
|
+
state;
|
|
124
|
+
constructor(operation, state) {
|
|
125
|
+
const when = state.stopped_at ? ` at ${state.stopped_at}` : '';
|
|
126
|
+
const who = state.actor && state.actor !== 'unattributed' ? ` by ${state.actor}` : '';
|
|
127
|
+
const why = state.reason ? ` — ${state.reason}` : '';
|
|
128
|
+
super(`Kill switch engaged${when}${who}${why}. Refusing: ${operation}. ` +
|
|
129
|
+
`Release it with killswitch.resume (never blocked), or delete ${KILL_SWITCH_PATH}.`);
|
|
130
|
+
this.name = 'KillSwitchEngagedError';
|
|
131
|
+
this.state = state;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The gate. Call at the top of anything that runs without someone watching.
|
|
136
|
+
*
|
|
137
|
+
* Takes the operation name so the refusal names what it refused — "agentic
|
|
138
|
+
* queue replay" is actionable, "operation failed" is not.
|
|
139
|
+
*/
|
|
140
|
+
export function assertRunnable(operation) {
|
|
141
|
+
const state = readKillSwitch();
|
|
142
|
+
if (state.stopped)
|
|
143
|
+
throw new KillSwitchEngagedError(operation, state);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Non-throwing form of {@link assertRunnable}, for call sites whose contract is
|
|
147
|
+
* to RETURN an error rather than throw one.
|
|
148
|
+
*
|
|
149
|
+
* Both forms exist deliberately. `assertRunnable` suits functions that already
|
|
150
|
+
* throw (agentQueueSync throws for unconfigured Supabase, so a throw is in
|
|
151
|
+
* keeping); this suits functions that declare `Promise<Output | SomeError>`,
|
|
152
|
+
* where throwing would be a lie about the signature and would behave
|
|
153
|
+
* differently when called directly than when called through a handler that
|
|
154
|
+
* catches. That divergence between the direct and dispatched path is a bug
|
|
155
|
+
* shape this repo has been bitten by more than once.
|
|
156
|
+
*/
|
|
157
|
+
export function checkRunnable(operation) {
|
|
158
|
+
const state = readKillSwitch();
|
|
159
|
+
return state.stopped ? new KillSwitchEngagedError(operation, state) : null;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=killSwitch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"killSwitch.js","sourceRoot":"","sources":["../src/killSwitch.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,4 +23,27 @@
|
|
|
23
23
|
* record does not quietly widen or narrow its mode.
|
|
24
24
|
*/
|
|
25
25
|
export declare function writeFileAtomic(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Create `filePath` with `content`, atomically AND exclusively (ISS-0195).
|
|
28
|
+
*
|
|
29
|
+
* `writeFile(…, { flag: 'wx' })` is exclusive but not atomic: O_EXCL creates
|
|
30
|
+
* the file empty and the bytes follow, so a SIGKILL in between leaves a 0-byte
|
|
31
|
+
* record under a real id. Temp-and-rename is atomic but not exclusive: rename
|
|
32
|
+
* silently replaces an existing file. `link(2)` is both. It publishes a fully
|
|
33
|
+
* written, fsynced temp under the final name in one step, and fails with
|
|
34
|
+
* EEXIST when the name is taken, exactly like O_EXCL. A kill can only leave
|
|
35
|
+
* the dot-temp behind, which no record scan reads; see sweepAllocationTemps.
|
|
36
|
+
*
|
|
37
|
+
* Where hard links are unavailable (some FAT, exFAT and network mounts), this
|
|
38
|
+
* falls back to the O_EXCL write: still exclusive, and no worse than before.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createFileExclusive(filePath: string, content: string, encoding?: BufferEncoding): Promise<void>;
|
|
41
|
+
/** Suffix of createFileExclusive's temp files. */
|
|
42
|
+
export declare const ALLOC_TEMP_SUFFIX = ".alloc.tmp";
|
|
43
|
+
/**
|
|
44
|
+
* Remove createFileExclusive temps in `dir` older than `maxAgeMs`: the debris
|
|
45
|
+
* a killed writer leaves behind. The age floor keeps it from touching a temp
|
|
46
|
+
* a live writer is still filling. Never throws; this is housekeeping.
|
|
47
|
+
*/
|
|
48
|
+
export declare function sweepAllocationTemps(dir: string, maxAgeMs?: number): Promise<number>;
|
|
26
49
|
//# sourceMappingURL=atomicWrite.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atomicWrite.d.ts","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,cAAwB,GACjC,OAAO,CAAC,IAAI,CAAC,CA2Cf"}
|
|
1
|
+
{"version":3,"file":"atomicWrite.d.ts","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,cAAwB,GACjC,OAAO,CAAC,IAAI,CAAC,CA2Cf;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,cAAwB,GACjC,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,kDAAkD;AAClD,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAE9C;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAoB/F"}
|
package/dist/lib/atomicWrite.js
CHANGED
|
@@ -70,4 +70,88 @@ export async function writeFileAtomic(filePath, content, encoding = 'utf-8') {
|
|
|
70
70
|
throw err;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
/** Errors meaning "this filesystem cannot hard-link", not "the target exists". */
|
|
74
|
+
const LINK_UNSUPPORTED = new Set(['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV', 'ENOSYS']);
|
|
75
|
+
/**
|
|
76
|
+
* Create `filePath` with `content`, atomically AND exclusively (ISS-0195).
|
|
77
|
+
*
|
|
78
|
+
* `writeFile(…, { flag: 'wx' })` is exclusive but not atomic: O_EXCL creates
|
|
79
|
+
* the file empty and the bytes follow, so a SIGKILL in between leaves a 0-byte
|
|
80
|
+
* record under a real id. Temp-and-rename is atomic but not exclusive: rename
|
|
81
|
+
* silently replaces an existing file. `link(2)` is both. It publishes a fully
|
|
82
|
+
* written, fsynced temp under the final name in one step, and fails with
|
|
83
|
+
* EEXIST when the name is taken, exactly like O_EXCL. A kill can only leave
|
|
84
|
+
* the dot-temp behind, which no record scan reads; see sweepAllocationTemps.
|
|
85
|
+
*
|
|
86
|
+
* Where hard links are unavailable (some FAT, exFAT and network mounts), this
|
|
87
|
+
* falls back to the O_EXCL write: still exclusive, and no worse than before.
|
|
88
|
+
*/
|
|
89
|
+
export async function createFileExclusive(filePath, content, encoding = 'utf-8') {
|
|
90
|
+
const dir = path.dirname(filePath);
|
|
91
|
+
const tmpPath = path.join(dir, `.${path.basename(filePath)}.${randomBytes(6).toString('hex')}${ALLOC_TEMP_SUFFIX}`);
|
|
92
|
+
let handle;
|
|
93
|
+
try {
|
|
94
|
+
handle = await fs.open(tmpPath, 'wx');
|
|
95
|
+
await handle.writeFile(content, encoding);
|
|
96
|
+
await handle.sync();
|
|
97
|
+
await handle.close();
|
|
98
|
+
handle = undefined;
|
|
99
|
+
try {
|
|
100
|
+
await fs.link(tmpPath, filePath);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
const code = err.code ?? '';
|
|
104
|
+
if (!LINK_UNSUPPORTED.has(code))
|
|
105
|
+
throw err; // EEXIST included: the caller retries.
|
|
106
|
+
await fs.writeFile(filePath, content, { encoding, flag: 'wx' });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (handle) {
|
|
111
|
+
try {
|
|
112
|
+
await handle.close();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Already closed or never opened cleanly.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
await fs.unlink(tmpPath);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Nothing to clean up.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Suffix of createFileExclusive's temp files. */
|
|
127
|
+
export const ALLOC_TEMP_SUFFIX = '.alloc.tmp';
|
|
128
|
+
/**
|
|
129
|
+
* Remove createFileExclusive temps in `dir` older than `maxAgeMs`: the debris
|
|
130
|
+
* a killed writer leaves behind. The age floor keeps it from touching a temp
|
|
131
|
+
* a live writer is still filling. Never throws; this is housekeeping.
|
|
132
|
+
*/
|
|
133
|
+
export async function sweepAllocationTemps(dir, maxAgeMs = 10 * 60_000) {
|
|
134
|
+
let removed = 0;
|
|
135
|
+
try {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
for (const name of await fs.readdir(dir)) {
|
|
138
|
+
if (!name.startsWith('.') || !name.endsWith(ALLOC_TEMP_SUFFIX))
|
|
139
|
+
continue;
|
|
140
|
+
const full = path.join(dir, name);
|
|
141
|
+
try {
|
|
142
|
+
if (now - (await fs.stat(full)).mtimeMs > maxAgeMs) {
|
|
143
|
+
await fs.unlink(full);
|
|
144
|
+
removed++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Raced with another sweeper, or already gone.
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Directory unreadable or absent: nothing to sweep.
|
|
154
|
+
}
|
|
155
|
+
return removed;
|
|
156
|
+
}
|
|
73
157
|
//# sourceMappingURL=atomicWrite.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atomicWrite.js","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,QAAgB,EAChB,OAAe,EACf,WAA2B,OAAO;IAElC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAEpG,qEAAqE;IACrE,IAAI,IAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;IAED,IAAI,MAAoD,CAAC;IACzD,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC1C,0EAA0E;QAC1E,yEAAyE;QACzE,yEAAyE;QACzE,YAAY;QACZ,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,GAAG,SAAS,CAAC;QAEnB,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;YAAC,MAAM,CAAC;gBACP,0CAA0C;YAC5C,CAAC;QACH,CAAC;QACD,2EAA2E;QAC3E,sEAAsE;QACtE,uDAAuD;QACvD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,uBAAuB;QACzB,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"atomicWrite.js","sourceRoot":"","sources":["../../src/lib/atomicWrite.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,9 +4,22 @@
|
|
|
4
4
|
* The defect this closes: `create_issue` picked the next id by scanning the
|
|
5
5
|
* issues directory for the current maximum, then wrote the file many awaits
|
|
6
6
|
* later. Two processes could read the same maximum and both write ISS-0141.
|
|
7
|
-
* Four duplicate-id groups
|
|
8
|
-
* ISS-0054, ISS-0112), reported independently by the
|
|
9
|
-
* reproduced locally.
|
|
7
|
+
* Four duplicate-id groups were produced by exactly this and were found on disk
|
|
8
|
+
* (ISS-0015, ISS-0028, ISS-0054, ISS-0112), reported independently by the
|
|
9
|
+
* `machina` peer and reproduced locally. THIS repo's four were cleared: as of
|
|
10
|
+
* 2026-09-19 it holds 200 issues and 38 epics with ZERO duplicate frontmatter
|
|
11
|
+
* ids. ISS-0136 remains OPEN, because it is portfolio-wide and 61 groups still
|
|
12
|
+
* sit in five other projects (frontend_v0.2, senken-trading-agent, machina,
|
|
13
|
+
* decibel-studio, decibel-tools-mobile). Do not read "clean" here as "clean
|
|
14
|
+
* everywhere". The history is kept because it is the evidence
|
|
15
|
+
* this lock exists, not because the state persists — stated in the past tense
|
|
16
|
+
* deliberately, since the present-tense version outlived the fact and was read
|
|
17
|
+
* as current by an outside reader, who repeated it in a document.
|
|
18
|
+
*
|
|
19
|
+
* Verify by parsing frontmatter, not by grepping. `grep "^id: ISS-"` over this
|
|
20
|
+
* store counts documents that QUOTE an id — a report about malformed
|
|
21
|
+
* frontmatter contains the line it is reporting — and that false positive has
|
|
22
|
+
* already cost one confidently wrong correction.
|
|
10
23
|
*
|
|
11
24
|
* Two independent defences, because a lock only protects callers that take it:
|
|
12
25
|
*
|
|
@@ -14,9 +27,12 @@
|
|
|
14
27
|
* calculation. Serializing only the scan leaves the same race — both
|
|
15
28
|
* processes still compute 141 before either file lands.
|
|
16
29
|
*
|
|
17
|
-
* 2. The write itself is
|
|
18
|
-
* old client, a migration script, a future mistake — a colliding write
|
|
30
|
+
* 2. The write itself is exclusive. If a caller bypasses the lock entirely —
|
|
31
|
+
* an old client, a migration script, a future mistake — a colliding write
|
|
19
32
|
* fails loudly with EEXIST instead of silently overwriting a real issue.
|
|
33
|
+
* It is also atomic (ISS-0195): the record is published by link(2) from a
|
|
34
|
+
* complete, fsynced temp, so a SIGKILL mid-write can no longer leave a
|
|
35
|
+
* 0-byte record under a real id, which plain O_EXCL could.
|
|
20
36
|
*
|
|
21
37
|
* Both `createIssue` implementations (src/tools/sentinel.ts and
|
|
22
38
|
* src/sentinelIssues.ts) allocate from a single numbering space and write
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordIdAllocator.d.ts","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"recordIdAllocator.d.ts","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAeH,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,MAAM,CAAC;AAgB1C,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAiC5F;AAED,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE3E;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,EAC1C,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;yEACqE;IACrE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,GACL,OAAO,CAAC,eAAe,CAAC,CAiC1B;AAED,+DAA+D;AAC/D,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,IAAI,GAAG,KAAK,EACvB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EACzC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,EACxC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,eAAe,CAAC,CAE1B"}
|
|
@@ -4,9 +4,22 @@
|
|
|
4
4
|
* The defect this closes: `create_issue` picked the next id by scanning the
|
|
5
5
|
* issues directory for the current maximum, then wrote the file many awaits
|
|
6
6
|
* later. Two processes could read the same maximum and both write ISS-0141.
|
|
7
|
-
* Four duplicate-id groups
|
|
8
|
-
* ISS-0054, ISS-0112), reported independently by the
|
|
9
|
-
* reproduced locally.
|
|
7
|
+
* Four duplicate-id groups were produced by exactly this and were found on disk
|
|
8
|
+
* (ISS-0015, ISS-0028, ISS-0054, ISS-0112), reported independently by the
|
|
9
|
+
* `machina` peer and reproduced locally. THIS repo's four were cleared: as of
|
|
10
|
+
* 2026-09-19 it holds 200 issues and 38 epics with ZERO duplicate frontmatter
|
|
11
|
+
* ids. ISS-0136 remains OPEN, because it is portfolio-wide and 61 groups still
|
|
12
|
+
* sit in five other projects (frontend_v0.2, senken-trading-agent, machina,
|
|
13
|
+
* decibel-studio, decibel-tools-mobile). Do not read "clean" here as "clean
|
|
14
|
+
* everywhere". The history is kept because it is the evidence
|
|
15
|
+
* this lock exists, not because the state persists — stated in the past tense
|
|
16
|
+
* deliberately, since the present-tense version outlived the fact and was read
|
|
17
|
+
* as current by an outside reader, who repeated it in a document.
|
|
18
|
+
*
|
|
19
|
+
* Verify by parsing frontmatter, not by grepping. `grep "^id: ISS-"` over this
|
|
20
|
+
* store counts documents that QUOTE an id — a report about malformed
|
|
21
|
+
* frontmatter contains the line it is reporting — and that false positive has
|
|
22
|
+
* already cost one confidently wrong correction.
|
|
10
23
|
*
|
|
11
24
|
* Two independent defences, because a lock only protects callers that take it:
|
|
12
25
|
*
|
|
@@ -14,9 +27,12 @@
|
|
|
14
27
|
* calculation. Serializing only the scan leaves the same race — both
|
|
15
28
|
* processes still compute 141 before either file lands.
|
|
16
29
|
*
|
|
17
|
-
* 2. The write itself is
|
|
18
|
-
* old client, a migration script, a future mistake — a colliding write
|
|
30
|
+
* 2. The write itself is exclusive. If a caller bypasses the lock entirely —
|
|
31
|
+
* an old client, a migration script, a future mistake — a colliding write
|
|
19
32
|
* fails loudly with EEXIST instead of silently overwriting a real issue.
|
|
33
|
+
* It is also atomic (ISS-0195): the record is published by link(2) from a
|
|
34
|
+
* complete, fsynced temp, so a SIGKILL mid-write can no longer leave a
|
|
35
|
+
* 0-byte record under a real id, which plain O_EXCL could.
|
|
20
36
|
*
|
|
21
37
|
* Both `createIssue` implementations (src/tools/sentinel.ts and
|
|
22
38
|
* src/sentinelIssues.ts) allocate from a single numbering space and write
|
|
@@ -26,6 +42,7 @@
|
|
|
26
42
|
import { promises as fs } from 'fs';
|
|
27
43
|
import path from 'path';
|
|
28
44
|
import { withFileLock } from './fileLock.js';
|
|
45
|
+
import { createFileExclusive, sweepAllocationTemps } from './atomicWrite.js';
|
|
29
46
|
/**
|
|
30
47
|
* Lock file guarding id allocation, scoped per record directory.
|
|
31
48
|
*
|
|
@@ -118,6 +135,7 @@ export async function allocateAndWriteRecord(dir, prefix, extension, slug, build
|
|
|
118
135
|
const maxAttempts = options.maxAttempts ?? 5;
|
|
119
136
|
const lockPath = path.join(dir, LOCK_FILENAME);
|
|
120
137
|
return withFileLock(lockPath, async () => {
|
|
138
|
+
await sweepAllocationTemps(dir);
|
|
121
139
|
let next = (await scanMaxRecordNumber(dir, prefix)) + 1;
|
|
122
140
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
123
141
|
const id = formatRecordId(prefix, next);
|
|
@@ -125,8 +143,8 @@ export async function allocateAndWriteRecord(dir, prefix, extension, slug, build
|
|
|
125
143
|
const filePath = path.join(dir, filename);
|
|
126
144
|
options.validate?.(filePath);
|
|
127
145
|
try {
|
|
128
|
-
//
|
|
129
|
-
await
|
|
146
|
+
// Exclusive (EEXIST rather than clobbering) and atomic (never partial).
|
|
147
|
+
await createFileExclusive(filePath, buildContent(id));
|
|
130
148
|
return { id, filename, filePath };
|
|
131
149
|
}
|
|
132
150
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordIdAllocator.js","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"recordIdAllocator.js","sourceRoot":"","sources":["../../src/lib/recordIdAllocator.ts"],"names":[],"mappings":"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"}
|
package/dist/orgId.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare class OrgIdError extends Error {
|
|
2
|
+
}
|
|
3
|
+
/**
|
|
4
|
+
* The configured org, or null when DECIBEL_ORG_ID is unset or blank.
|
|
5
|
+
* A value that is set but not a uuid throws: a typo must not look like "unset".
|
|
6
|
+
*/
|
|
7
|
+
export declare function getOrgId(): string | null;
|
|
8
|
+
/** The configured org, or an actionable error naming the variable. */
|
|
9
|
+
export declare function requireOrgId(): string;
|
|
10
|
+
/** True when this process holds HQ service credentials. */
|
|
11
|
+
export declare function isHqConnected(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Startup gate. Throws when HQ credentials are present but no valid org is.
|
|
14
|
+
* Returns the org when connected, or null for a local-only install.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assertOrgForHqConnection(): string | null;
|
|
17
|
+
/**
|
|
18
|
+
* For the HQ writers' entry points, which must never throw into the daemon:
|
|
19
|
+
* the org, or null after logging why the feature is off. Reaching null on the
|
|
20
|
+
* daemon path means the startup gate was skipped.
|
|
21
|
+
*/
|
|
22
|
+
export declare function orgOrLog(feature: string): string | null;
|
|
23
|
+
//# sourceMappingURL=orgId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orgId.d.ts","sourceRoot":"","sources":["../src/orgId.ts"],"names":[],"mappings":"AAqBA,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC;;;GAGG;AACH,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAUxC;AAED,sEAAsE;AACtE,wBAAgB,YAAY,IAAI,MAAM,CAWrC;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAGxD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOvD"}
|
package/dist/orgId.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// The org this daemon serves: one source (EPIC-0040, ADR-0014)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// There is no default. This used to fall back to a hardcoded uuid, Ben's org,
|
|
5
|
+
// so an HQ-connected daemon that forgot DECIBEL_ORG_ID silently wrote presence
|
|
6
|
+
// and command settlements into that org under service_role. RLS sits below
|
|
7
|
+
// service_role, so nothing caught it. Once HQ gives every account a personal
|
|
8
|
+
// org (decibel-hq ADR-0016), that fallback becomes a cross-tenant write on
|
|
9
|
+
// every solo install, which is why removing it is step (a) of ADR-0014.
|
|
10
|
+
//
|
|
11
|
+
// The org is required exactly where it is used: the HQ-connected path, i.e.
|
|
12
|
+
// SUPABASE_URL and SUPABASE_SERVICE_KEY are set. A local-only install never
|
|
13
|
+
// reads it. assertOrgForHqConnection() is the daemon's startup gate; the
|
|
14
|
+
// readers call requireOrgId() lazily, never at import time, so a missing
|
|
15
|
+
// variable cannot break boot for a process that would never use it.
|
|
16
|
+
// ============================================================================
|
|
17
|
+
import { log } from './config.js';
|
|
18
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
|
+
export class OrgIdError extends Error {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The configured org, or null when DECIBEL_ORG_ID is unset or blank.
|
|
23
|
+
* A value that is set but not a uuid throws: a typo must not look like "unset".
|
|
24
|
+
*/
|
|
25
|
+
export function getOrgId() {
|
|
26
|
+
const raw = process.env.DECIBEL_ORG_ID?.trim();
|
|
27
|
+
if (!raw)
|
|
28
|
+
return null;
|
|
29
|
+
if (!UUID_RE.test(raw)) {
|
|
30
|
+
throw new OrgIdError('DECIBEL_ORG_ID is set but is not a uuid. It must be the id of your org ' +
|
|
31
|
+
'in HQ (hq.orgs.id).');
|
|
32
|
+
}
|
|
33
|
+
return raw.toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
/** The configured org, or an actionable error naming the variable. */
|
|
36
|
+
export function requireOrgId() {
|
|
37
|
+
const org = getOrgId();
|
|
38
|
+
if (!org) {
|
|
39
|
+
throw new OrgIdError('DECIBEL_ORG_ID is not set. HQ-connected features (presence, agent commands, ' +
|
|
40
|
+
'roster resolution) need the id of the org this machine works for. Set ' +
|
|
41
|
+
'DECIBEL_ORG_ID to your org id from HQ (hq.orgs.id) in the daemon environment. ' +
|
|
42
|
+
'There is no default, so writes cannot land in someone else\'s org.');
|
|
43
|
+
}
|
|
44
|
+
return org;
|
|
45
|
+
}
|
|
46
|
+
/** True when this process holds HQ service credentials. */
|
|
47
|
+
export function isHqConnected() {
|
|
48
|
+
return Boolean(process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_KEY);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Startup gate. Throws when HQ credentials are present but no valid org is.
|
|
52
|
+
* Returns the org when connected, or null for a local-only install.
|
|
53
|
+
*/
|
|
54
|
+
export function assertOrgForHqConnection() {
|
|
55
|
+
if (!isHqConnected())
|
|
56
|
+
return null;
|
|
57
|
+
return requireOrgId();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* For the HQ writers' entry points, which must never throw into the daemon:
|
|
61
|
+
* the org, or null after logging why the feature is off. Reaching null on the
|
|
62
|
+
* daemon path means the startup gate was skipped.
|
|
63
|
+
*/
|
|
64
|
+
export function orgOrLog(feature) {
|
|
65
|
+
try {
|
|
66
|
+
return requireOrgId();
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
log(`${feature}: disabled. ${e instanceof Error ? e.message : String(e)}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=orgId.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orgId.js","sourceRoot":"","sources":["../src/orgId.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+DAA+D;AAC/D,+EAA+E;AAC/E,8EAA8E;AAC9E,+EAA+E;AAC/E,2EAA2E;AAC3E,6EAA6E;AAC7E,2EAA2E;AAC3E,wEAAwE;AACxE,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,oEAAoE;AACpE,+EAA+E;AAE/E,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,OAAO,GAAG,iEAAiE,CAAC;AAElF,MAAM,OAAO,UAAW,SAAQ,KAAK;CAAG;AAExC;;;GAGG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,UAAU,CAClB,yEAAyE;YACvE,qBAAqB,CACxB,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC;AAC3B,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,YAAY;IAC1B,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;IACvB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,UAAU,CAClB,8EAA8E;YAC5E,wEAAwE;YACxE,gFAAgF;YAChF,oEAAoE,CACvE,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAC/E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,IAAI,CAAC,aAAa,EAAE;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,YAAY,EAAE,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,IAAI,CAAC;QACH,OAAO,YAAY,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,GAAG,CAAC,GAAG,OAAO,eAAe,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairCli.d.ts","sourceRoot":"","sources":["../src/pairCli.ts"],"names":[],"mappings":"AAwCA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0DhE"}
|