@skrr-ai/cli 0.1.122 → 0.1.124
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/commands/agents/chat.d.ts +7 -0
- package/dist/commands/agents/chat.js +32 -6
- package/dist/commands/agents/delete.d.ts +8 -0
- package/dist/commands/agents/delete.js +16 -0
- package/dist/commands/context.js +7 -0
- package/dist/commands/followups/act.js +3 -0
- package/dist/commands/followups/checkin.js +3 -0
- package/dist/commands/followups/remind.js +3 -0
- package/dist/commands/followups/show.js +1 -1
- package/dist/commands/followups/watch.js +3 -0
- package/dist/commands/search/workspace.js +1 -1
- package/dist/commands/store/gallery-admissions/approve.js +1 -1
- package/dist/commands/store/gallery-admissions/preview.js +1 -1
- package/dist/commands/store/gallery-admissions/refuse.js +1 -1
- package/dist/commands/store/gallery-admissions/show.js +1 -1
- package/dist/lib/agentic-stream.d.ts +46 -0
- package/dist/lib/agentic-stream.js +135 -14
- package/dist/lib/auth-core-init.d.ts +15 -2
- package/dist/lib/auth-core-init.js +47 -6
- package/dist/lib/auth-failure.js +12 -0
- package/dist/lib/command-hygiene.js +30 -1
- package/dist/lib/daemonBrokerRefusal.js +5 -1
- package/dist/lib/exec-runtime-binary.d.ts +9 -0
- package/dist/lib/exec-runtime-binary.js +15 -1
- package/dist/lib/followups.d.ts +13 -0
- package/dist/lib/followups.js +8 -0
- package/dist/lib/keychain.js +41 -4
- package/dist/lib/profile-machine-identity-guard.js +2 -1
- package/dist/lib/runtime-start-verification.js +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +69 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +135 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +440 -83
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +110 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +13 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +11 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +14 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +69 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +67 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +24 -16
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +122 -38
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +135 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +603 -90
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +14 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +18 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +23 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +69 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +134 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +45 -16
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +40 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +7 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +7349 -7325
- package/oclif.manifest.json +11 -2
- package/package.json +4 -1
|
@@ -25,7 +25,9 @@ import fs from 'node:fs';
|
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import lockfile from 'proper-lockfile';
|
|
27
27
|
import { getAuthLogger, getAuthConfigDir, emitAuthTelemetry, notifyResponseObserver, } from './runtime.js';
|
|
28
|
+
import { rethrowIfLiveStateRefusal } from './liveState.js';
|
|
28
29
|
import { permanentReasonForRefreshCode } from './refreshClassification.js';
|
|
30
|
+
import { ensureStateDir, removeStateFile, writeStateFile } from './stateFiles.js';
|
|
29
31
|
import { PermanentAuthFailure, TransientAuthFailure, } from './types.js';
|
|
30
32
|
/**
|
|
31
33
|
* P1-6: Reduced from 30 s to 5 s.
|
|
@@ -254,9 +256,9 @@ export async function withAuthLock(body, daemonId) {
|
|
|
254
256
|
const lockPath = authLockPath();
|
|
255
257
|
const dir = path.dirname(lockPath);
|
|
256
258
|
if (!fs.existsSync(dir))
|
|
257
|
-
|
|
259
|
+
ensureStateDir(dir, 'create the auth lock directory', null);
|
|
258
260
|
if (!fs.existsSync(lockPath)) {
|
|
259
|
-
|
|
261
|
+
writeStateFile(lockPath, '', { mode: 0o600 }, 'create the auth lock');
|
|
260
262
|
}
|
|
261
263
|
const acquireStart = Date.now();
|
|
262
264
|
emitAuthTelemetry({
|
|
@@ -611,7 +613,12 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
611
613
|
// "proof-per-attempt invariant: a retry loop must call buildRequestInit()
|
|
612
614
|
// per attempt" in `refresh.deviceProof.test.ts` asserts the function
|
|
613
615
|
// is defined inside `doRefresh` and will fail if it is moved.
|
|
614
|
-
|
|
616
|
+
// Whether the request the server just answered carried a device proof. A
|
|
617
|
+
// `DEVICE_PROOF_MISSING` is only a LOCAL fact (no key to sign with) when we
|
|
618
|
+
// know none went out; if one did, something between us stripped it.
|
|
619
|
+
let proofAttached = false;
|
|
620
|
+
async function buildRequestInit() {
|
|
621
|
+
proofAttached = false;
|
|
615
622
|
// Phase 3-R5: the stable `X-Oversky-Origin` header is the server's
|
|
616
623
|
// preferred identity signal on the benign-grace rotation match. Newer
|
|
617
624
|
// servers prefer it over the raw User-Agent; older servers ignore
|
|
@@ -633,7 +640,9 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
633
640
|
__refreshDiagnostics.requestAttemptCount += 1;
|
|
634
641
|
if (options.signRequestProof) {
|
|
635
642
|
try {
|
|
636
|
-
|
|
643
|
+
// Awaited: a hardware key signs in a helper process. A synchronous
|
|
644
|
+
// signer (a test double, an older caller) still works unchanged.
|
|
645
|
+
const proof = await options.signRequestProof({
|
|
637
646
|
method: 'POST',
|
|
638
647
|
url,
|
|
639
648
|
refreshToken: currentRefreshToken,
|
|
@@ -646,6 +655,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
646
655
|
__refreshDiagnostics.proofAttemptCount += 1;
|
|
647
656
|
if (typeof proof === 'string' && proof.length > 0) {
|
|
648
657
|
headers['OverSky-DPoP'] = proof;
|
|
658
|
+
proofAttached = true;
|
|
649
659
|
}
|
|
650
660
|
}
|
|
651
661
|
catch (err) {
|
|
@@ -669,7 +679,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
669
679
|
// Single attempt today. The builder structure is the load-bearing
|
|
670
680
|
// bit: any future retry must call `buildRequestInit()` again so
|
|
671
681
|
// the per-attempt invariant — fresh jti per request — holds.
|
|
672
|
-
response = await fetch(url, buildRequestInit());
|
|
682
|
+
response = await fetch(url, await buildRequestInit());
|
|
673
683
|
notifyResponseObserver(response);
|
|
674
684
|
}
|
|
675
685
|
catch (err) {
|
|
@@ -867,10 +877,29 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
867
877
|
// the FINAL fallback. The distinct `UNKNOWN_401` telemetry tag is kept
|
|
868
878
|
// on BOTH branches so monitoring can still see a server-side bug
|
|
869
879
|
// separately from a real expiration (and watch the retry-vs-latch split).
|
|
880
|
+
//
|
|
881
|
+
// One unknown-shape 401 IS diagnosable: the server demanded a device proof
|
|
882
|
+
// (`DEVICE_PROOF_MISSING`) and this request carried none. That is a fact
|
|
883
|
+
// about this machine — an unreadable, never-created or unwired device key —
|
|
884
|
+
// and it never self-heals, but it stays on the same budget (a proxy that
|
|
885
|
+
// strips the header, or an identity still initialising, reads the same on
|
|
886
|
+
// one attempt). Only what the exhausted latch SAYS differs, so a person is
|
|
887
|
+
// sent to the sign-in that mints a key rather than told "sign in again".
|
|
888
|
+
// If a proof DID go out, the header was lost on the way: that stays generic.
|
|
889
|
+
const noLocalProof = parsedBody.code === 'DEVICE_PROOF_MISSING' && !proofAttached;
|
|
890
|
+
const latchReason = noLocalProof
|
|
891
|
+
? 'DEVICE_KEY_UNAVAILABLE'
|
|
892
|
+
: 'UNKNOWN_401';
|
|
870
893
|
const message = parsedBody.message || 'Unknown auth failure';
|
|
894
|
+
const latchMessage = noLocalProof
|
|
895
|
+
? `This computer has no usable device key, so the refresh carried no device proof (server: ${message})`
|
|
896
|
+
: message;
|
|
871
897
|
const { exhausted, count } = recordUnknown401(daemonId, Date.now());
|
|
872
898
|
if (!exhausted) {
|
|
873
|
-
log.warn(`[auth/refresh] unknown-shape 401 (${count}/${UNKNOWN_401_BUDGET}) — treating as transient, will retry: ${message}`
|
|
899
|
+
log.warn(`[auth/refresh] unknown-shape 401 (${count}/${UNKNOWN_401_BUDGET}) — treating as transient, will retry: ${message}` +
|
|
900
|
+
(noLocalProof
|
|
901
|
+
? ' (no device proof was sent: this machine has no usable device key)'
|
|
902
|
+
: ''));
|
|
874
903
|
emitAuthTelemetry({
|
|
875
904
|
timestamp: new Date().toISOString(),
|
|
876
905
|
eventType: 'auth',
|
|
@@ -893,7 +922,7 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
893
922
|
// permanent behavior: latch + persist the reauth flag so the daemon
|
|
894
923
|
// surfaces the banner and the interactive modal becomes the final
|
|
895
924
|
// recovery path.
|
|
896
|
-
log.warn(`[auth/refresh] unknown-shape 401 budget exhausted (${count}/${UNKNOWN_401_BUDGET}) — latching permanent: ${message}`);
|
|
925
|
+
log.warn(`[auth/refresh] unknown-shape 401 budget exhausted (${count}/${UNKNOWN_401_BUDGET}) — latching permanent ${latchReason}: ${message}`);
|
|
897
926
|
emitAuthTelemetry({
|
|
898
927
|
timestamp: new Date().toISOString(),
|
|
899
928
|
eventType: 'auth',
|
|
@@ -902,18 +931,18 @@ async function doRefresh(serverUrl, currentRefreshToken, daemonId, options = {})
|
|
|
902
931
|
event: 'refresh_failure',
|
|
903
932
|
daemonId,
|
|
904
933
|
durationMs: Date.now() - startMs,
|
|
905
|
-
errorCode:
|
|
934
|
+
errorCode: latchReason,
|
|
906
935
|
statusCode: 401,
|
|
907
936
|
transient: false,
|
|
908
937
|
unknown401Streak: count,
|
|
909
938
|
},
|
|
910
939
|
});
|
|
911
940
|
persistReauthState({
|
|
912
|
-
code:
|
|
913
|
-
message,
|
|
941
|
+
code: latchReason,
|
|
942
|
+
message: latchMessage,
|
|
914
943
|
at: Date.now(),
|
|
915
944
|
});
|
|
916
|
-
throw new PermanentAuthFailure(
|
|
945
|
+
throw new PermanentAuthFailure(latchReason, latchMessage);
|
|
917
946
|
}
|
|
918
947
|
// 5xx / unexpected — transient. Caller will back off.
|
|
919
948
|
// RESET SEMANTICS (Finding 4 follow-up): a 5xx is transient and does NOT
|
|
@@ -947,12 +976,11 @@ export function persistReauthState(state) {
|
|
|
947
976
|
try {
|
|
948
977
|
const dir = path.dirname(flagPath);
|
|
949
978
|
if (!fs.existsSync(dir))
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
mode: 0o600,
|
|
953
|
-
});
|
|
979
|
+
ensureStateDir(dir, 'create the re-auth latch directory');
|
|
980
|
+
writeStateFile(flagPath, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 }, 'write the re-auth latch');
|
|
954
981
|
}
|
|
955
982
|
catch (err) {
|
|
983
|
+
rethrowIfLiveStateRefusal(err);
|
|
956
984
|
log.debug(`[auth/refresh] failed to persist reauth flag: ${err?.message}`);
|
|
957
985
|
}
|
|
958
986
|
}
|
|
@@ -984,9 +1012,10 @@ export function clearReauthState() {
|
|
|
984
1012
|
try {
|
|
985
1013
|
const flagPath = reauthFlagPath();
|
|
986
1014
|
if (fs.existsSync(flagPath))
|
|
987
|
-
|
|
1015
|
+
removeStateFile(flagPath, 'clear the re-auth latch');
|
|
988
1016
|
}
|
|
989
1017
|
catch (err) {
|
|
1018
|
+
rethrowIfLiveStateRefusal(err);
|
|
990
1019
|
log.debug(`[auth/refresh] clearReauthState: ${err?.message}`);
|
|
991
1020
|
}
|
|
992
1021
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stateFiles.ts — the ONE place auth-core mutates credential state on disk.
|
|
3
|
+
*
|
|
4
|
+
* Every write, delete, rename and mkdir of a wrapped DEK, a device key, an enrollment marker, a
|
|
5
|
+
* lock sentinel, a re-auth latch or a machine identity goes through a function here, and each
|
|
6
|
+
* asks `assertMayMutateLiveState` first: a test process aimed at the account's real home is
|
|
7
|
+
* refused with a named error before a byte changes (`liveState.ts` says why that cannot be left
|
|
8
|
+
* to each runner).
|
|
9
|
+
*
|
|
10
|
+
* This is the "own a control surface, or do not touch it" rule applied to the real home. The
|
|
11
|
+
* atomic writer used to exist three times (device identity, credential envelope, profile
|
|
12
|
+
* consolidation), so a guard added to one left two doors open. `stateFileBoundary.spec.ts`
|
|
13
|
+
* fails when a file other than this one starts calling a raw `fs` mutator, so a fourth copy
|
|
14
|
+
* cannot appear unguarded.
|
|
15
|
+
*
|
|
16
|
+
* Outside a test process every function here is exactly the `fs` call it wraps.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
/** `mkdir -p` for a state directory, 0700 unless `mode` says otherwise. */
|
|
20
|
+
export declare function ensureStateDir(dir: string, what: string, mode?: number | null): void;
|
|
21
|
+
/**
|
|
22
|
+
* Atomic 0600 write — tmp+fsync+rename+dir-fsync. The same shape the daemon and CLI always used,
|
|
23
|
+
* so a partial-write race between `skrr` and `skrrd` produces the same on-disk transitions.
|
|
24
|
+
*/
|
|
25
|
+
export declare function atomicWrite0600(target: string, value: Buffer | string, what: string): void;
|
|
26
|
+
/** `fs.writeFileSync` for a state file: lock sentinels, a re-auth latch, a machine identity. */
|
|
27
|
+
export declare function writeStateFile(target: string, data: string | NodeJS.ArrayBufferView, options: fs.WriteFileOptions, what: string): void;
|
|
28
|
+
/** `fs.unlinkSync` for a state file. Throws what `unlinkSync` throws, ENOENT included. */
|
|
29
|
+
export declare function removeStateFile(target: string, what: string): void;
|
|
30
|
+
/** `fs.renameSync` between state paths; both ends are checked. */
|
|
31
|
+
export declare function renameStateFile(from: string, to: string, what: string): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stateFiles.ts — the ONE place auth-core mutates credential state on disk.
|
|
3
|
+
*
|
|
4
|
+
* Every write, delete, rename and mkdir of a wrapped DEK, a device key, an enrollment marker, a
|
|
5
|
+
* lock sentinel, a re-auth latch or a machine identity goes through a function here, and each
|
|
6
|
+
* asks `assertMayMutateLiveState` first: a test process aimed at the account's real home is
|
|
7
|
+
* refused with a named error before a byte changes (`liveState.ts` says why that cannot be left
|
|
8
|
+
* to each runner).
|
|
9
|
+
*
|
|
10
|
+
* This is the "own a control surface, or do not touch it" rule applied to the real home. The
|
|
11
|
+
* atomic writer used to exist three times (device identity, credential envelope, profile
|
|
12
|
+
* consolidation), so a guard added to one left two doors open. `stateFileBoundary.spec.ts`
|
|
13
|
+
* fails when a file other than this one starts calling a raw `fs` mutator, so a fourth copy
|
|
14
|
+
* cannot appear unguarded.
|
|
15
|
+
*
|
|
16
|
+
* Outside a test process every function here is exactly the `fs` call it wraps.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { assertMayMutateLiveState } from './liveState.js';
|
|
21
|
+
/** `mkdir -p` for a state directory, 0700 unless `mode` says otherwise. */
|
|
22
|
+
export function ensureStateDir(dir, what, mode = 0o700) {
|
|
23
|
+
assertMayMutateLiveState(dir, what);
|
|
24
|
+
fs.mkdirSync(dir, mode === null ? { recursive: true } : { recursive: true, mode });
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Atomic 0600 write — tmp+fsync+rename+dir-fsync. The same shape the daemon and CLI always used,
|
|
28
|
+
* so a partial-write race between `skrr` and `skrrd` produces the same on-disk transitions.
|
|
29
|
+
*/
|
|
30
|
+
export function atomicWrite0600(target, value, what) {
|
|
31
|
+
assertMayMutateLiveState(target, what);
|
|
32
|
+
const dir = path.dirname(target);
|
|
33
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
34
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
35
|
+
const buf = typeof value === 'string' ? Buffer.from(value, 'utf-8') : value;
|
|
36
|
+
const fd = fs.openSync(tmp, 'w', 0o600);
|
|
37
|
+
try {
|
|
38
|
+
fs.writeSync(fd, buf);
|
|
39
|
+
fs.fsyncSync(fd);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
fs.closeSync(fd);
|
|
43
|
+
}
|
|
44
|
+
fs.renameSync(tmp, target);
|
|
45
|
+
try {
|
|
46
|
+
const dirFd = fs.openSync(dir, 'r');
|
|
47
|
+
try {
|
|
48
|
+
fs.fsyncSync(dirFd);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
fs.closeSync(dirFd);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* best-effort — Windows / some BSDs reject dir fsync */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** `fs.writeFileSync` for a state file: lock sentinels, a re-auth latch, a machine identity. */
|
|
59
|
+
export function writeStateFile(target, data, options, what) {
|
|
60
|
+
assertMayMutateLiveState(target, what);
|
|
61
|
+
fs.writeFileSync(target, data, options);
|
|
62
|
+
}
|
|
63
|
+
/** `fs.unlinkSync` for a state file. Throws what `unlinkSync` throws, ENOENT included. */
|
|
64
|
+
export function removeStateFile(target, what) {
|
|
65
|
+
assertMayMutateLiveState(target, what);
|
|
66
|
+
fs.unlinkSync(target);
|
|
67
|
+
}
|
|
68
|
+
/** `fs.renameSync` between state paths; both ends are checked. */
|
|
69
|
+
export function renameStateFile(from, to, what) {
|
|
70
|
+
assertMayMutateLiveState(from, what);
|
|
71
|
+
assertMayMutateLiveState(to, what);
|
|
72
|
+
fs.renameSync(from, to);
|
|
73
|
+
}
|
|
@@ -61,6 +61,9 @@ export declare const PREVIEW_TOKEN_REDACTION: "pvw_[redacted]";
|
|
|
61
61
|
export declare const QR_SIGN_IN_REDACTION: "[redacted]";
|
|
62
62
|
/** Fields are protocol-specific; preserve ordinary request ids, error codes and metrics. */
|
|
63
63
|
export declare function isQrSignInSecretField(key: string): boolean;
|
|
64
|
+
export declare function isOAuthQuerySecretField(key: string): boolean;
|
|
65
|
+
export declare function isOAuthQueryContainerField(key: string): boolean;
|
|
66
|
+
export declare function isGraphHandoffCookieField(key: string): boolean;
|
|
64
67
|
/**
|
|
65
68
|
* Replace every credential that travels in a URL with a marker: a Hosted Machine preview token with
|
|
66
69
|
* {@link PREVIEW_TOKEN_REDACTION}, and a workspace invitation, platform invitation, member link or operator code with
|
|
@@ -107,6 +107,21 @@ export function isQrSignInSecretField(key) {
|
|
|
107
107
|
return QR_SECRET_FIELDS.has(leaf.replace(/[_-]/g, '').toLowerCase());
|
|
108
108
|
}
|
|
109
109
|
const QR_SECRET_ASSIGNMENT = /((?:["']?)(?:statusToken|claimToken|codeVerifier|codeChallenge|comparisonCode|status_token|claim_token|code_verifier|code_challenge|comparison_code)(?:["']?)\s*[:=]\s*["']?)([A-Za-z0-9._~%+-]+)/gi;
|
|
110
|
+
// OAuth callback queries may be exported detached from their URL. Redact
|
|
111
|
+
// sensitive parameter values even without path context, including when the
|
|
112
|
+
// query separators were percent-encoded by an enclosing URL.
|
|
113
|
+
const OAUTH_QUERY_SECRET = /(^|[?&]|%(?:25)*26)(code|state|error_description|handoff)(=|%(?:25)*3[dD])([^&#\s"'<>]*)/gi;
|
|
114
|
+
export function isOAuthQuerySecretField(key) {
|
|
115
|
+
return ['code', 'state', 'error_description', 'handoff'].includes(key.toLowerCase());
|
|
116
|
+
}
|
|
117
|
+
export function isOAuthQueryContainerField(key) {
|
|
118
|
+
const leaf = (key.split('.').pop() || '').replace(/[_-]/g, '').toLowerCase();
|
|
119
|
+
return leaf === 'query' || leaf === 'querystring';
|
|
120
|
+
}
|
|
121
|
+
export function isGraphHandoffCookieField(key) {
|
|
122
|
+
return key.toLowerCase() === '__secure-pc-graph-handoff';
|
|
123
|
+
}
|
|
124
|
+
const GRAPH_HANDOFF_COOKIE = /((?:_|%(?:25)*5[fF]){2}secure(?:-|%(?:25)*2[dD])pc(?:-|%(?:25)*2[dD])graph(?:-|%(?:25)*2[dD])handoff(?:=|%(?:25)*3[dD]))([^;,&\s"'<>]+)/gi;
|
|
110
125
|
function redactQrSignInText(value) {
|
|
111
126
|
// Undici/Sentry may split url.query/query_string from its path. The QR's
|
|
112
127
|
// direction vocabulary identifies that standalone query without treating an
|
|
@@ -124,7 +139,8 @@ const credentialMarker = (_match, prefix) => `${prefix}_[redacted]`;
|
|
|
124
139
|
export function redactPreviewTokens(value) {
|
|
125
140
|
if (typeof value !== 'string')
|
|
126
141
|
return value;
|
|
127
|
-
const qrSafe = redactQrSignInText(value)
|
|
142
|
+
const qrSafe = redactQrSignInText(value).replace(OAUTH_QUERY_SECRET, '$1$2$3[redacted]')
|
|
143
|
+
.replace(GRAPH_HANDOFF_COOKIE, '$1[redacted]');
|
|
128
144
|
if (!CREDENTIAL_HINTS.some((hint) => qrSafe.indexOf(hint) !== -1))
|
|
129
145
|
return qrSafe;
|
|
130
146
|
return qrSafe
|
|
@@ -153,7 +169,7 @@ export function redactPreviewTokensDeep(root, limits = {}) {
|
|
|
153
169
|
let visited = 0;
|
|
154
170
|
let complete = true;
|
|
155
171
|
const rewrite = (holder, key, current) => {
|
|
156
|
-
const next = isQrSignInSecretField(key) ? QR_SIGN_IN_REDACTION : redactPreviewTokens(current);
|
|
172
|
+
const next = isQrSignInSecretField(key) || isGraphHandoffCookieField(key) ? QR_SIGN_IN_REDACTION : redactPreviewTokens(current);
|
|
157
173
|
if (next === current) {
|
|
158
174
|
return;
|
|
159
175
|
}
|
|
@@ -195,7 +211,28 @@ export function redactPreviewTokensDeep(root, limits = {}) {
|
|
|
195
211
|
complete = false;
|
|
196
212
|
continue;
|
|
197
213
|
}
|
|
198
|
-
if (
|
|
214
|
+
if (isOAuthQueryContainerField(key) && Array.isArray(child)) {
|
|
215
|
+
try {
|
|
216
|
+
holder[key] = '[redacted]';
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
complete = false;
|
|
220
|
+
}
|
|
221
|
+
if (holder[key] !== '[redacted]')
|
|
222
|
+
complete = false;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (isOAuthQueryContainerField(key) && child && typeof child === 'object' && !Array.isArray(child)) {
|
|
226
|
+
const query = child;
|
|
227
|
+
for (const queryKey of Object.keys(query)) {
|
|
228
|
+
if (isOAuthQuerySecretField(queryKey)) {
|
|
229
|
+
query[queryKey] = '[redacted]';
|
|
230
|
+
if (query[queryKey] !== '[redacted]')
|
|
231
|
+
complete = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (isQrSignInSecretField(key) || isGraphHandoffCookieField(key) || typeof child === 'string') {
|
|
199
236
|
rewrite(holder, key, child);
|
|
200
237
|
}
|
|
201
238
|
else if (child !== null && typeof child === 'object') {
|
|
@@ -67,6 +67,12 @@ export interface TokenPair {
|
|
|
67
67
|
* (server code `DEVICE_PROOF_THUMBPRINT_MISMATCH`).
|
|
68
68
|
* The daemon signs every retry with the same key, so
|
|
69
69
|
* retrying can never succeed.
|
|
70
|
+
* - DEVICE_KEY_UNAVAILABLE — the server demanded a device proof
|
|
71
|
+
* (`DEVICE_PROOF_MISSING`) and this process had none
|
|
72
|
+
* to send: the key is unreadable, never created, or no
|
|
73
|
+
* signer is wired. Unlike `DEVICE_KEY_MISMATCH` there is
|
|
74
|
+
* no second key to reconcile; the fix is an interactive
|
|
75
|
+
* daemon sign-in, which mints and re-pins a new one.
|
|
70
76
|
* - UNKNOWN_401 — server returned 401 with a code we don't
|
|
71
77
|
* recognize. Treated as permanent (better to prompt
|
|
72
78
|
* re-auth than to loop forever) but tagged
|
|
@@ -79,7 +85,7 @@ export interface TokenPair {
|
|
|
79
85
|
* Without this, the client `describeAuthFailure` exhaustiveness check
|
|
80
86
|
* was silently broken for the non-REFRESH branches.
|
|
81
87
|
*/
|
|
82
|
-
export type PermanentAuthReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'UNKNOWN_401';
|
|
88
|
+
export type PermanentAuthReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'DEVICE_KEY_MISMATCH' | 'DEVICE_KEY_UNAVAILABLE' | 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'UNKNOWN_401';
|
|
83
89
|
export declare class PermanentAuthFailure extends Error {
|
|
84
90
|
readonly code: PermanentAuthReason;
|
|
85
91
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skrr-ai/auth-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.33",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"types": "dist/esm/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -294,6 +294,26 @@
|
|
|
294
294
|
"default": "./dist/cjs/restartArtifact.js"
|
|
295
295
|
}
|
|
296
296
|
},
|
|
297
|
+
"./master-kek-service": {
|
|
298
|
+
"import": {
|
|
299
|
+
"types": "./dist/esm/kek/masterKekService.d.ts",
|
|
300
|
+
"default": "./dist/esm/kek/masterKekService.js"
|
|
301
|
+
},
|
|
302
|
+
"require": {
|
|
303
|
+
"types": "./dist/cjs/kek/masterKekService.d.ts",
|
|
304
|
+
"default": "./dist/cjs/kek/masterKekService.js"
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
"./live-state": {
|
|
308
|
+
"import": {
|
|
309
|
+
"types": "./dist/esm/liveState.d.ts",
|
|
310
|
+
"default": "./dist/esm/liveState.js"
|
|
311
|
+
},
|
|
312
|
+
"require": {
|
|
313
|
+
"types": "./dist/cjs/liveState.d.ts",
|
|
314
|
+
"default": "./dist/cjs/liveState.js"
|
|
315
|
+
}
|
|
316
|
+
},
|
|
297
317
|
"./daemon-consent-proof": {
|
|
298
318
|
"import": {
|
|
299
319
|
"types": "./dist/esm/daemonConsentProof.d.ts",
|