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