@skrr-ai/auth-core 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/cjs/aead.d.ts +24 -0
- package/dist/cjs/aead.js +65 -0
- package/dist/cjs/authExpiredDetector.d.ts +65 -0
- package/dist/cjs/authExpiredDetector.js +89 -0
- package/dist/cjs/authHelper.d.ts +51 -0
- package/dist/cjs/authHelper.js +321 -0
- package/dist/cjs/credentialEnvelope.d.ts +85 -0
- package/dist/cjs/credentialEnvelope.js +230 -0
- package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/cjs/credentialEnvelopeBridge.js +637 -0
- package/dist/cjs/credentialResolver.d.ts +278 -0
- package/dist/cjs/credentialResolver.js +423 -0
- package/dist/cjs/daemonPair.d.ts +172 -0
- package/dist/cjs/daemonPair.js +495 -0
- package/dist/cjs/daemonScopes.d.ts +74 -0
- package/dist/cjs/daemonScopes.js +84 -0
- package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
- package/dist/cjs/deviceIdentityBridge.js +591 -0
- package/dist/cjs/deviceKey.d.ts +167 -0
- package/dist/cjs/deviceKey.js +332 -0
- package/dist/cjs/fdToken.d.ts +17 -0
- package/dist/cjs/fdToken.js +238 -0
- package/dist/cjs/harnessTrust.d.ts +72 -0
- package/dist/cjs/harnessTrust.js +103 -0
- package/dist/cjs/headers-helper.d.ts +25 -0
- package/dist/cjs/headers-helper.js +323 -0
- package/dist/cjs/index.d.ts +37 -0
- package/dist/cjs/index.js +305 -0
- package/dist/cjs/jwtUtils.d.ts +35 -0
- package/dist/cjs/jwtUtils.js +61 -0
- package/dist/cjs/kek/index.d.ts +89 -0
- package/dist/cjs/kek/index.js +208 -0
- package/dist/cjs/kek/linux.d.ts +92 -0
- package/dist/cjs/kek/linux.js +596 -0
- package/dist/cjs/kek/macos.d.ts +112 -0
- package/dist/cjs/kek/macos.js +599 -0
- package/dist/cjs/kek/types.d.ts +111 -0
- package/dist/cjs/kek/types.js +43 -0
- package/dist/cjs/kek/windows.d.ts +71 -0
- package/dist/cjs/kek/windows.js +350 -0
- package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
- package/dist/cjs/kek/zeroize-registry.js +63 -0
- package/dist/cjs/legacyStatePreflight.d.ts +67 -0
- package/dist/cjs/legacyStatePreflight.js +78 -0
- package/dist/cjs/localIdentity.d.ts +54 -0
- package/dist/cjs/localIdentity.js +57 -0
- package/dist/cjs/loginLocalhost.d.ts +115 -0
- package/dist/cjs/loginLocalhost.js +368 -0
- package/dist/cjs/loginWithLocalhost.d.ts +131 -0
- package/dist/cjs/loginWithLocalhost.js +359 -0
- package/dist/cjs/machineId.d.ts +35 -0
- package/dist/cjs/machineId.js +239 -0
- package/dist/cjs/messages.d.ts +9 -0
- package/dist/cjs/messages.js +44 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pkce.d.ts +62 -0
- package/dist/cjs/pkce.js +158 -0
- package/dist/cjs/recoveryCode.d.ts +61 -0
- package/dist/cjs/recoveryCode.js +213 -0
- package/dist/cjs/refresh.d.ts +215 -0
- package/dist/cjs/refresh.js +877 -0
- package/dist/cjs/refreshClassification.d.ts +53 -0
- package/dist/cjs/refreshClassification.js +105 -0
- package/dist/cjs/refreshScheduler.d.ts +53 -0
- package/dist/cjs/refreshScheduler.js +332 -0
- package/dist/cjs/runtime.d.ts +149 -0
- package/dist/cjs/runtime.js +136 -0
- package/dist/cjs/spawnEnv.d.ts +76 -0
- package/dist/cjs/spawnEnv.js +153 -0
- package/dist/cjs/ttlParser.d.ts +54 -0
- package/dist/cjs/ttlParser.js +87 -0
- package/dist/cjs/types.d.ts +91 -0
- package/dist/cjs/types.js +26 -0
- package/dist/esm/aead.d.ts +24 -0
- package/dist/esm/aead.js +57 -0
- package/dist/esm/authExpiredDetector.d.ts +65 -0
- package/dist/esm/authExpiredDetector.js +85 -0
- package/dist/esm/authHelper.d.ts +51 -0
- package/dist/esm/authHelper.js +311 -0
- package/dist/esm/credentialEnvelope.d.ts +85 -0
- package/dist/esm/credentialEnvelope.js +213 -0
- package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/esm/credentialEnvelopeBridge.js +620 -0
- package/dist/esm/credentialResolver.d.ts +278 -0
- package/dist/esm/credentialResolver.js +414 -0
- package/dist/esm/daemonPair.d.ts +172 -0
- package/dist/esm/daemonPair.js +476 -0
- package/dist/esm/daemonScopes.d.ts +74 -0
- package/dist/esm/daemonScopes.js +80 -0
- package/dist/esm/deviceIdentityBridge.d.ts +113 -0
- package/dist/esm/deviceIdentityBridge.js +572 -0
- package/dist/esm/deviceKey.d.ts +167 -0
- package/dist/esm/deviceKey.js +320 -0
- package/dist/esm/fdToken.d.ts +17 -0
- package/dist/esm/fdToken.js +231 -0
- package/dist/esm/harnessTrust.d.ts +72 -0
- package/dist/esm/harnessTrust.js +97 -0
- package/dist/esm/headers-helper.d.ts +25 -0
- package/dist/esm/headers-helper.js +316 -0
- package/dist/esm/index.d.ts +37 -0
- package/dist/esm/index.js +144 -0
- package/dist/esm/jwtUtils.d.ts +35 -0
- package/dist/esm/jwtUtils.js +57 -0
- package/dist/esm/kek/index.d.ts +89 -0
- package/dist/esm/kek/index.js +162 -0
- package/dist/esm/kek/linux.d.ts +92 -0
- package/dist/esm/kek/linux.js +585 -0
- package/dist/esm/kek/macos.d.ts +112 -0
- package/dist/esm/kek/macos.js +586 -0
- package/dist/esm/kek/types.d.ts +111 -0
- package/dist/esm/kek/types.js +39 -0
- package/dist/esm/kek/windows.d.ts +71 -0
- package/dist/esm/kek/windows.js +344 -0
- package/dist/esm/kek/zeroize-registry.d.ts +44 -0
- package/dist/esm/kek/zeroize-registry.js +58 -0
- package/dist/esm/legacyStatePreflight.d.ts +67 -0
- package/dist/esm/legacyStatePreflight.js +71 -0
- package/dist/esm/localIdentity.d.ts +54 -0
- package/dist/esm/localIdentity.js +54 -0
- package/dist/esm/loginLocalhost.d.ts +115 -0
- package/dist/esm/loginLocalhost.js +355 -0
- package/dist/esm/loginWithLocalhost.d.ts +131 -0
- package/dist/esm/loginWithLocalhost.js +353 -0
- package/dist/esm/machineId.d.ts +35 -0
- package/dist/esm/machineId.js +231 -0
- package/dist/esm/messages.d.ts +9 -0
- package/dist/esm/messages.js +40 -0
- package/dist/esm/pkce.d.ts +62 -0
- package/dist/esm/pkce.js +148 -0
- package/dist/esm/recoveryCode.d.ts +61 -0
- package/dist/esm/recoveryCode.js +207 -0
- package/dist/esm/refresh.d.ts +215 -0
- package/dist/esm/refresh.js +863 -0
- package/dist/esm/refreshClassification.d.ts +53 -0
- package/dist/esm/refreshClassification.js +106 -0
- package/dist/esm/refreshScheduler.d.ts +53 -0
- package/dist/esm/refreshScheduler.js +329 -0
- package/dist/esm/runtime.d.ts +149 -0
- package/dist/esm/runtime.js +121 -0
- package/dist/esm/spawnEnv.d.ts +76 -0
- package/dist/esm/spawnEnv.js +149 -0
- package/dist/esm/ttlParser.d.ts +54 -0
- package/dist/esm/ttlParser.js +83 -0
- package/dist/esm/types.d.ts +91 -0
- package/dist/esm/types.js +21 -0
- package/package.json +82 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export interface AuthLogger {
|
|
2
|
+
trace?: (...args: unknown[]) => void;
|
|
3
|
+
debug: (...args: unknown[]) => void;
|
|
4
|
+
info: (...args: unknown[]) => void;
|
|
5
|
+
warn: (...args: unknown[]) => void;
|
|
6
|
+
error: (...args: unknown[]) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Kill switches that gate auth behavior. Mirrors the surface of the daemon's
|
|
10
|
+
* `KILL` object so the daemon can pass it through verbatim. Each flag defaults
|
|
11
|
+
* OFF — i.e., the new code path runs.
|
|
12
|
+
*/
|
|
13
|
+
export interface AuthKillSwitches {
|
|
14
|
+
/** Bypass the OVERSKY_AUTH_FD handoff and use file/keychain backend only. */
|
|
15
|
+
fdTokenHandoff?: boolean;
|
|
16
|
+
/** Disable the proactive refresh scheduler — fall back to ad-hoc 4401 refresh. */
|
|
17
|
+
refreshScheduler?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Minimal telemetry event shape passed to the consumer-supplied logEvent.
|
|
21
|
+
* Kept narrow so auth-core has no dependency on the daemon's full DaemonEvent.
|
|
22
|
+
*
|
|
23
|
+
* Known event names and their metadata payloads:
|
|
24
|
+
* refresh_start — {daemonId, sessionId?}
|
|
25
|
+
* refresh_success — {daemonId, durationMs, tokensRotated: boolean}
|
|
26
|
+
* refresh_failure — {daemonId, durationMs, errorCode, statusCode?}
|
|
27
|
+
* refresh_lock_acquiring — {daemonId}
|
|
28
|
+
* refresh_lock_acquired — {daemonId, waitedMs}
|
|
29
|
+
* refresh_lock_retry — {daemonId, attempt, errorCode}
|
|
30
|
+
* refresh_lock_released — {daemonId, heldForMs}
|
|
31
|
+
* refresh_race_resolved — {daemonId, sourceTokenAdopted: boolean}
|
|
32
|
+
*/
|
|
33
|
+
export interface AuthTelemetryEvent {
|
|
34
|
+
timestamp: string;
|
|
35
|
+
eventType: 'auth';
|
|
36
|
+
success: boolean;
|
|
37
|
+
metadata: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
export interface AuthCoreConfig {
|
|
40
|
+
/** Resolves the directory where auth state lives. Called per-access; may
|
|
41
|
+
* return different values across invocations (e.g. profile switching). */
|
|
42
|
+
configDir: () => string;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the MACHINE-scoped root directory — the location for identity
|
|
45
|
+
* that must be stable across profile switches and restarts (currently just
|
|
46
|
+
* `daemon.id`, via `machineId.ts`). Unlike `configDir`, this must NOT vary
|
|
47
|
+
* by profile: the daemon's `configDir` is `~/.skrr/profiles/<profile>`,
|
|
48
|
+
* so pinning the machine UUID to it made the "stable per-machine id" drift
|
|
49
|
+
* per-profile and silently killed the `legacyDaemonUuids()` fold safety net
|
|
50
|
+
* (it scanned `<profileDir>/profiles/*`, which never exists). See OSK-1478.
|
|
51
|
+
*
|
|
52
|
+
* Default: falls back to `configDir()` when unset. The CLI's `configDir` is
|
|
53
|
+
* already the machine root (`~/.skrr`), so it needs no wiring; the daemon
|
|
54
|
+
* wires this explicitly to its root `CONFIG_DIR` in `auth/configure.ts`.
|
|
55
|
+
*/
|
|
56
|
+
machineConfigDir?: () => string;
|
|
57
|
+
/** Logger used by all stateful helpers. */
|
|
58
|
+
logger: AuthLogger;
|
|
59
|
+
/** Kill switches; missing keys default to false (new path runs). */
|
|
60
|
+
killSwitches?: AuthKillSwitches;
|
|
61
|
+
/**
|
|
62
|
+
* Binary name used in user-facing reauth messages (e.g. "skrrd", "skrr").
|
|
63
|
+
* Default: "skrrd".
|
|
64
|
+
*/
|
|
65
|
+
binaryName?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Optional structured event sink. When supplied, auth-core emits telemetry
|
|
68
|
+
* events (refresh lifecycle, lock acquire/release, race resolution) into this
|
|
69
|
+
* callback. The callback must be fire-and-forget — auth-core never awaits it.
|
|
70
|
+
* Default: no-op (telemetry silently dropped).
|
|
71
|
+
*/
|
|
72
|
+
logEvent?: (event: AuthTelemetryEvent) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Optional response observer invoked once per HTTP response received from
|
|
75
|
+
* the OverSky backend (currently: refresh + revoke endpoints in `refresh.ts`).
|
|
76
|
+
*
|
|
77
|
+
* Intended for cross-cutting observation that does not branch the auth flow —
|
|
78
|
+
* e.g. the daemon wires `checkResponseClockSkew` here so the server's `Date:`
|
|
79
|
+
* header is sampled on every refresh round-trip. The observer must be
|
|
80
|
+
* synchronous, non-throwing, and free of side effects on the response itself
|
|
81
|
+
* (the body is consumed downstream).
|
|
82
|
+
*
|
|
83
|
+
* Auth-core wraps every invocation in try/catch, so a buggy observer cannot
|
|
84
|
+
* break refresh. Default: no-op.
|
|
85
|
+
*/
|
|
86
|
+
observeResponse?: (response: {
|
|
87
|
+
headers: Headers;
|
|
88
|
+
}) => void;
|
|
89
|
+
/**
|
|
90
|
+
* When true, `maybeEncryptForWrite` throws (fail-closed) whenever the
|
|
91
|
+
* envelope path is not active — callers must surface the failure rather
|
|
92
|
+
* than silently writing plaintext.
|
|
93
|
+
*
|
|
94
|
+
* Daemon still sets this explicitly to true in `auth/configure.ts` to
|
|
95
|
+
* keep the L12 contract pinned regardless of env-var state. Other
|
|
96
|
+
* consumers (CLI, Electron main) inherit the env-resolved default —
|
|
97
|
+
* see `getCredEnvelopeFailClosed()` below.
|
|
98
|
+
*
|
|
99
|
+
* Default after the 2026-04-28 audit M2 flip: env-resolved fail-closed
|
|
100
|
+
* (true) unless the operator explicitly sets `OVERSKY_KEK_REQUIRED=0`
|
|
101
|
+
* (or `=false`, case-insensitive) as an emergency rollback.
|
|
102
|
+
*
|
|
103
|
+
* Default: undefined (env-resolved; fail-closed unless overridden).
|
|
104
|
+
*/
|
|
105
|
+
credEnvelopeFailClosed?: boolean;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Wire up the auth-core runtime. Call once at process startup, before any
|
|
109
|
+
* other auth-core function. Subsequent calls replace the previous config —
|
|
110
|
+
* useful for tests but not intended for production code paths.
|
|
111
|
+
*/
|
|
112
|
+
export declare function configureAuthCore(config: Partial<AuthCoreConfig>): void;
|
|
113
|
+
export declare function getAuthLogger(): AuthLogger;
|
|
114
|
+
export declare function getAuthConfigDir(): string;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the machine-scoped root dir (see `machineConfigDir` on
|
|
117
|
+
* `AuthCoreConfig`). Falls back to the profile-scoped `configDir()` when no
|
|
118
|
+
* machine root is wired, preserving pre-OSK-1478 behavior for consumers whose
|
|
119
|
+
* `configDir` is already the machine root (e.g. the CLI).
|
|
120
|
+
*/
|
|
121
|
+
export declare function getAuthMachineConfigDir(): string;
|
|
122
|
+
export declare function getAuthKillSwitches(): AuthKillSwitches;
|
|
123
|
+
export declare function getAuthBinaryName(): string;
|
|
124
|
+
/**
|
|
125
|
+
* Resolve whether the credential envelope path should fail-closed on writes
|
|
126
|
+
* when not active. Honors the explicit `configureAuthCore({credEnvelopeFailClosed})`
|
|
127
|
+
* first; otherwise resolves from the env.
|
|
128
|
+
*
|
|
129
|
+
* Default after the 2026-04-28 audit M2 flip: fail-closed for everyone.
|
|
130
|
+
* Operators can opt back to fail-open via `OVERSKY_KEK_REQUIRED=0` (or
|
|
131
|
+
* `=false`, case-insensitive) as an emergency rollback only — it is NOT a
|
|
132
|
+
* feature flag. Any other value, including unset, means fail-closed.
|
|
133
|
+
*/
|
|
134
|
+
export declare function getCredEnvelopeFailClosed(): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Emit a telemetry event via the consumer-supplied logEvent callback.
|
|
137
|
+
* No-op when the callback is not configured. Never throws.
|
|
138
|
+
*/
|
|
139
|
+
export declare function emitAuthTelemetry(event: AuthTelemetryEvent): void;
|
|
140
|
+
/**
|
|
141
|
+
* Hand a freshly-received HTTP response to the consumer's `observeResponse`
|
|
142
|
+
* callback. No-op when the callback is not configured. Never throws — the
|
|
143
|
+
* caller (refresh.ts) must continue regardless of observer state.
|
|
144
|
+
*/
|
|
145
|
+
export declare function notifyResponseObserver(response: {
|
|
146
|
+
headers: Headers;
|
|
147
|
+
}): void;
|
|
148
|
+
/** Test-only: restore defaults. Not exported from the package barrel. */
|
|
149
|
+
export declare function __resetAuthCoreForTest(): void;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime.ts — dependency-injection seam for @skrr-ai/auth-core.
|
|
3
|
+
*
|
|
4
|
+
* The package never imports the daemon's `logger` or `config` directly. Each
|
|
5
|
+
* consumer (daemon, CLI, Electron main) calls `configureAuthCore()` once at
|
|
6
|
+
* startup with its own logger, config-dir resolver, and kill-switch source.
|
|
7
|
+
* Until configured, sensible no-op defaults apply so imports never crash and
|
|
8
|
+
* unit tests can exercise pure helpers without setup.
|
|
9
|
+
*
|
|
10
|
+
* Why DI not module-globals: the same auth substrate must serve binaries
|
|
11
|
+
* with different home dirs (`~/.skrr`, and historically `~/.skrr`), different log sinks
|
|
12
|
+
* (winston, console, oclif), and different rollback flags. A single shared
|
|
13
|
+
* runtime is fine; hard-coding either choice into the package would force
|
|
14
|
+
* every consumer onto the daemon's conventions.
|
|
15
|
+
*/
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { CONFIG_DIR_NAME, DEFAULT_BINARY_NAME } from './localIdentity.js';
|
|
19
|
+
const DEFAULT_LOGGER = {
|
|
20
|
+
debug: () => undefined,
|
|
21
|
+
info: () => undefined,
|
|
22
|
+
warn: (...args) => {
|
|
23
|
+
if (process.env.NODE_ENV !== 'test')
|
|
24
|
+
console.warn('[auth-core]', ...args);
|
|
25
|
+
},
|
|
26
|
+
error: (...args) => {
|
|
27
|
+
if (process.env.NODE_ENV !== 'test')
|
|
28
|
+
console.error('[auth-core]', ...args);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
const DEFAULT_CONFIG = {
|
|
32
|
+
configDir: () => path.join(os.homedir(), CONFIG_DIR_NAME),
|
|
33
|
+
logger: DEFAULT_LOGGER,
|
|
34
|
+
killSwitches: {},
|
|
35
|
+
binaryName: DEFAULT_BINARY_NAME,
|
|
36
|
+
};
|
|
37
|
+
let _config = DEFAULT_CONFIG;
|
|
38
|
+
/**
|
|
39
|
+
* Wire up the auth-core runtime. Call once at process startup, before any
|
|
40
|
+
* other auth-core function. Subsequent calls replace the previous config —
|
|
41
|
+
* useful for tests but not intended for production code paths.
|
|
42
|
+
*/
|
|
43
|
+
export function configureAuthCore(config) {
|
|
44
|
+
_config = {
|
|
45
|
+
...DEFAULT_CONFIG,
|
|
46
|
+
..._config,
|
|
47
|
+
...config,
|
|
48
|
+
killSwitches: { ...(_config.killSwitches ?? {}), ...(config.killSwitches ?? {}) },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function getAuthLogger() {
|
|
52
|
+
return _config.logger;
|
|
53
|
+
}
|
|
54
|
+
export function getAuthConfigDir() {
|
|
55
|
+
return _config.configDir();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the machine-scoped root dir (see `machineConfigDir` on
|
|
59
|
+
* `AuthCoreConfig`). Falls back to the profile-scoped `configDir()` when no
|
|
60
|
+
* machine root is wired, preserving pre-OSK-1478 behavior for consumers whose
|
|
61
|
+
* `configDir` is already the machine root (e.g. the CLI).
|
|
62
|
+
*/
|
|
63
|
+
export function getAuthMachineConfigDir() {
|
|
64
|
+
return (_config.machineConfigDir ?? _config.configDir)();
|
|
65
|
+
}
|
|
66
|
+
export function getAuthKillSwitches() {
|
|
67
|
+
return _config.killSwitches ?? {};
|
|
68
|
+
}
|
|
69
|
+
export function getAuthBinaryName() {
|
|
70
|
+
return _config.binaryName ?? DEFAULT_BINARY_NAME;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve whether the credential envelope path should fail-closed on writes
|
|
74
|
+
* when not active. Honors the explicit `configureAuthCore({credEnvelopeFailClosed})`
|
|
75
|
+
* first; otherwise resolves from the env.
|
|
76
|
+
*
|
|
77
|
+
* Default after the 2026-04-28 audit M2 flip: fail-closed for everyone.
|
|
78
|
+
* Operators can opt back to fail-open via `OVERSKY_KEK_REQUIRED=0` (or
|
|
79
|
+
* `=false`, case-insensitive) as an emergency rollback only — it is NOT a
|
|
80
|
+
* feature flag. Any other value, including unset, means fail-closed.
|
|
81
|
+
*/
|
|
82
|
+
export function getCredEnvelopeFailClosed() {
|
|
83
|
+
if (typeof _config.credEnvelopeFailClosed === 'boolean') {
|
|
84
|
+
return _config.credEnvelopeFailClosed;
|
|
85
|
+
}
|
|
86
|
+
// Default after 2026-04-28 audit M2 flip: fail-closed for everyone.
|
|
87
|
+
// Operators can opt back to fail-open via OVERSKY_KEK_REQUIRED=0
|
|
88
|
+
// (emergency rollback only). Any other value — including unset —
|
|
89
|
+
// means fail-closed.
|
|
90
|
+
const raw = (process.env.OVERSKY_KEK_REQUIRED ?? '').toLowerCase();
|
|
91
|
+
return raw !== '0' && raw !== 'false';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Emit a telemetry event via the consumer-supplied logEvent callback.
|
|
95
|
+
* No-op when the callback is not configured. Never throws.
|
|
96
|
+
*/
|
|
97
|
+
export function emitAuthTelemetry(event) {
|
|
98
|
+
try {
|
|
99
|
+
_config.logEvent?.(event);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* telemetry is best-effort — never let it crash auth paths */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Hand a freshly-received HTTP response to the consumer's `observeResponse`
|
|
107
|
+
* callback. No-op when the callback is not configured. Never throws — the
|
|
108
|
+
* caller (refresh.ts) must continue regardless of observer state.
|
|
109
|
+
*/
|
|
110
|
+
export function notifyResponseObserver(response) {
|
|
111
|
+
try {
|
|
112
|
+
_config.observeResponse?.(response);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* observation is best-effort — never let a buggy observer crash refresh */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Test-only: restore defaults. Not exported from the package barrel. */
|
|
119
|
+
export function __resetAuthCoreForTest() {
|
|
120
|
+
_config = DEFAULT_CONFIG;
|
|
121
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSK-3892 — spawn-env sanitization, shared by every OverSky process that
|
|
3
|
+
* spawns a model-driven child.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The daemon has stripped its own credentials out of `process.env` before every
|
|
8
|
+
* harness spawn since `daemon/src/env-sanitize.ts` was written. `sky code` spawns
|
|
9
|
+
* THE SAME engine — `daemon/src/backends/acp.ts` and `cli/src/lib/sky-code.ts`
|
|
10
|
+
* resolve the same binary, from the same managed location — and had no
|
|
11
|
+
* equivalent, so `OVERSKY_TOKEN` (a cloud-reusable platform credential that
|
|
12
|
+
* authenticates every `/api/*` call as the human) entered the environment of a
|
|
13
|
+
* process that executes model-directed tools.
|
|
14
|
+
*
|
|
15
|
+
* The CLI cannot import `daemon/src`: separate packages, and the daemon already
|
|
16
|
+
* depends on the CLI, so the reverse edge would be a cycle. `auth-core` is the
|
|
17
|
+
* one package BOTH already resolve — it is a daemon dependency and is copied
|
|
18
|
+
* verbatim into the published CLI by `cli/scripts/vendor-monorepo-deps.js` — so
|
|
19
|
+
* this is the same move `harnessTrust.ts` made, for the same reason, with no
|
|
20
|
+
* build or release wiring to change.
|
|
21
|
+
*
|
|
22
|
+
* ── What deliberately did NOT move ────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* `filterCustomEnv` and the `BLOCKED_CUSTOM_ENV_*` constants stay in the daemon.
|
|
25
|
+
* They govern SERVER-PROVIDED `customEnv` — a different threat model (stopping
|
|
26
|
+
* THEIR values coming IN and overriding the sandbox, rather than stopping OUR
|
|
27
|
+
* secrets leaking OUT) and a daemon-only concern: no other process is handed an
|
|
28
|
+
* env map by the control plane.
|
|
29
|
+
*
|
|
30
|
+
* ── The one asymmetry a consumer must decide for itself ───────────────────
|
|
31
|
+
*
|
|
32
|
+
* This is a DENY-LIST over the parent's own environment, and it strips provider
|
|
33
|
+
* credentials because the daemon delivers a child's model credential explicitly
|
|
34
|
+
* (`cred.env`, after the trust gate) and never by ambient inheritance. A parent
|
|
35
|
+
* whose supported mode IS ambient inheritance — `sky code` running BYOK, where
|
|
36
|
+
* the user's own key is the credential the run is meant to spend — has to
|
|
37
|
+
* re-admit that credential deliberately after this filter. See
|
|
38
|
+
* `cli/src/lib/sky-code.ts` (`engineSpawnEnv`). Do not soften the list here to
|
|
39
|
+
* serve that case: a deny-list with a hole is not a deny-list.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Env vars that must NEVER reach a spawned subprocess.
|
|
43
|
+
*
|
|
44
|
+
* A malicious or compromised cloud-issued prompt that achieves command
|
|
45
|
+
* execution should NOT also exfiltrate every credential the parent has
|
|
46
|
+
* in its own env. We use a deny-list (strip these, keep everything
|
|
47
|
+
* else) because real tools legitimately need PATH, HOME, USER, LANG,
|
|
48
|
+
* etc.
|
|
49
|
+
*
|
|
50
|
+
* Categories:
|
|
51
|
+
* - Daemon/Claude/Anthropic auth — direct account takeover.
|
|
52
|
+
* - AWS — STS / IAM credentials, full cloud access.
|
|
53
|
+
* - GitHub / npm / OpenAI — package-publish, repo, model access.
|
|
54
|
+
* - OverSky internal secrets — match by prefix.
|
|
55
|
+
* - DYLD/LD preload — library-injection vector for in-process compromise.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SENSITIVE_ENV_VARS: ReadonlySet<string>;
|
|
58
|
+
/** Variable name prefixes that should be stripped (catches families like AWS_*). */
|
|
59
|
+
export declare const SENSITIVE_ENV_PREFIXES: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* Credential-shaped suffixes are also forbidden. A fixed provider list is
|
|
62
|
+
* never complete: a newly configured provider would otherwise leak its key
|
|
63
|
+
* into every harness until an engineer remembers to edit this file. The
|
|
64
|
+
* parent's own credentials must be delivered through a backend's explicit
|
|
65
|
+
* per-request allow-list, never by ambient inheritance.
|
|
66
|
+
*/
|
|
67
|
+
export declare const SENSITIVE_ENV_SUFFIXES: readonly string[];
|
|
68
|
+
/** Specific OVERSKY_ vars to keep (the prefix sweep is broad on purpose). */
|
|
69
|
+
export declare const SENSITIVE_ENV_PREFIX_EXCEPTIONS: ReadonlySet<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Build a sanitized snapshot of the parent's own env for a spawned
|
|
72
|
+
* child, stripping credentials and library-injection vectors while
|
|
73
|
+
* preserving everything else (PATH, HOME, USER, LANG, terminal vars,
|
|
74
|
+
* etc.) so legitimate tools keep working.
|
|
75
|
+
*/
|
|
76
|
+
export declare function sanitizeSpawnEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSK-3892 — spawn-env sanitization, shared by every OverSky process that
|
|
3
|
+
* spawns a model-driven child.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this lives in auth-core ────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The daemon has stripped its own credentials out of `process.env` before every
|
|
8
|
+
* harness spawn since `daemon/src/env-sanitize.ts` was written. `sky code` spawns
|
|
9
|
+
* THE SAME engine — `daemon/src/backends/acp.ts` and `cli/src/lib/sky-code.ts`
|
|
10
|
+
* resolve the same binary, from the same managed location — and had no
|
|
11
|
+
* equivalent, so `OVERSKY_TOKEN` (a cloud-reusable platform credential that
|
|
12
|
+
* authenticates every `/api/*` call as the human) entered the environment of a
|
|
13
|
+
* process that executes model-directed tools.
|
|
14
|
+
*
|
|
15
|
+
* The CLI cannot import `daemon/src`: separate packages, and the daemon already
|
|
16
|
+
* depends on the CLI, so the reverse edge would be a cycle. `auth-core` is the
|
|
17
|
+
* one package BOTH already resolve — it is a daemon dependency and is copied
|
|
18
|
+
* verbatim into the published CLI by `cli/scripts/vendor-monorepo-deps.js` — so
|
|
19
|
+
* this is the same move `harnessTrust.ts` made, for the same reason, with no
|
|
20
|
+
* build or release wiring to change.
|
|
21
|
+
*
|
|
22
|
+
* ── What deliberately did NOT move ────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* `filterCustomEnv` and the `BLOCKED_CUSTOM_ENV_*` constants stay in the daemon.
|
|
25
|
+
* They govern SERVER-PROVIDED `customEnv` — a different threat model (stopping
|
|
26
|
+
* THEIR values coming IN and overriding the sandbox, rather than stopping OUR
|
|
27
|
+
* secrets leaking OUT) and a daemon-only concern: no other process is handed an
|
|
28
|
+
* env map by the control plane.
|
|
29
|
+
*
|
|
30
|
+
* ── The one asymmetry a consumer must decide for itself ───────────────────
|
|
31
|
+
*
|
|
32
|
+
* This is a DENY-LIST over the parent's own environment, and it strips provider
|
|
33
|
+
* credentials because the daemon delivers a child's model credential explicitly
|
|
34
|
+
* (`cred.env`, after the trust gate) and never by ambient inheritance. A parent
|
|
35
|
+
* whose supported mode IS ambient inheritance — `sky code` running BYOK, where
|
|
36
|
+
* the user's own key is the credential the run is meant to spend — has to
|
|
37
|
+
* re-admit that credential deliberately after this filter. See
|
|
38
|
+
* `cli/src/lib/sky-code.ts` (`engineSpawnEnv`). Do not soften the list here to
|
|
39
|
+
* serve that case: a deny-list with a hole is not a deny-list.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Env vars that must NEVER reach a spawned subprocess.
|
|
43
|
+
*
|
|
44
|
+
* A malicious or compromised cloud-issued prompt that achieves command
|
|
45
|
+
* execution should NOT also exfiltrate every credential the parent has
|
|
46
|
+
* in its own env. We use a deny-list (strip these, keep everything
|
|
47
|
+
* else) because real tools legitimately need PATH, HOME, USER, LANG,
|
|
48
|
+
* etc.
|
|
49
|
+
*
|
|
50
|
+
* Categories:
|
|
51
|
+
* - Daemon/Claude/Anthropic auth — direct account takeover.
|
|
52
|
+
* - AWS — STS / IAM credentials, full cloud access.
|
|
53
|
+
* - GitHub / npm / OpenAI — package-publish, repo, model access.
|
|
54
|
+
* - OverSky internal secrets — match by prefix.
|
|
55
|
+
* - DYLD/LD preload — library-injection vector for in-process compromise.
|
|
56
|
+
*/
|
|
57
|
+
export const SENSITIVE_ENV_VARS = new Set([
|
|
58
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
59
|
+
'OVERSKY_TOKEN',
|
|
60
|
+
'AWS_ACCESS_KEY_ID',
|
|
61
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
62
|
+
'AWS_SESSION_TOKEN',
|
|
63
|
+
'GH_TOKEN',
|
|
64
|
+
'GITHUB_TOKEN',
|
|
65
|
+
'NPM_TOKEN',
|
|
66
|
+
'OPENAI_API_KEY',
|
|
67
|
+
'ANTHROPIC_API_KEY',
|
|
68
|
+
// Provider keys are parent-process credentials just as much as the two
|
|
69
|
+
// original OpenAI/Anthropic entries. A child must receive one only through
|
|
70
|
+
// its backend's per-request, trust-gated credential contract; inheriting an
|
|
71
|
+
// operator export turns a compromised harness into a provider-account
|
|
72
|
+
// takeover. Keep this explicit rather than guessing from a suffix: ordinary
|
|
73
|
+
// application variables sometimes end in `_KEY` and are not credentials.
|
|
74
|
+
'OPENROUTER_API_KEY',
|
|
75
|
+
'GEMINI_API_KEY',
|
|
76
|
+
'GOOGLE_API_KEY',
|
|
77
|
+
'MOONSHOT_API_KEY',
|
|
78
|
+
'AZURE_OPENAI_API_KEY',
|
|
79
|
+
'COHERE_API_KEY',
|
|
80
|
+
'DEEPSEEK_API_KEY',
|
|
81
|
+
'MISTRAL_API_KEY',
|
|
82
|
+
'PERPLEXITY_API_KEY',
|
|
83
|
+
'XAI_API_KEY',
|
|
84
|
+
'HUGGINGFACE_HUB_TOKEN',
|
|
85
|
+
'LITELLM_MASTER_KEY',
|
|
86
|
+
'DYLD_INSERT_LIBRARIES',
|
|
87
|
+
'LD_PRELOAD',
|
|
88
|
+
]);
|
|
89
|
+
/** Variable name prefixes that should be stripped (catches families like AWS_*). */
|
|
90
|
+
export const SENSITIVE_ENV_PREFIXES = ['AWS_', 'OVERSKY_'];
|
|
91
|
+
/**
|
|
92
|
+
* Credential-shaped suffixes are also forbidden. A fixed provider list is
|
|
93
|
+
* never complete: a newly configured provider would otherwise leak its key
|
|
94
|
+
* into every harness until an engineer remembers to edit this file. The
|
|
95
|
+
* parent's own credentials must be delivered through a backend's explicit
|
|
96
|
+
* per-request allow-list, never by ambient inheritance.
|
|
97
|
+
*/
|
|
98
|
+
export const SENSITIVE_ENV_SUFFIXES = [
|
|
99
|
+
'_API_KEY',
|
|
100
|
+
'_API_TOKEN',
|
|
101
|
+
'_AUTH_TOKEN',
|
|
102
|
+
'_ACCESS_TOKEN',
|
|
103
|
+
'_REFRESH_TOKEN',
|
|
104
|
+
'_SECRET',
|
|
105
|
+
'_SECRET_KEY',
|
|
106
|
+
'_PASSWORD',
|
|
107
|
+
'_CREDENTIALS',
|
|
108
|
+
];
|
|
109
|
+
/** Specific OVERSKY_ vars to keep (the prefix sweep is broad on purpose). */
|
|
110
|
+
export const SENSITIVE_ENV_PREFIX_EXCEPTIONS = new Set([
|
|
111
|
+
// Headless flag is read by spawned tools to know they're inside the daemon —
|
|
112
|
+
// not a secret. Add more here if a tool legitimately needs an OVERSKY_ var.
|
|
113
|
+
'OVERSKY_HEADLESS',
|
|
114
|
+
'OVERSKY_LAUNCHD_LABEL',
|
|
115
|
+
// OSK-1108 — canonical supervisor marker; spawned tools read it to
|
|
116
|
+
// know they're inside a supervised daemon on Linux/Windows in addition
|
|
117
|
+
// to macOS launchd. Not a secret.
|
|
118
|
+
'OVERSKY_SUPERVISOR',
|
|
119
|
+
]);
|
|
120
|
+
/**
|
|
121
|
+
* Build a sanitized snapshot of the parent's own env for a spawned
|
|
122
|
+
* child, stripping credentials and library-injection vectors while
|
|
123
|
+
* preserving everything else (PATH, HOME, USER, LANG, terminal vars,
|
|
124
|
+
* etc.) so legitimate tools keep working.
|
|
125
|
+
*/
|
|
126
|
+
export function sanitizeSpawnEnv(env) {
|
|
127
|
+
const out = {};
|
|
128
|
+
for (const [key, value] of Object.entries(env)) {
|
|
129
|
+
if (value === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
// Explicit deny wins unconditionally — even if key is in SENSITIVE_ENV_PREFIX_EXCEPTIONS.
|
|
132
|
+
if (SENSITIVE_ENV_VARS.has(key))
|
|
133
|
+
continue;
|
|
134
|
+
let stripped = false;
|
|
135
|
+
for (const prefix of SENSITIVE_ENV_PREFIXES) {
|
|
136
|
+
if (key.startsWith(prefix) && !SENSITIVE_ENV_PREFIX_EXCEPTIONS.has(key)) {
|
|
137
|
+
stripped = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!stripped && SENSITIVE_ENV_SUFFIXES.some((suffix) => key.endsWith(suffix))) {
|
|
142
|
+
stripped = true;
|
|
143
|
+
}
|
|
144
|
+
if (stripped)
|
|
145
|
+
continue;
|
|
146
|
+
out[key] = value;
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ttlParser.ts — shared TTL-duration grammar for CI-token lifetimes.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the `--ttl` flag accepted by both
|
|
5
|
+
* `oversky create-token` (daemon CLI) and `sky create-token` (CLI).
|
|
6
|
+
* Before this module the CLI accepted bare integers only while the
|
|
7
|
+
* daemon accepted `30d`/`1y`/`1w` — users would learn the daemon
|
|
8
|
+
* grammar, hit a cryptic oclif integer-parser error when they tried
|
|
9
|
+
* the same string on `sky`, and file a confused bug (ADD-CLI1).
|
|
10
|
+
*
|
|
11
|
+
* Grammar:
|
|
12
|
+
* <number><unit?>
|
|
13
|
+
* unit := 'd' | 'w' | 'y' (default 'd' if absent)
|
|
14
|
+
*
|
|
15
|
+
* Constraints:
|
|
16
|
+
* days ∈ [MIN_TTL_DAYS, MAX_TTL_DAYS] (1 .. 365)
|
|
17
|
+
*
|
|
18
|
+
* Errors are returned as a discriminated-union value rather than
|
|
19
|
+
* thrown, so each CLI can surface them with its own idioms
|
|
20
|
+
* (`this.error(...)` on oclif vs `showError(); process.exit(2)` on
|
|
21
|
+
* the daemon) without the parser owning process-exit semantics.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MIN_TTL_DAYS = 1;
|
|
24
|
+
export declare const MAX_TTL_DAYS = 365;
|
|
25
|
+
export interface ParseTtlSuccess {
|
|
26
|
+
ok: true;
|
|
27
|
+
days: number;
|
|
28
|
+
/** Raw input for error echoing by the caller if needed. */
|
|
29
|
+
input: string;
|
|
30
|
+
}
|
|
31
|
+
export interface ParseTtlError {
|
|
32
|
+
ok: false;
|
|
33
|
+
code: 'INVALID_FORMAT' | 'OUT_OF_RANGE';
|
|
34
|
+
message: string;
|
|
35
|
+
input: string;
|
|
36
|
+
}
|
|
37
|
+
export type ParseTtlResult = ParseTtlSuccess | ParseTtlError;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a `--ttl` string into a concrete day count. Pure; never
|
|
40
|
+
* throws. Accepts `undefined`/empty as "use default."
|
|
41
|
+
*
|
|
42
|
+
* Examples:
|
|
43
|
+
* parseTtlDays('30d') → { ok: true, days: 30 }
|
|
44
|
+
* parseTtlDays('1y') → { ok: true, days: 365 }
|
|
45
|
+
* parseTtlDays('2w') → { ok: true, days: 14 }
|
|
46
|
+
* parseTtlDays('90') → { ok: true, days: 90 }
|
|
47
|
+
* parseTtlDays('0') → { ok: false, code: 'OUT_OF_RANGE' }
|
|
48
|
+
* parseTtlDays('abc') → { ok: false, code: 'INVALID_FORMAT' }
|
|
49
|
+
* parseTtlDays('2y') → { ok: false, code: 'OUT_OF_RANGE' } // 730d > 365
|
|
50
|
+
*
|
|
51
|
+
* The default-days argument is a day count, not a string, because the
|
|
52
|
+
* caller already knows it's in range — no need to re-parse.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseTtlDays(input: string | undefined | null, defaultDays: number): ParseTtlResult;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ttlParser.ts — shared TTL-duration grammar for CI-token lifetimes.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the `--ttl` flag accepted by both
|
|
5
|
+
* `oversky create-token` (daemon CLI) and `sky create-token` (CLI).
|
|
6
|
+
* Before this module the CLI accepted bare integers only while the
|
|
7
|
+
* daemon accepted `30d`/`1y`/`1w` — users would learn the daemon
|
|
8
|
+
* grammar, hit a cryptic oclif integer-parser error when they tried
|
|
9
|
+
* the same string on `sky`, and file a confused bug (ADD-CLI1).
|
|
10
|
+
*
|
|
11
|
+
* Grammar:
|
|
12
|
+
* <number><unit?>
|
|
13
|
+
* unit := 'd' | 'w' | 'y' (default 'd' if absent)
|
|
14
|
+
*
|
|
15
|
+
* Constraints:
|
|
16
|
+
* days ∈ [MIN_TTL_DAYS, MAX_TTL_DAYS] (1 .. 365)
|
|
17
|
+
*
|
|
18
|
+
* Errors are returned as a discriminated-union value rather than
|
|
19
|
+
* thrown, so each CLI can surface them with its own idioms
|
|
20
|
+
* (`this.error(...)` on oclif vs `showError(); process.exit(2)` on
|
|
21
|
+
* the daemon) without the parser owning process-exit semantics.
|
|
22
|
+
*/
|
|
23
|
+
export const MIN_TTL_DAYS = 1;
|
|
24
|
+
export const MAX_TTL_DAYS = 365;
|
|
25
|
+
const TTL_RE = /^(\d+)\s*([dwy]?)$/i;
|
|
26
|
+
/**
|
|
27
|
+
* Parse a `--ttl` string into a concrete day count. Pure; never
|
|
28
|
+
* throws. Accepts `undefined`/empty as "use default."
|
|
29
|
+
*
|
|
30
|
+
* Examples:
|
|
31
|
+
* parseTtlDays('30d') → { ok: true, days: 30 }
|
|
32
|
+
* parseTtlDays('1y') → { ok: true, days: 365 }
|
|
33
|
+
* parseTtlDays('2w') → { ok: true, days: 14 }
|
|
34
|
+
* parseTtlDays('90') → { ok: true, days: 90 }
|
|
35
|
+
* parseTtlDays('0') → { ok: false, code: 'OUT_OF_RANGE' }
|
|
36
|
+
* parseTtlDays('abc') → { ok: false, code: 'INVALID_FORMAT' }
|
|
37
|
+
* parseTtlDays('2y') → { ok: false, code: 'OUT_OF_RANGE' } // 730d > 365
|
|
38
|
+
*
|
|
39
|
+
* The default-days argument is a day count, not a string, because the
|
|
40
|
+
* caller already knows it's in range — no need to re-parse.
|
|
41
|
+
*/
|
|
42
|
+
export function parseTtlDays(input, defaultDays) {
|
|
43
|
+
if (input == null || input === '') {
|
|
44
|
+
return { ok: true, days: defaultDays, input: '' };
|
|
45
|
+
}
|
|
46
|
+
const trimmed = String(input).trim();
|
|
47
|
+
const m = trimmed.match(TTL_RE);
|
|
48
|
+
if (!m) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
code: 'INVALID_FORMAT',
|
|
52
|
+
input: trimmed,
|
|
53
|
+
message: `Invalid --ttl value "${trimmed}". ` +
|
|
54
|
+
`Examples: 30d (30 days), 2w (14 days), 1y (365 days), 90 (treated as days).`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const n = parseInt(m[1], 10);
|
|
58
|
+
const unit = (m[2] || 'd').toLowerCase();
|
|
59
|
+
let days;
|
|
60
|
+
switch (unit) {
|
|
61
|
+
case 'd':
|
|
62
|
+
days = n;
|
|
63
|
+
break;
|
|
64
|
+
case 'w':
|
|
65
|
+
days = n * 7;
|
|
66
|
+
break;
|
|
67
|
+
case 'y':
|
|
68
|
+
days = n * 365;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
days = n;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (!Number.isFinite(days) || days < MIN_TTL_DAYS || days > MAX_TTL_DAYS) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
code: 'OUT_OF_RANGE',
|
|
78
|
+
input: trimmed,
|
|
79
|
+
message: `--ttl must be ${MIN_TTL_DAYS}–${MAX_TTL_DAYS} days (got ${days} days from "${trimmed}").`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, days, input: trimmed };
|
|
83
|
+
}
|