@skrr-ai/cli 0.1.44 → 0.1.46
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/base-command.d.ts +13 -1
- package/dist/base-command.js +37 -3
- package/dist/commands/agents/actions/create.js +11 -4
- package/dist/commands/agents/chat.js +3 -4
- package/dist/commands/browser/cloud-agents.d.ts +20 -0
- package/dist/commands/browser/cloud-agents.js +43 -0
- package/dist/commands/code/handover.d.ts +11 -0
- package/dist/commands/code/handover.js +105 -1
- package/dist/commands/commitments/handover.d.ts +29 -0
- package/dist/commands/commitments/handover.js +99 -0
- package/dist/commands/followups/list.js +1 -1
- package/dist/commands/followups/watch.js +40 -5
- package/dist/commands/initiatives/list.d.ts +1 -0
- package/dist/commands/initiatives/list.js +11 -2
- package/dist/commands/labels/list.d.ts +28 -0
- package/dist/commands/labels/list.js +50 -32
- package/dist/commands/tasks/fork.d.ts +32 -0
- package/dist/commands/tasks/fork.js +171 -0
- package/dist/commands/tasks/handover.d.ts +23 -0
- package/dist/commands/tasks/handover.js +205 -0
- package/dist/commands/tasks/promote.d.ts +23 -0
- package/dist/commands/tasks/promote.js +82 -0
- package/dist/commands/tasks/runs.d.ts +16 -0
- package/dist/commands/tasks/runs.js +46 -1
- package/dist/commands/tasks/show.d.ts +23 -0
- package/dist/commands/tasks/show.js +59 -0
- package/dist/lib/agentic-stream.js +41 -2
- package/dist/lib/api-fetch.d.ts +7 -1
- package/dist/lib/api-fetch.js +9 -2
- package/dist/lib/auth-core-init.js +11 -4
- package/dist/lib/code-handover.d.ts +60 -0
- package/dist/lib/code-handover.js +120 -0
- package/dist/lib/commitments.d.ts +22 -0
- package/dist/lib/commitments.js +42 -0
- package/dist/lib/dedicated-wait.js +22 -1
- package/dist/lib/followups.d.ts +18 -0
- package/dist/lib/followups.js +44 -0
- package/dist/lib/initiatives.d.ts +15 -3
- package/dist/lib/label-ref.d.ts +2 -0
- package/dist/lib/node-adapter.js +6 -0
- package/dist/lib/task-extras.d.ts +44 -0
- package/dist/lib/task-extras.js +73 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +169 -23
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +91 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +26 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +177 -26
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +119 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +346 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +169 -24
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +93 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +177 -26
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +119 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +333 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2746 -2711
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +34300 -33843
- package/package.json +2 -2
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SUPERSEDED_COPY_SUFFIX = exports.SUPERSEDED_MARKER_SUFFIX = exports.PROFILES_SEGMENT = void 0;
|
|
7
|
+
exports.profileStateDir = profileStateDir;
|
|
8
|
+
exports.legacyProfileStateDir = legacyProfileStateDir;
|
|
9
|
+
exports.resolveProfileStateDir = resolveProfileStateDir;
|
|
10
|
+
exports.resolveProfileStateFile = resolveProfileStateFile;
|
|
11
|
+
exports.profileStateFileLocations = profileStateFileLocations;
|
|
12
|
+
exports.consolidateProfileState = consolidateProfileState;
|
|
13
|
+
exports.profileStateFilePathsToClear = profileStateFilePathsToClear;
|
|
14
|
+
/**
|
|
15
|
+
* profileStateDir.ts — the ONE definition of where a profile's auth state lives
|
|
16
|
+
* on disk, and the migration for a machine that ended up with two.
|
|
17
|
+
*
|
|
18
|
+
* A profile is this product's unit of credential isolation, and its state has
|
|
19
|
+
* exactly one home:
|
|
20
|
+
*
|
|
21
|
+
* <machine root>/profiles/<profile>/cred-dek-wrapped
|
|
22
|
+
* <machine root>/profiles/<profile>/cred-envelope.lock
|
|
23
|
+
* <machine root>/profiles/<profile>/device-private-key
|
|
24
|
+
* …
|
|
25
|
+
*
|
|
26
|
+
* It lives here, next to `configRoot.ts`, for the same reason that file exists:
|
|
27
|
+
* both binaries need the same answer and neither owns it. `configRoot.ts` made
|
|
28
|
+
* "where is the skrr root" one function; this makes "where is a profile's auth
|
|
29
|
+
* state" one function, because the layer above it had the identical defect one
|
|
30
|
+
* level down.
|
|
31
|
+
*
|
|
32
|
+
* WHAT WENT WRONG (OSK-10314). `credentialEnvelopeBridge` and
|
|
33
|
+
* `deviceIdentityBridge` each wrote `path.join(getAuthConfigDir(), 'profiles',
|
|
34
|
+
* profile)` — two independent spellings of one rule — and `configDir` means
|
|
35
|
+
* different things to the two consumers:
|
|
36
|
+
*
|
|
37
|
+
* - the CLI passes the MACHINE ROOT, so it landed on the layout above;
|
|
38
|
+
* - the daemon passes `getConfigDir()`, which is ALREADY `<root>/profiles/<p>`,
|
|
39
|
+
* so it landed on `<root>/profiles/<p>/profiles/<p>/…`.
|
|
40
|
+
*
|
|
41
|
+
* One profile therefore had two wrapped DEKs, two device keypairs and two
|
|
42
|
+
* lockfiles, and the lockfile the shared module exists to share served neither
|
|
43
|
+
* of the two processes it was meant to serialize. Nothing cross-read, so
|
|
44
|
+
* nothing broke — it was a split-brain waiting for the first cross-read.
|
|
45
|
+
*
|
|
46
|
+
* The fix is to derive the directory from `getAuthMachineConfigDir()`, which
|
|
47
|
+
* BOTH consumers already wire explicitly (daemon: `getMachineConfigDir()`; CLI:
|
|
48
|
+
* the skrr root). Note what is deliberately NOT done: the daemon is not asked to
|
|
49
|
+
* pass a different `configDir`. It has one knob and auth-core needs two answers
|
|
50
|
+
* from it — `refresh.ts` reads `getAuthConfigDir()` raw for `auth.lock` and
|
|
51
|
+
* `needs-reauth.json`, so repointing the daemon's `configDir` at the machine
|
|
52
|
+
* root would quietly move its reauth latch from per-profile to machine-wide and
|
|
53
|
+
* let one profile latch every profile. Profile-scoped and root-scoped artifacts
|
|
54
|
+
* are only distinguishable INSIDE auth-core, so that is where the line is drawn.
|
|
55
|
+
*
|
|
56
|
+
* WHY A BARE PATH SWAP IS NOT THE FIX. A daemon with a nested wrapped DEK has
|
|
57
|
+
* sealed real credentials under it. Pointing it at the canonical path hands it a
|
|
58
|
+
* DIFFERENT DEK and every one of those credentials stops decrypting — a silent,
|
|
59
|
+
* fleet-wide forced re-login, to repair a bug that had so far harmed nobody. So
|
|
60
|
+
* the resolver PREFERS THE FILE A PROCESS IS ALREADY USING until a migration has
|
|
61
|
+
* proved the canonical one is equivalent, and the migration COPIES.
|
|
62
|
+
*
|
|
63
|
+
* Resolution order for `<filename>` under `<profile>` (stateless — no ordering
|
|
64
|
+
* requirement between this and any init):
|
|
65
|
+
*
|
|
66
|
+
* 1. legacy dir IS the canonical dir (the CLI) → canonical
|
|
67
|
+
* 2. no legacy copy of the file → canonical
|
|
68
|
+
* 3. a `<filename>.superseded.json` marker beside it → canonical (consolidated)
|
|
69
|
+
* 4. no canonical copy of the file → legacy (not yet consolidated)
|
|
70
|
+
* 5. both copies, no marker → legacy (unresolved: stay put)
|
|
71
|
+
*
|
|
72
|
+
* The migration writes the marker, never deletes, and refuses case 5 when both
|
|
73
|
+
* sides hold a usable and DIFFERENT key — because merging those would require
|
|
74
|
+
* re-sealing content that lives in the OS keychain and is not auth-core's to
|
|
75
|
+
* re-seal. A human resolves that one, and the loud signal is the deliverable.
|
|
76
|
+
*
|
|
77
|
+
* ROLLBACK. The legacy file is left exactly where it was, under its own name.
|
|
78
|
+
* Older code looks there, finds it, and works — no operator step. The marker is
|
|
79
|
+
* a sidecar older code never reads.
|
|
80
|
+
*/
|
|
81
|
+
const node_crypto_1 = require("node:crypto");
|
|
82
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
83
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
84
|
+
const runtime_js_1 = require("./runtime.js");
|
|
85
|
+
/** Directory segment that separates per-profile state from machine-scoped state. */
|
|
86
|
+
exports.PROFILES_SEGMENT = 'profiles';
|
|
87
|
+
/**
|
|
88
|
+
* Sidecar written beside a legacy file once it has been consolidated. Its
|
|
89
|
+
* PRESENCE is what flips resolution to the canonical copy; its contents are for
|
|
90
|
+
* a human reading the directory afterwards.
|
|
91
|
+
*/
|
|
92
|
+
exports.SUPERSEDED_MARKER_SUFFIX = '.superseded.json';
|
|
93
|
+
/**
|
|
94
|
+
* Suffix for a canonical file moved aside to make room. Only used when the
|
|
95
|
+
* canonical copy existed and was NOT usable — a usable one is never replaced.
|
|
96
|
+
*/
|
|
97
|
+
exports.SUPERSEDED_COPY_SUFFIX = '.superseded';
|
|
98
|
+
/** The canonical directory for a profile's auth state. */
|
|
99
|
+
function profileStateDir(profile) {
|
|
100
|
+
assertProfile(profile);
|
|
101
|
+
return node_path_1.default.join((0, runtime_js_1.getAuthMachineConfigDir)(), exports.PROFILES_SEGMENT, profile);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Where a consumer whose `configDir` is already profile-scoped used to write.
|
|
105
|
+
*
|
|
106
|
+
* `null` when it resolves to the canonical directory — i.e. the consumer passes
|
|
107
|
+
* the machine root and never had a second location. Callers branch on `null`
|
|
108
|
+
* rather than comparing strings so the "one root" case costs one check.
|
|
109
|
+
*/
|
|
110
|
+
function legacyProfileStateDir(profile) {
|
|
111
|
+
assertProfile(profile);
|
|
112
|
+
const legacy = node_path_1.default.join((0, runtime_js_1.getAuthConfigDir)(), exports.PROFILES_SEGMENT, profile);
|
|
113
|
+
const canonical = profileStateDir(profile);
|
|
114
|
+
return node_path_1.default.resolve(legacy) === node_path_1.default.resolve(canonical) ? null : legacy;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve the directory holding `primaryFilename` for `profile`.
|
|
118
|
+
*
|
|
119
|
+
* `primaryFilename` is the file whose presence decides where a group of related
|
|
120
|
+
* files lives. Device identity has four files that must be read from ONE
|
|
121
|
+
* directory — a public JWK from one copy and a private key from the other is a
|
|
122
|
+
* keypair mismatch — so the group anchors on the private key rather than each
|
|
123
|
+
* file answering for itself.
|
|
124
|
+
*/
|
|
125
|
+
function resolveProfileStateDir(profile, primaryFilename) {
|
|
126
|
+
const canonicalDir = profileStateDir(profile);
|
|
127
|
+
const legacyDir = legacyProfileStateDir(profile);
|
|
128
|
+
if (legacyDir === null) {
|
|
129
|
+
return { dir: canonicalDir, canonicalDir, legacyDir: null, usingLegacy: false };
|
|
130
|
+
}
|
|
131
|
+
const legacyFile = node_path_1.default.join(legacyDir, primaryFilename);
|
|
132
|
+
const stayOnLegacy = existsFile(legacyFile) && !existsFile(`${legacyFile}${exports.SUPERSEDED_MARKER_SUFFIX}`);
|
|
133
|
+
return {
|
|
134
|
+
dir: stayOnLegacy ? legacyDir : canonicalDir,
|
|
135
|
+
canonicalDir,
|
|
136
|
+
legacyDir,
|
|
137
|
+
usingLegacy: stayOnLegacy,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** `resolveProfileStateDir` for a single self-anchoring file. */
|
|
141
|
+
function resolveProfileStateFile(profile, filename) {
|
|
142
|
+
const resolved = resolveProfileStateDir(profile, filename);
|
|
143
|
+
return {
|
|
144
|
+
...resolved,
|
|
145
|
+
path: node_path_1.default.join(resolved.dir, filename),
|
|
146
|
+
canonicalPath: node_path_1.default.join(resolved.canonicalDir, filename),
|
|
147
|
+
legacyPath: resolved.legacyDir === null ? null : node_path_1.default.join(resolved.legacyDir, filename),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Every location `filename` may occupy for `profile`, canonical first. */
|
|
151
|
+
function profileStateFileLocations(profile, filename) {
|
|
152
|
+
const canonicalDir = profileStateDir(profile);
|
|
153
|
+
const legacyDir = legacyProfileStateDir(profile);
|
|
154
|
+
const out = [node_path_1.default.join(canonicalDir, filename)];
|
|
155
|
+
if (legacyDir !== null)
|
|
156
|
+
out.push(node_path_1.default.join(legacyDir, filename));
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Consolidate a profile's state onto the canonical path. Idempotent, additive,
|
|
161
|
+
* and refuses rather than guesses.
|
|
162
|
+
*
|
|
163
|
+
* The one case it will not decide is two usable, different keys for one profile:
|
|
164
|
+
* each side has sealed content under its own, that content lives in the OS
|
|
165
|
+
* keychain, and nothing here can re-seal it. Picking a winner there would
|
|
166
|
+
* destroy the loser's credentials silently, which is the exact failure this
|
|
167
|
+
* whole module exists to avoid.
|
|
168
|
+
*/
|
|
169
|
+
async function consolidateProfileState(profile, spec) {
|
|
170
|
+
assertProfile(profile);
|
|
171
|
+
const dryRun = spec.dryRun === true;
|
|
172
|
+
const canonicalDir = profileStateDir(profile);
|
|
173
|
+
const legacyDir = legacyProfileStateDir(profile);
|
|
174
|
+
const base = { profile, canonicalDir, legacyDir, written: [], supersededAside: [], dryRun };
|
|
175
|
+
if (legacyDir === null)
|
|
176
|
+
return { ...base, status: 'not_applicable' };
|
|
177
|
+
const legacyPrimary = node_path_1.default.join(legacyDir, spec.primary);
|
|
178
|
+
if (!existsFile(legacyPrimary))
|
|
179
|
+
return { ...base, status: 'nothing_to_migrate' };
|
|
180
|
+
const markerPath = `${legacyPrimary}${exports.SUPERSEDED_MARKER_SUFFIX}`;
|
|
181
|
+
if (existsFile(markerPath))
|
|
182
|
+
return { ...base, status: 'already_consolidated' };
|
|
183
|
+
const canonicalPrimary = node_path_1.default.join(canonicalDir, spec.primary);
|
|
184
|
+
if (!existsFile(canonicalPrimary)) {
|
|
185
|
+
const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
|
|
186
|
+
if (!dryRun)
|
|
187
|
+
writeMarker(markerPath, canonicalPrimary, 'adopted');
|
|
188
|
+
emitOutcome(spec, 'adopted', { profile, from: legacyDir, to: canonicalDir });
|
|
189
|
+
return { ...base, status: 'adopted', ...moved };
|
|
190
|
+
}
|
|
191
|
+
const identify = spec.identify ?? ((dir) => sha256OfFile(node_path_1.default.join(dir, spec.primary)));
|
|
192
|
+
const legacyIdentity = await identify(legacyDir);
|
|
193
|
+
const canonicalIdentity = await identify(canonicalDir);
|
|
194
|
+
const identities = { canonical: canonicalIdentity, legacy: legacyIdentity };
|
|
195
|
+
// Same state in both places, or the legacy copy is unreadable while the
|
|
196
|
+
// canonical one is fine. Either way the canonical copy is authoritative and
|
|
197
|
+
// the legacy one has nothing to contribute — record that and move on.
|
|
198
|
+
const sameState = legacyIdentity !== null && canonicalIdentity !== null && legacyIdentity === canonicalIdentity;
|
|
199
|
+
const legacyHasNothingToOffer = legacyIdentity === null && canonicalIdentity !== null;
|
|
200
|
+
const bothUnreadableButIdentical = legacyIdentity === null &&
|
|
201
|
+
canonicalIdentity === null &&
|
|
202
|
+
sha256OfFile(legacyPrimary) !== null &&
|
|
203
|
+
sha256OfFile(legacyPrimary) === sha256OfFile(canonicalPrimary);
|
|
204
|
+
if (sameState || legacyHasNothingToOffer || bothUnreadableButIdentical) {
|
|
205
|
+
if (!dryRun)
|
|
206
|
+
writeMarker(markerPath, canonicalPrimary, 'already_consolidated');
|
|
207
|
+
emitOutcome(spec, 'already_consolidated', { profile, identities });
|
|
208
|
+
return { ...base, status: 'already_consolidated', identities };
|
|
209
|
+
}
|
|
210
|
+
// The legacy copy is usable and the canonical one is not. Nothing is sealed
|
|
211
|
+
// under an unreadable key that was not already lost, so replacing it costs
|
|
212
|
+
// nothing — but keep it, because "unreadable here, today" is not "worthless".
|
|
213
|
+
if (legacyIdentity !== null && canonicalIdentity === null) {
|
|
214
|
+
const aside = dryRun
|
|
215
|
+
? plannedAside(canonicalDir, spec.files)
|
|
216
|
+
: moveAside(canonicalDir, spec.files);
|
|
217
|
+
const moved = adopt(legacyDir, canonicalDir, spec.files, dryRun);
|
|
218
|
+
if (!dryRun)
|
|
219
|
+
writeMarker(markerPath, canonicalPrimary, 'superseded');
|
|
220
|
+
emitOutcome(spec, 'superseded', { profile, identities, supersededAside: aside });
|
|
221
|
+
return {
|
|
222
|
+
...base,
|
|
223
|
+
status: 'superseded',
|
|
224
|
+
written: moved.written,
|
|
225
|
+
supersededAside: aside,
|
|
226
|
+
identities,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
emitOutcome(spec, 'conflict', { profile, identities, canonicalDir, legacyDir });
|
|
230
|
+
return { ...base, status: 'conflict', identities };
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Every path a profile's copy of `filename` can occupy, plus its marker —
|
|
234
|
+
* what a genuine logout must remove. Deliberately separate from the migration:
|
|
235
|
+
* this module never deletes, and logout is the one caller that must.
|
|
236
|
+
*/
|
|
237
|
+
function profileStateFilePathsToClear(profile, filename) {
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const location of profileStateFileLocations(profile, filename)) {
|
|
240
|
+
out.push(location);
|
|
241
|
+
out.push(`${location}${exports.SUPERSEDED_MARKER_SUFFIX}`);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// internals
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
function assertProfile(profile) {
|
|
249
|
+
if (typeof profile !== 'string' || profile.length === 0) {
|
|
250
|
+
throw new Error('profileStateDir: profile must be a non-empty string');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function existsFile(target) {
|
|
254
|
+
try {
|
|
255
|
+
return node_fs_1.default.statSync(target).isFile();
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function sha256OfFile(target) {
|
|
262
|
+
try {
|
|
263
|
+
return (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(target)).digest('hex');
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* tmp + fsync + rename + dir-fsync, 0600. The same shape the two bridges
|
|
271
|
+
* already use, so a crash mid-migration leaves either the old file or the new
|
|
272
|
+
* one and never a half-written key.
|
|
273
|
+
*/
|
|
274
|
+
function atomicWrite0600(target, value) {
|
|
275
|
+
const dir = node_path_1.default.dirname(target);
|
|
276
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
277
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
278
|
+
const fd = node_fs_1.default.openSync(tmp, 'w', 0o600);
|
|
279
|
+
try {
|
|
280
|
+
node_fs_1.default.writeSync(fd, value);
|
|
281
|
+
node_fs_1.default.fsyncSync(fd);
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
node_fs_1.default.closeSync(fd);
|
|
285
|
+
}
|
|
286
|
+
node_fs_1.default.renameSync(tmp, target);
|
|
287
|
+
try {
|
|
288
|
+
const dirFd = node_fs_1.default.openSync(dir, 'r');
|
|
289
|
+
try {
|
|
290
|
+
node_fs_1.default.fsyncSync(dirFd);
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
node_fs_1.default.closeSync(dirFd);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
/* best-effort — Windows / some BSDs reject dir fsync */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function adopt(fromDir, toDir, files, dryRun) {
|
|
301
|
+
const written = [];
|
|
302
|
+
for (const name of files) {
|
|
303
|
+
const source = node_path_1.default.join(fromDir, name);
|
|
304
|
+
if (!existsFile(source))
|
|
305
|
+
continue;
|
|
306
|
+
const target = node_path_1.default.join(toDir, name);
|
|
307
|
+
if (!dryRun)
|
|
308
|
+
atomicWrite0600(target, node_fs_1.default.readFileSync(source));
|
|
309
|
+
written.push(target);
|
|
310
|
+
}
|
|
311
|
+
return { written, supersededAside: [] };
|
|
312
|
+
}
|
|
313
|
+
function moveAside(dir, files) {
|
|
314
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
315
|
+
const aside = [];
|
|
316
|
+
for (const name of files) {
|
|
317
|
+
const target = node_path_1.default.join(dir, name);
|
|
318
|
+
if (!existsFile(target))
|
|
319
|
+
continue;
|
|
320
|
+
node_fs_1.default.renameSync(target, `${target}${exports.SUPERSEDED_COPY_SUFFIX}.${stamp}`);
|
|
321
|
+
aside.push(target);
|
|
322
|
+
}
|
|
323
|
+
return aside;
|
|
324
|
+
}
|
|
325
|
+
function plannedAside(dir, files) {
|
|
326
|
+
return files.map((name) => node_path_1.default.join(dir, name)).filter((target) => existsFile(target));
|
|
327
|
+
}
|
|
328
|
+
function writeMarker(markerPath, supersededBy, reason) {
|
|
329
|
+
const payload = {
|
|
330
|
+
supersededBy,
|
|
331
|
+
reason,
|
|
332
|
+
sha256: sha256OfFile(supersededBy),
|
|
333
|
+
at: new Date().toISOString(),
|
|
334
|
+
note: 'Kept deliberately. This file is the pre-consolidation copy (OSK-10314); ' +
|
|
335
|
+
'older binaries read it and it is the only rollback that needs no operator step. ' +
|
|
336
|
+
'Remove it only through logout.',
|
|
337
|
+
};
|
|
338
|
+
atomicWrite0600(markerPath, Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf-8'));
|
|
339
|
+
}
|
|
340
|
+
function emitOutcome(spec, status, meta) {
|
|
341
|
+
spec.onEvent?.(`path_consolidation.${status}`, status !== 'conflict', {
|
|
342
|
+
...meta,
|
|
343
|
+
primary: spec.primary,
|
|
344
|
+
...(spec.dryRun === true ? { dryRun: true } : {}),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
@@ -36,6 +36,14 @@ export declare function initCredEnvelope(profile: string): Promise<void>;
|
|
|
36
36
|
* Reset state. With `clearOnDisk` deletes the wrapped DEK file too —
|
|
37
37
|
* used by `oversky logout` / `sky logout`. Zeroes the in-memory DEK
|
|
38
38
|
* before dropping state for forward secrecy on the active credential.
|
|
39
|
+
*
|
|
40
|
+
* It deletes EVERY location this profile's key can occupy, plus the
|
|
41
|
+
* consolidation marker. `profileStateDir.ts` never deletes anything and says so;
|
|
42
|
+
* this is the one caller that must, and the reason is not symmetry — a logout
|
|
43
|
+
* that removed only the canonical copy would leave the pre-consolidation copy
|
|
44
|
+
* behind, and the next init would adopt it straight back. That is a deleted
|
|
45
|
+
* credential key resurrecting itself, which is a worse bug than the one this
|
|
46
|
+
* whole change is fixing.
|
|
39
47
|
*/
|
|
40
48
|
export declare function resetCredEnvelope(opts?: {
|
|
41
49
|
profile?: string;
|
|
@@ -49,6 +57,17 @@ export declare function __resetShutdownGuardForTest(): void;
|
|
|
49
57
|
* predicate used by sync transforms; never throws.
|
|
50
58
|
*/
|
|
51
59
|
export declare function isCredEnvelopeActive(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* True while this profile holds two usable and different wrapped DEKs.
|
|
62
|
+
*
|
|
63
|
+
* Read by `deviceIdentityBridge` before it migrates anything of its own. The
|
|
64
|
+
* device private key is sealed with whichever DEK is active, so moving it to the
|
|
65
|
+
* canonical directory while the two binaries are still on DIFFERENT DEKs hands
|
|
66
|
+
* the other one a key it cannot open — it regenerates, overwrites, and the two
|
|
67
|
+
* processes then destroy each other's device identity on every start. Unresolved
|
|
68
|
+
* conflicts freeze the whole profile's layout, not just the DEK's.
|
|
69
|
+
*/
|
|
70
|
+
export declare function hasCredEnvelopePathConflict(): boolean;
|
|
52
71
|
/**
|
|
53
72
|
* Operator-readable summary of envelope state. Used by `oversky status`
|
|
54
73
|
* and CLI debug commands. Never includes the DEK or any key material.
|
|
@@ -65,6 +84,16 @@ export declare function describeCredEnvelopeState(): {
|
|
|
65
84
|
kekKind?: string | null;
|
|
66
85
|
profile?: string;
|
|
67
86
|
kekRotated?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Set when this profile holds two usable and DIFFERENT wrapped DEKs and the
|
|
89
|
+
* consolidation refused to pick one (OSK-10314). The envelope is fully
|
|
90
|
+
* operational — this process reads the key it has always read — but the other
|
|
91
|
+
* binary is holding a different one and cannot read what this one seals.
|
|
92
|
+
*/
|
|
93
|
+
pathConflict?: {
|
|
94
|
+
canonicalPath: string;
|
|
95
|
+
legacyPath: string;
|
|
96
|
+
};
|
|
68
97
|
};
|
|
69
98
|
/**
|
|
70
99
|
* Sync write transform. Returns the wrapped serialized form when active.
|