@skrr-ai/auth-core 0.1.2 → 0.1.4

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.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The skrr root — the single definition of where this machine keeps its skrr
3
+ * state.
4
+ *
5
+ * It lives here because BOTH binaries need the same answer and neither owns it.
6
+ * `skrr` and `skrrd` keep separate credentials on purpose, but they share one
7
+ * root, and every place that re-derived it became a place they could disagree.
8
+ * They did, twice in one day:
9
+ *
10
+ * - The CLI's engine path honoured `OVERSKY_CONFIG_DIR` while its config did
11
+ * not, so a relocated root moved the engine and left the credentials in
12
+ * `$HOME`. Fixed by teaching `config.ts` the variable — which promptly
13
+ * re-created the split in the mirror image, because `sky-code.ts` still read
14
+ * only the old name.
15
+ * - The daemon then had the same shape, and fixing the CLI's half turned it
16
+ * into a CLI/daemon divergence: with the variable set, the two halves of one
17
+ * product disagreed about where the credentials live.
18
+ *
19
+ * Each of those was a correct local fix that created the next defect, because
20
+ * the root was being DERIVED rather than READ. Teaching every site the same list
21
+ * of variable names only defers the problem to whenever the list changes again —
22
+ * which is exactly what `SKRR_CONFIG_DIR` did.
23
+ *
24
+ * So: one function, and a boundary test that fails if anything re-derives it.
25
+ *
26
+ * `SKRR_CONFIG_DIR` is the spelling for anything new (root `CLAUDE.md`) and wins
27
+ * when both are set. `OVERSKY_CONFIG_DIR` keeps working because existing installs,
28
+ * the engine resolver and the daemon all read it, and breaking a documented
29
+ * override to tidy a name would be a worse trade than carrying two.
30
+ *
31
+ * Takes `env` rather than reading the global so a caller can resolve a root for a
32
+ * child process it is about to spawn, and so tests need no process-wide mutation.
33
+ */
34
+ export declare function resolveConfigRoot(env?: NodeJS.ProcessEnv): string;
35
+ /** A path under the skrr root, e.g. `configRootPath(env, 'sky-code', 'bin')`. */
36
+ export declare function configRootPath(env: NodeJS.ProcessEnv, ...segments: string[]): string;
37
+ /**
38
+ * Has the operator explicitly relocated the root?
39
+ *
40
+ * A distinct question from "what is the root", and it has a real caller: the
41
+ * legacy-location fallbacks exist to find state left by an older layout under
42
+ * `$HOME`, and once someone has POINTED the root somewhere, guessing at
43
+ * `$HOME` is no longer a helpful fallback — it is a different machine's data.
44
+ *
45
+ * Shared for the same reason as `resolveConfigRoot`: this check was copied to
46
+ * three call sites, each spelling the variable list itself, so adding
47
+ * `SKRR_CONFIG_DIR` to the resolver silently left the copies answering the old
48
+ * question.
49
+ */
50
+ export declare function isConfigRootOverridden(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,67 @@
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.resolveConfigRoot = resolveConfigRoot;
7
+ exports.configRootPath = configRootPath;
8
+ exports.isConfigRootOverridden = isConfigRootOverridden;
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ /**
12
+ * The skrr root — the single definition of where this machine keeps its skrr
13
+ * state.
14
+ *
15
+ * It lives here because BOTH binaries need the same answer and neither owns it.
16
+ * `skrr` and `skrrd` keep separate credentials on purpose, but they share one
17
+ * root, and every place that re-derived it became a place they could disagree.
18
+ * They did, twice in one day:
19
+ *
20
+ * - The CLI's engine path honoured `OVERSKY_CONFIG_DIR` while its config did
21
+ * not, so a relocated root moved the engine and left the credentials in
22
+ * `$HOME`. Fixed by teaching `config.ts` the variable — which promptly
23
+ * re-created the split in the mirror image, because `sky-code.ts` still read
24
+ * only the old name.
25
+ * - The daemon then had the same shape, and fixing the CLI's half turned it
26
+ * into a CLI/daemon divergence: with the variable set, the two halves of one
27
+ * product disagreed about where the credentials live.
28
+ *
29
+ * Each of those was a correct local fix that created the next defect, because
30
+ * the root was being DERIVED rather than READ. Teaching every site the same list
31
+ * of variable names only defers the problem to whenever the list changes again —
32
+ * which is exactly what `SKRR_CONFIG_DIR` did.
33
+ *
34
+ * So: one function, and a boundary test that fails if anything re-derives it.
35
+ *
36
+ * `SKRR_CONFIG_DIR` is the spelling for anything new (root `CLAUDE.md`) and wins
37
+ * when both are set. `OVERSKY_CONFIG_DIR` keeps working because existing installs,
38
+ * the engine resolver and the daemon all read it, and breaking a documented
39
+ * override to tidy a name would be a worse trade than carrying two.
40
+ *
41
+ * Takes `env` rather than reading the global so a caller can resolve a root for a
42
+ * child process it is about to spawn, and so tests need no process-wide mutation.
43
+ */
44
+ function resolveConfigRoot(env = process.env) {
45
+ const override = (env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim();
46
+ return override || node_path_1.default.join((0, node_os_1.homedir)(), '.skrr');
47
+ }
48
+ /** A path under the skrr root, e.g. `configRootPath(env, 'sky-code', 'bin')`. */
49
+ function configRootPath(env, ...segments) {
50
+ return node_path_1.default.join(resolveConfigRoot(env), ...segments);
51
+ }
52
+ /**
53
+ * Has the operator explicitly relocated the root?
54
+ *
55
+ * A distinct question from "what is the root", and it has a real caller: the
56
+ * legacy-location fallbacks exist to find state left by an older layout under
57
+ * `$HOME`, and once someone has POINTED the root somewhere, guessing at
58
+ * `$HOME` is no longer a helpful fallback — it is a different machine's data.
59
+ *
60
+ * Shared for the same reason as `resolveConfigRoot`: this check was copied to
61
+ * three call sites, each spelling the variable list itself, so adding
62
+ * `SKRR_CONFIG_DIR` to the resolver silently left the copies answering the old
63
+ * question.
64
+ */
65
+ function isConfigRootOverridden(env = process.env) {
66
+ return Boolean((env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim());
67
+ }
@@ -215,7 +215,7 @@ export declare class AuthHelperUnavailableError extends Error {
215
215
  /**
216
216
  * L6.1 — typed error thrown when the workspace-trust gate blocks a
217
217
  * project- or local-scope helper. Surfaces the actionable next step
218
- * (`oversky trust accept`) to the user; the daemon catches and presents
218
+ * (`skrrd trust accept`) to the user; the daemon catches and presents
219
219
  * this without falling through to OAuth.
220
220
  */
221
221
  export declare class AuthHelperUntrustedError extends Error {
@@ -137,7 +137,7 @@ exports.AuthHelperUnavailableError = AuthHelperUnavailableError;
137
137
  /**
138
138
  * L6.1 — typed error thrown when the workspace-trust gate blocks a
139
139
  * project- or local-scope helper. Surfaces the actionable next step
140
- * (`oversky trust accept`) to the user; the daemon catches and presents
140
+ * (`skrrd trust accept`) to the user; the daemon catches and presents
141
141
  * this without falling through to OAuth.
142
142
  */
143
143
  class AuthHelperUntrustedError extends Error {
@@ -146,7 +146,7 @@ class AuthHelperUntrustedError extends Error {
146
146
  helperPath;
147
147
  constructor(helperPath, origin, reason) {
148
148
  super(`OVERSKY_AUTH_HELPER (${origin}-scope) is not trusted for this workspace ` +
149
- `(reason: ${reason}). Run \`oversky trust accept\` to allow it.`);
149
+ `(reason: ${reason}). Run \`skrrd trust accept\` to allow it.`);
150
150
  this.name = 'AuthHelperUntrustedError';
151
151
  this.origin = origin;
152
152
  this.reason = reason;
@@ -35,3 +35,5 @@ export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConf
35
35
  export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
36
36
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
37
37
  export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
38
+ export { DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveChannelManifestUrl, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, type SkyCodeChannel, } from './skyCodeChannels.js';
39
+ export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
package/dist/cjs/index.js CHANGED
@@ -11,7 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.challenge = exports.generateVerifier = exports.invalidateHeadersHelperCache = exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
12
12
  exports.tokenHash = exports.publicJwkFromX = exports.normalizeHtu = exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = void 0;
13
13
  exports.getHarnessTier = exports.HARNESS_TIERS = exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = exports.ensureMachineDaemonId = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDeviceProof = void 0;
14
- exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = void 0;
14
+ exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.SKY_CODE_FEED_BASE_ENV = exports.skyCodeFeedBase = exports.skyCodeChannelPrefix = exports.resolveSkyCodeChannel = exports.resolveChannelManifestUrl = exports.isSkyCodeChannel = exports.SKY_CODE_MANIFEST_FILE = exports.SKY_CODE_CHANNEL_ENV = exports.SKY_CODE_CHANNELS = exports.DEFAULT_SKY_CODE_FEED_BASE = exports.DEFAULT_SKY_CODE_CHANNEL = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = void 0;
15
15
  // Daemon scope vocabulary — single source of truth for non-Mongoose tiers
16
16
  // (daemon, desktop, cli). Schema-bound canonical lives in
17
17
  // `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
@@ -303,3 +303,21 @@ Object.defineProperty(exports, "DEFAULT_BINARY_NAME", { enumerable: true, get: f
303
303
  var legacyStatePreflight_js_1 = require("./legacyStatePreflight.js");
304
304
  Object.defineProperty(exports, "findLegacyLocalState", { enumerable: true, get: function () { return legacyStatePreflight_js_1.findLegacyLocalState; } });
305
305
  Object.defineProperty(exports, "describeLegacyState", { enumerable: true, get: function () { return legacyStatePreflight_js_1.describeLegacyState; } });
306
+ var skyCodeChannels_js_1 = require("./skyCodeChannels.js");
307
+ Object.defineProperty(exports, "DEFAULT_SKY_CODE_CHANNEL", { enumerable: true, get: function () { return skyCodeChannels_js_1.DEFAULT_SKY_CODE_CHANNEL; } });
308
+ Object.defineProperty(exports, "DEFAULT_SKY_CODE_FEED_BASE", { enumerable: true, get: function () { return skyCodeChannels_js_1.DEFAULT_SKY_CODE_FEED_BASE; } });
309
+ Object.defineProperty(exports, "SKY_CODE_CHANNELS", { enumerable: true, get: function () { return skyCodeChannels_js_1.SKY_CODE_CHANNELS; } });
310
+ Object.defineProperty(exports, "SKY_CODE_CHANNEL_ENV", { enumerable: true, get: function () { return skyCodeChannels_js_1.SKY_CODE_CHANNEL_ENV; } });
311
+ Object.defineProperty(exports, "SKY_CODE_MANIFEST_FILE", { enumerable: true, get: function () { return skyCodeChannels_js_1.SKY_CODE_MANIFEST_FILE; } });
312
+ Object.defineProperty(exports, "isSkyCodeChannel", { enumerable: true, get: function () { return skyCodeChannels_js_1.isSkyCodeChannel; } });
313
+ Object.defineProperty(exports, "resolveChannelManifestUrl", { enumerable: true, get: function () { return skyCodeChannels_js_1.resolveChannelManifestUrl; } });
314
+ Object.defineProperty(exports, "resolveSkyCodeChannel", { enumerable: true, get: function () { return skyCodeChannels_js_1.resolveSkyCodeChannel; } });
315
+ Object.defineProperty(exports, "skyCodeChannelPrefix", { enumerable: true, get: function () { return skyCodeChannels_js_1.skyCodeChannelPrefix; } });
316
+ Object.defineProperty(exports, "skyCodeFeedBase", { enumerable: true, get: function () { return skyCodeChannels_js_1.skyCodeFeedBase; } });
317
+ Object.defineProperty(exports, "SKY_CODE_FEED_BASE_ENV", { enumerable: true, get: function () { return skyCodeChannels_js_1.SKY_CODE_FEED_BASE_ENV; } });
318
+ // The one definition of the skrr root, shared by `skrr` and `skrrd`. See the
319
+ // module for why re-deriving it kept producing divergences.
320
+ var configRoot_js_1 = require("./configRoot.js");
321
+ Object.defineProperty(exports, "resolveConfigRoot", { enumerable: true, get: function () { return configRoot_js_1.resolveConfigRoot; } });
322
+ Object.defineProperty(exports, "configRootPath", { enumerable: true, get: function () { return configRoot_js_1.configRootPath; } });
323
+ Object.defineProperty(exports, "isConfigRootOverridden", { enumerable: true, get: function () { return configRoot_js_1.isConfigRootOverridden; } });
@@ -84,8 +84,21 @@ const DBUS_TRANSPORT_ERRORS = [
84
84
  'No such interface',
85
85
  'Cannot autolaunch',
86
86
  ];
87
- /** HKDF-SHA256 parameters for Tier-3. Stable across versions; bumping the
88
- * `v1` suffix would silently invalidate every wrapped DEK on disk. */
87
+ /**
88
+ * HKDF-SHA256 parameters for Tier-3. Stable across versions: ANY change
89
+ * silently invalidates every wrapped DEK on disk.
90
+ *
91
+ * That includes the brand. These strings are cryptographic derivation inputs
92
+ * that happen to read like a name, so a rename sweep produces exactly the same
93
+ * outcome as bumping the `v1` suffix — every credential on every machine
94
+ * becomes undecryptable, with no error at write time and a failure that
95
+ * surfaces only when someone next tries to unwrap. The KEK's SERVICE name moved
96
+ * to `ai.skrr.daemon.kek`; these did not, and must not.
97
+ *
98
+ * Rotating them deliberately is a re-issue, not a rename: it needs the old
99
+ * envelope discarded and credentials re-obtained, which is a decision with a
100
+ * user-visible cost rather than a string edit.
101
+ */
89
102
  const HKDF_SALT = Buffer.from('oversky-cred-kek-salt-v1');
90
103
  const HKDF_INFO = Buffer.from('oversky-cred-kek-v1');
91
104
  /** Filesystem paths for Tier-3 IKM components. */
@@ -299,7 +312,7 @@ async function writeKekToSecretService(kek, service, account) {
299
312
  }
300
313
  const b64 = kek.toString('base64');
301
314
  try {
302
- await exec('secret-tool', ['store', '--label=OverSky daemon master KEK', 'service', service, 'account', account], { input: b64 });
315
+ await exec('secret-tool', ['store', '--label=skrr daemon master KEK', 'service', service, 'account', account], { input: b64 });
303
316
  }
304
317
  catch (err) {
305
318
  throw new types_js_1.KekUnavailableError(KIND_LIBSECRET, 'secret-tool store failed', { cause: err });
@@ -55,13 +55,4 @@ export interface LegacyStateReport {
55
55
  * real `~` passes or fails based on who runs it.
56
56
  */
57
57
  export declare function findLegacyLocalState(home?: string): LegacyStateReport;
58
- /**
59
- * The message shown when preflight refuses.
60
- *
61
- * Written for someone who has just been stopped and wants to know why and what
62
- * to do — so it names the exact paths, says explicitly that nothing was touched,
63
- * and points at the runbook rather than improvising a fix. It does NOT offer a
64
- * `--force`: the operator's next step is the backup the runbook specifies, and
65
- * a flag that skips a safety check is the flag everyone learns to paste.
66
- */
67
58
  export declare function describeLegacyState(report: LegacyStateReport, binaryName: string): string;
@@ -58,6 +58,13 @@ function findLegacyLocalState(home = node_os_1.default.homedir()) {
58
58
  * `--force`: the operator's next step is the backup the runbook specifies, and
59
59
  * a flag that skips a safety check is the flag everyone learns to paste.
60
60
  */
61
+ /**
62
+ * The pre-rename runtime binary. Named in the refusal above because deregistering
63
+ * the OLD service is the one step the NEW binary cannot do: `uninstall` resolves
64
+ * the current profile's label (`ai.skrr.daemon.*`), and the legacy sweep is wired
65
+ * only into the install path.
66
+ */
67
+ const LEGACY_BINARY_NAME = 'oversky';
61
68
  function describeLegacyState(report, binaryName) {
62
69
  const lines = [];
63
70
  lines.push('Found state from the previous product identity on this machine:');
@@ -72,7 +79,17 @@ function describeLegacyState(report, binaryName) {
72
79
  lines.push('Nothing above has been read, copied or removed. Follow the team cutover');
73
80
  lines.push('runbook — it takes a backup first — and then run this again:');
74
81
  lines.push('');
75
- lines.push(` ${binaryName} uninstall # stop and deregister the old service`);
82
+ // The OLD binary, deliberately: it is the one that knows the old service
83
+ // label. `<current> uninstall` resolves the CURRENT profile's label and would
84
+ // report success while leaving the pre-rename service registered. Guarded by
85
+ // "only if" rather than omitted, because this module inspects directories and
86
+ // has no way to know whether that binary is still on PATH.
87
+ lines.push(` ${LEGACY_BINARY_NAME} uninstall # only if the old runtime is still installed —`);
88
+ lines.push(` # it is the one that knows the old service label`);
76
89
  lines.push(' # then remove the directories listed above, per the runbook');
90
+ lines.push('');
91
+ lines.push(`Any pre-rename service still registered is swept by \`${binaryName} install\``);
92
+ lines.push('once those directories are gone, so re-running the install completes the');
93
+ lines.push('cutover either way.');
77
94
  return lines.join('\n');
78
95
  }
@@ -354,15 +354,15 @@ function htmlShell(title, accent, headline, body) {
354
354
  function successHtml() {
355
355
  const headline = `<div class="badge"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></div>
356
356
  <h1>Authorized</h1>`;
357
- return htmlShell('OverSky — Authorized', '#10b981', headline, 'You can close this tab and return to your terminal.');
357
+ return htmlShell('skrr — Authorized', '#10b981', headline, 'You can close this tab and return to your terminal.');
358
358
  }
359
359
  function declinedHtml() {
360
360
  const headline = `<div class="badge" style="background:#64748b"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></div>
361
361
  <h1>Authorization declined</h1>`;
362
- return htmlShell('OverSky — Authorization declined', '#64748b', headline, 'You can close this tab. Run the CLI command again to retry.');
362
+ return htmlShell('skrr — Authorization declined', '#64748b', headline, 'You can close this tab. Run the CLI command again to retry.');
363
363
  }
364
364
  function genericFailureHtml() {
365
365
  const headline = `<div class="badge" style="background:#ef4444"><svg viewBox="0 0 24 24"><path d="M12 8v4M12 16h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div>
366
366
  <h1>Authorization error</h1>`;
367
- return htmlShell('OverSky — Authorization error', '#ef4444', headline, 'Something went wrong. You can close this tab and retry from the terminal.');
367
+ return htmlShell('skrr — Authorization error', '#ef4444', headline, 'Something went wrong. You can close this tab and retry from the terminal.');
368
368
  }
@@ -18,8 +18,8 @@ const REASON_HUMAN = {
18
18
  SESSION_REVOKED: 'Your session was ended (you signed out elsewhere or your password changed).',
19
19
  TOKEN_EXPIRED: 'Your access token has expired.',
20
20
  TOKEN_INVALID: 'Your saved credentials are no longer valid.',
21
- UNKNOWN_401: 'OverSky needs you to sign in again.',
22
- UNKNOWN: 'OverSky needs you to sign in again.',
21
+ UNKNOWN_401: 'skrr needs you to sign in again.',
22
+ UNKNOWN: 'skrr needs you to sign in again.',
23
23
  };
24
24
  function formatReauthMessage(reason) {
25
25
  const norm = REASON_HUMAN[reason] || REASON_HUMAN.UNKNOWN;
@@ -28,7 +28,7 @@ function formatReauthMessage(reason) {
28
28
  const cmd = isHeadless ? `${bin} login --device` : `${bin} login`;
29
29
  return [
30
30
  '',
31
- 'OverSky cannot connect — re-authentication required.',
31
+ 'skrr cannot connect — re-authentication required.',
32
32
  ` Reason: ${norm}`,
33
33
  '',
34
34
  ` To continue, run: ${cmd}`,
@@ -49,9 +49,13 @@ export interface AuthCoreConfig {
49
49
  * per-profile and silently killed the `legacyDaemonUuids()` fold safety net
50
50
  * (it scanned `<profileDir>/profiles/*`, which never exists). See OSK-1478.
51
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`.
52
+ * Default: falls back to `configDir()` when unset. BOTH callers now wire it
53
+ * explicitly the daemon to its root `CONFIG_DIR` in `auth/configure.ts`,
54
+ * and the CLI in `auth-core-init.ts`. The CLI's used to need no wiring
55
+ * because its `configDir` was unconditionally the machine root; that stopped
56
+ * being true when `configDir` became profile-aware, and an unwired default
57
+ * would have drifted `cliId` per profile — which the server rejects, because
58
+ * it binds that value into the JWT `did` claim.
55
59
  */
56
60
  machineConfigDir?: () => string;
57
61
  /** Logger used by all stateful helpers. */
@@ -0,0 +1,96 @@
1
+ /**
2
+ * skrr Code update channels — the vocabulary, in the ONE place both readers resolve.
3
+ *
4
+ * It lived in `daemon/src/sky-code/channels.ts`, whose only importer was the
5
+ * daemon's own installer. That was fine while the daemon was the only thing that
6
+ * needed it, and it stopped being fine the moment a user could be told to go
7
+ * looking: `skrr code`'s not-installed message promised that `skrr code doctor`
8
+ * would name "which channel this machine follows and whether that channel has a
9
+ * published release", and the CLI had no access to any of it — so the sentence
10
+ * described a diagnosis nobody had written (dogfood OSK-274).
11
+ *
12
+ * A MIRROR in the CLI was the obvious alternative and is the wrong answer here,
13
+ * with precedent: OSK-3894 shipped exactly that for the harness trust tiers and
14
+ * OSK-3897 removed it, because a mirror proves two copies agree rather than
15
+ * proving there is one copy. `harnessTrust.ts` moved into this package for that
16
+ * reason, the daemon re-exports it, and a static check keeps the daemon from
17
+ * quietly redeclaring. This follows that road rather than reopening the one it
18
+ * replaced.
19
+ *
20
+ * Only the VOCABULARY moves — channel names, the env var, the default, and the
21
+ * URL shape. The installer, the signature verification and the promotion
22
+ * machinery stay in the daemon, which is the only process that performs them.
23
+ */
24
+ /** Channels in promotion order. Index order IS the promotion order. */
25
+ export declare const SKY_CODE_CHANNELS: readonly ["internal", "canary", "beta", "stable"];
26
+ export type SkyCodeChannel = (typeof SKY_CODE_CHANNELS)[number];
27
+ /**
28
+ * The channel a build without an explicit setting follows.
29
+ *
30
+ * `stable`, and never inferred from anything else. A default that drifted with
31
+ * the build (say, dev builds silently on `canary`) would mean a user's update
32
+ * path depended on how their binary happened to be produced.
33
+ */
34
+ export declare const DEFAULT_SKY_CODE_CHANNEL: SkyCodeChannel;
35
+ /** Env var selecting the channel. Explicit opt-in; never set implicitly. */
36
+ export declare const SKY_CODE_CHANNEL_ENV = "OVERSKY_SKY_CODE_CHANNEL";
37
+ /** Manifest filename, under a channel prefix or an immutable release prefix. */
38
+ export declare const SKY_CODE_MANIFEST_FILE = "sky-code-version.json";
39
+ export declare function isSkyCodeChannel(value: unknown): value is SkyCodeChannel;
40
+ /**
41
+ * The channel this process should follow.
42
+ *
43
+ * An unrecognised value falls back to the default rather than throwing. A
44
+ * typo'd channel must not brick updates altogether — that turns a harmless
45
+ * mistake into an un-updatable machine, which is the failure the updater exists
46
+ * to prevent. The caller is handed `invalid` so it can say so out loud.
47
+ */
48
+ export declare function resolveSkyCodeChannel(env?: NodeJS.ProcessEnv): {
49
+ channel: SkyCodeChannel;
50
+ invalid?: string;
51
+ };
52
+ /**
53
+ * Where a channel's signed manifest lives.
54
+ *
55
+ * `stable` is NOT special-cased to `latest/`. Two paths that must always agree
56
+ * is a pair that eventually will not, and the one that disagrees silently is
57
+ * the one serving production. `latest/` remains for the daemon feed and for
58
+ * anything already pointing at it; skrr Code channel clients read
59
+ * `channels/<name>/` uniformly.
60
+ */
61
+ export declare function skyCodeChannelPrefix(channel: SkyCodeChannel): string;
62
+ /**
63
+ * Manifest URL for a channel, or for a pinned version.
64
+ *
65
+ * A pinned version wins over the channel and reads its immutable per-release
66
+ * copy: pinning means "this exact build", and a channel pointer that moved
67
+ * underneath would make the pin a suggestion.
68
+ *
69
+ * Builds only MANIFEST urls, never artifact ones. `buildManifestMessage` signs
70
+ * version, minimum, forceUpdate and the per-platform sha256s; `buildArtifactMessage`
71
+ * signs the artifact URL. Neither signs a channel — so artifact URLs stay
72
+ * channel-independent and promotion is a COPY OF AN ALREADY-SIGNED OBJECT that
73
+ * never needs the release key. Put the channel in the artifact URL instead and
74
+ * every promotion becomes a re-signing ceremony, which decides whether the
75
+ * ed25519 key has to be reachable from routine automation. A helper here that
76
+ * adjusted artifact urls would silently reintroduce exactly that coupling.
77
+ */
78
+ export declare function resolveChannelManifestUrl(input: {
79
+ base: string;
80
+ manifestFilename: string;
81
+ channel: SkyCodeChannel;
82
+ pinnedVersion?: string | null;
83
+ }): string;
84
+ /** Default public feed. Mirrors the daemon's `updates.oversky.ai/daemon` prefix. */
85
+ export declare const DEFAULT_SKY_CODE_FEED_BASE = "https://updates.oversky.ai/sky-code";
86
+ /**
87
+ * Feed override, read at call time rather than module load so a test or an
88
+ * operator can repoint it without re-importing.
89
+ *
90
+ * Explicit-only: a production user must never be silently enrolled onto a
91
+ * different feed. Selecting a feed changes WHICH signed manifest is read, never
92
+ * WHETHER the signature is checked — every artifact is verified against the
93
+ * pinned keys regardless of where it came from.
94
+ */
95
+ export declare const SKY_CODE_FEED_BASE_ENV = "OVERSKY_SKY_CODE_UPDATE_FEED_BASE";
96
+ export declare function skyCodeFeedBase(env?: NodeJS.ProcessEnv): string;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * skrr Code update channels — the vocabulary, in the ONE place both readers resolve.
4
+ *
5
+ * It lived in `daemon/src/sky-code/channels.ts`, whose only importer was the
6
+ * daemon's own installer. That was fine while the daemon was the only thing that
7
+ * needed it, and it stopped being fine the moment a user could be told to go
8
+ * looking: `skrr code`'s not-installed message promised that `skrr code doctor`
9
+ * would name "which channel this machine follows and whether that channel has a
10
+ * published release", and the CLI had no access to any of it — so the sentence
11
+ * described a diagnosis nobody had written (dogfood OSK-274).
12
+ *
13
+ * A MIRROR in the CLI was the obvious alternative and is the wrong answer here,
14
+ * with precedent: OSK-3894 shipped exactly that for the harness trust tiers and
15
+ * OSK-3897 removed it, because a mirror proves two copies agree rather than
16
+ * proving there is one copy. `harnessTrust.ts` moved into this package for that
17
+ * reason, the daemon re-exports it, and a static check keeps the daemon from
18
+ * quietly redeclaring. This follows that road rather than reopening the one it
19
+ * replaced.
20
+ *
21
+ * Only the VOCABULARY moves — channel names, the env var, the default, and the
22
+ * URL shape. The installer, the signature verification and the promotion
23
+ * machinery stay in the daemon, which is the only process that performs them.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.SKY_CODE_FEED_BASE_ENV = exports.DEFAULT_SKY_CODE_FEED_BASE = exports.SKY_CODE_MANIFEST_FILE = exports.SKY_CODE_CHANNEL_ENV = exports.DEFAULT_SKY_CODE_CHANNEL = exports.SKY_CODE_CHANNELS = void 0;
27
+ exports.isSkyCodeChannel = isSkyCodeChannel;
28
+ exports.resolveSkyCodeChannel = resolveSkyCodeChannel;
29
+ exports.skyCodeChannelPrefix = skyCodeChannelPrefix;
30
+ exports.resolveChannelManifestUrl = resolveChannelManifestUrl;
31
+ exports.skyCodeFeedBase = skyCodeFeedBase;
32
+ /** Channels in promotion order. Index order IS the promotion order. */
33
+ exports.SKY_CODE_CHANNELS = ['internal', 'canary', 'beta', 'stable'];
34
+ /**
35
+ * The channel a build without an explicit setting follows.
36
+ *
37
+ * `stable`, and never inferred from anything else. A default that drifted with
38
+ * the build (say, dev builds silently on `canary`) would mean a user's update
39
+ * path depended on how their binary happened to be produced.
40
+ */
41
+ exports.DEFAULT_SKY_CODE_CHANNEL = 'stable';
42
+ /** Env var selecting the channel. Explicit opt-in; never set implicitly. */
43
+ exports.SKY_CODE_CHANNEL_ENV = 'OVERSKY_SKY_CODE_CHANNEL';
44
+ /** Manifest filename, under a channel prefix or an immutable release prefix. */
45
+ exports.SKY_CODE_MANIFEST_FILE = 'sky-code-version.json';
46
+ function isSkyCodeChannel(value) {
47
+ return typeof value === 'string' && exports.SKY_CODE_CHANNELS.includes(value);
48
+ }
49
+ /**
50
+ * The channel this process should follow.
51
+ *
52
+ * An unrecognised value falls back to the default rather than throwing. A
53
+ * typo'd channel must not brick updates altogether — that turns a harmless
54
+ * mistake into an un-updatable machine, which is the failure the updater exists
55
+ * to prevent. The caller is handed `invalid` so it can say so out loud.
56
+ */
57
+ function resolveSkyCodeChannel(env = process.env) {
58
+ const raw = env[exports.SKY_CODE_CHANNEL_ENV]?.trim();
59
+ if (!raw)
60
+ return { channel: exports.DEFAULT_SKY_CODE_CHANNEL };
61
+ if (isSkyCodeChannel(raw))
62
+ return { channel: raw };
63
+ return { channel: exports.DEFAULT_SKY_CODE_CHANNEL, invalid: raw };
64
+ }
65
+ /**
66
+ * Where a channel's signed manifest lives.
67
+ *
68
+ * `stable` is NOT special-cased to `latest/`. Two paths that must always agree
69
+ * is a pair that eventually will not, and the one that disagrees silently is
70
+ * the one serving production. `latest/` remains for the daemon feed and for
71
+ * anything already pointing at it; skrr Code channel clients read
72
+ * `channels/<name>/` uniformly.
73
+ */
74
+ function skyCodeChannelPrefix(channel) {
75
+ return `channels/${channel}/`;
76
+ }
77
+ /**
78
+ * Manifest URL for a channel, or for a pinned version.
79
+ *
80
+ * A pinned version wins over the channel and reads its immutable per-release
81
+ * copy: pinning means "this exact build", and a channel pointer that moved
82
+ * underneath would make the pin a suggestion.
83
+ *
84
+ * Builds only MANIFEST urls, never artifact ones. `buildManifestMessage` signs
85
+ * version, minimum, forceUpdate and the per-platform sha256s; `buildArtifactMessage`
86
+ * signs the artifact URL. Neither signs a channel — so artifact URLs stay
87
+ * channel-independent and promotion is a COPY OF AN ALREADY-SIGNED OBJECT that
88
+ * never needs the release key. Put the channel in the artifact URL instead and
89
+ * every promotion becomes a re-signing ceremony, which decides whether the
90
+ * ed25519 key has to be reachable from routine automation. A helper here that
91
+ * adjusted artifact urls would silently reintroduce exactly that coupling.
92
+ */
93
+ function resolveChannelManifestUrl(input) {
94
+ const base = input.base.replace(/\/+$/, '');
95
+ if (input.pinnedVersion) {
96
+ return `${base}/releases/${input.pinnedVersion}/${input.manifestFilename}`;
97
+ }
98
+ return `${base}/${skyCodeChannelPrefix(input.channel)}${input.manifestFilename}`;
99
+ }
100
+ /** Default public feed. Mirrors the daemon's `updates.oversky.ai/daemon` prefix. */
101
+ exports.DEFAULT_SKY_CODE_FEED_BASE = 'https://updates.oversky.ai/sky-code';
102
+ /**
103
+ * Feed override, read at call time rather than module load so a test or an
104
+ * operator can repoint it without re-importing.
105
+ *
106
+ * Explicit-only: a production user must never be silently enrolled onto a
107
+ * different feed. Selecting a feed changes WHICH signed manifest is read, never
108
+ * WHETHER the signature is checked — every artifact is verified against the
109
+ * pinned keys regardless of where it came from.
110
+ */
111
+ exports.SKY_CODE_FEED_BASE_ENV = 'OVERSKY_SKY_CODE_UPDATE_FEED_BASE';
112
+ function skyCodeFeedBase(env = process.env) {
113
+ const override = (env[exports.SKY_CODE_FEED_BASE_ENV] || '').trim();
114
+ return (override || exports.DEFAULT_SKY_CODE_FEED_BASE).replace(/\/+$/, '');
115
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The skrr root — the single definition of where this machine keeps its skrr
3
+ * state.
4
+ *
5
+ * It lives here because BOTH binaries need the same answer and neither owns it.
6
+ * `skrr` and `skrrd` keep separate credentials on purpose, but they share one
7
+ * root, and every place that re-derived it became a place they could disagree.
8
+ * They did, twice in one day:
9
+ *
10
+ * - The CLI's engine path honoured `OVERSKY_CONFIG_DIR` while its config did
11
+ * not, so a relocated root moved the engine and left the credentials in
12
+ * `$HOME`. Fixed by teaching `config.ts` the variable — which promptly
13
+ * re-created the split in the mirror image, because `sky-code.ts` still read
14
+ * only the old name.
15
+ * - The daemon then had the same shape, and fixing the CLI's half turned it
16
+ * into a CLI/daemon divergence: with the variable set, the two halves of one
17
+ * product disagreed about where the credentials live.
18
+ *
19
+ * Each of those was a correct local fix that created the next defect, because
20
+ * the root was being DERIVED rather than READ. Teaching every site the same list
21
+ * of variable names only defers the problem to whenever the list changes again —
22
+ * which is exactly what `SKRR_CONFIG_DIR` did.
23
+ *
24
+ * So: one function, and a boundary test that fails if anything re-derives it.
25
+ *
26
+ * `SKRR_CONFIG_DIR` is the spelling for anything new (root `CLAUDE.md`) and wins
27
+ * when both are set. `OVERSKY_CONFIG_DIR` keeps working because existing installs,
28
+ * the engine resolver and the daemon all read it, and breaking a documented
29
+ * override to tidy a name would be a worse trade than carrying two.
30
+ *
31
+ * Takes `env` rather than reading the global so a caller can resolve a root for a
32
+ * child process it is about to spawn, and so tests need no process-wide mutation.
33
+ */
34
+ export declare function resolveConfigRoot(env?: NodeJS.ProcessEnv): string;
35
+ /** A path under the skrr root, e.g. `configRootPath(env, 'sky-code', 'bin')`. */
36
+ export declare function configRootPath(env: NodeJS.ProcessEnv, ...segments: string[]): string;
37
+ /**
38
+ * Has the operator explicitly relocated the root?
39
+ *
40
+ * A distinct question from "what is the root", and it has a real caller: the
41
+ * legacy-location fallbacks exist to find state left by an older layout under
42
+ * `$HOME`, and once someone has POINTED the root somewhere, guessing at
43
+ * `$HOME` is no longer a helpful fallback — it is a different machine's data.
44
+ *
45
+ * Shared for the same reason as `resolveConfigRoot`: this check was copied to
46
+ * three call sites, each spelling the variable list itself, so adding
47
+ * `SKRR_CONFIG_DIR` to the resolver silently left the copies answering the old
48
+ * question.
49
+ */
50
+ export declare function isConfigRootOverridden(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,59 @@
1
+ import { homedir } from 'node:os';
2
+ import path from 'node:path';
3
+ /**
4
+ * The skrr root — the single definition of where this machine keeps its skrr
5
+ * state.
6
+ *
7
+ * It lives here because BOTH binaries need the same answer and neither owns it.
8
+ * `skrr` and `skrrd` keep separate credentials on purpose, but they share one
9
+ * root, and every place that re-derived it became a place they could disagree.
10
+ * They did, twice in one day:
11
+ *
12
+ * - The CLI's engine path honoured `OVERSKY_CONFIG_DIR` while its config did
13
+ * not, so a relocated root moved the engine and left the credentials in
14
+ * `$HOME`. Fixed by teaching `config.ts` the variable — which promptly
15
+ * re-created the split in the mirror image, because `sky-code.ts` still read
16
+ * only the old name.
17
+ * - The daemon then had the same shape, and fixing the CLI's half turned it
18
+ * into a CLI/daemon divergence: with the variable set, the two halves of one
19
+ * product disagreed about where the credentials live.
20
+ *
21
+ * Each of those was a correct local fix that created the next defect, because
22
+ * the root was being DERIVED rather than READ. Teaching every site the same list
23
+ * of variable names only defers the problem to whenever the list changes again —
24
+ * which is exactly what `SKRR_CONFIG_DIR` did.
25
+ *
26
+ * So: one function, and a boundary test that fails if anything re-derives it.
27
+ *
28
+ * `SKRR_CONFIG_DIR` is the spelling for anything new (root `CLAUDE.md`) and wins
29
+ * when both are set. `OVERSKY_CONFIG_DIR` keeps working because existing installs,
30
+ * the engine resolver and the daemon all read it, and breaking a documented
31
+ * override to tidy a name would be a worse trade than carrying two.
32
+ *
33
+ * Takes `env` rather than reading the global so a caller can resolve a root for a
34
+ * child process it is about to spawn, and so tests need no process-wide mutation.
35
+ */
36
+ export function resolveConfigRoot(env = process.env) {
37
+ const override = (env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim();
38
+ return override || path.join(homedir(), '.skrr');
39
+ }
40
+ /** A path under the skrr root, e.g. `configRootPath(env, 'sky-code', 'bin')`. */
41
+ export function configRootPath(env, ...segments) {
42
+ return path.join(resolveConfigRoot(env), ...segments);
43
+ }
44
+ /**
45
+ * Has the operator explicitly relocated the root?
46
+ *
47
+ * A distinct question from "what is the root", and it has a real caller: the
48
+ * legacy-location fallbacks exist to find state left by an older layout under
49
+ * `$HOME`, and once someone has POINTED the root somewhere, guessing at
50
+ * `$HOME` is no longer a helpful fallback — it is a different machine's data.
51
+ *
52
+ * Shared for the same reason as `resolveConfigRoot`: this check was copied to
53
+ * three call sites, each spelling the variable list itself, so adding
54
+ * `SKRR_CONFIG_DIR` to the resolver silently left the copies answering the old
55
+ * question.
56
+ */
57
+ export function isConfigRootOverridden(env = process.env) {
58
+ return Boolean((env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim());
59
+ }
@@ -215,7 +215,7 @@ export declare class AuthHelperUnavailableError extends Error {
215
215
  /**
216
216
  * L6.1 — typed error thrown when the workspace-trust gate blocks a
217
217
  * project- or local-scope helper. Surfaces the actionable next step
218
- * (`oversky trust accept`) to the user; the daemon catches and presents
218
+ * (`skrrd trust accept`) to the user; the daemon catches and presents
219
219
  * this without falling through to OAuth.
220
220
  */
221
221
  export declare class AuthHelperUntrustedError extends Error {
@@ -130,7 +130,7 @@ export class AuthHelperUnavailableError extends Error {
130
130
  /**
131
131
  * L6.1 — typed error thrown when the workspace-trust gate blocks a
132
132
  * project- or local-scope helper. Surfaces the actionable next step
133
- * (`oversky trust accept`) to the user; the daemon catches and presents
133
+ * (`skrrd trust accept`) to the user; the daemon catches and presents
134
134
  * this without falling through to OAuth.
135
135
  */
136
136
  export class AuthHelperUntrustedError extends Error {
@@ -139,7 +139,7 @@ export class AuthHelperUntrustedError extends Error {
139
139
  helperPath;
140
140
  constructor(helperPath, origin, reason) {
141
141
  super(`OVERSKY_AUTH_HELPER (${origin}-scope) is not trusted for this workspace ` +
142
- `(reason: ${reason}). Run \`oversky trust accept\` to allow it.`);
142
+ `(reason: ${reason}). Run \`skrrd trust accept\` to allow it.`);
143
143
  this.name = 'AuthHelperUntrustedError';
144
144
  this.origin = origin;
145
145
  this.reason = reason;
@@ -35,3 +35,5 @@ export { HARNESS_TIERS, getHarnessTier, credentialPolicyForTier, tierPermitsConf
35
35
  export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE_ENV_SUFFIXES, SENSITIVE_ENV_PREFIX_EXCEPTIONS, } from './spawnEnv.js';
36
36
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
37
37
  export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
38
+ export { DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveChannelManifestUrl, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, type SkyCodeChannel, } from './skyCodeChannels.js';
39
+ export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
package/dist/esm/index.js CHANGED
@@ -142,3 +142,7 @@ export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATI
142
142
  /* Pre-rename state detection. Callers REFUSE on a positive result; nothing here
143
143
  reads, copies or removes what it finds (§7.2, §8.4). */
144
144
  export { findLegacyLocalState, describeLegacyState, } from './legacyStatePreflight.js';
145
+ export { DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveChannelManifestUrl, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, } from './skyCodeChannels.js';
146
+ // The one definition of the skrr root, shared by `skrr` and `skrrd`. See the
147
+ // module for why re-deriving it kept producing divergences.
148
+ export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
@@ -74,8 +74,21 @@ const DBUS_TRANSPORT_ERRORS = [
74
74
  'No such interface',
75
75
  'Cannot autolaunch',
76
76
  ];
77
- /** HKDF-SHA256 parameters for Tier-3. Stable across versions; bumping the
78
- * `v1` suffix would silently invalidate every wrapped DEK on disk. */
77
+ /**
78
+ * HKDF-SHA256 parameters for Tier-3. Stable across versions: ANY change
79
+ * silently invalidates every wrapped DEK on disk.
80
+ *
81
+ * That includes the brand. These strings are cryptographic derivation inputs
82
+ * that happen to read like a name, so a rename sweep produces exactly the same
83
+ * outcome as bumping the `v1` suffix — every credential on every machine
84
+ * becomes undecryptable, with no error at write time and a failure that
85
+ * surfaces only when someone next tries to unwrap. The KEK's SERVICE name moved
86
+ * to `ai.skrr.daemon.kek`; these did not, and must not.
87
+ *
88
+ * Rotating them deliberately is a re-issue, not a rename: it needs the old
89
+ * envelope discarded and credentials re-obtained, which is a decision with a
90
+ * user-visible cost rather than a string edit.
91
+ */
79
92
  const HKDF_SALT = Buffer.from('oversky-cred-kek-salt-v1');
80
93
  const HKDF_INFO = Buffer.from('oversky-cred-kek-v1');
81
94
  /** Filesystem paths for Tier-3 IKM components. */
@@ -289,7 +302,7 @@ async function writeKekToSecretService(kek, service, account) {
289
302
  }
290
303
  const b64 = kek.toString('base64');
291
304
  try {
292
- await exec('secret-tool', ['store', '--label=OverSky daemon master KEK', 'service', service, 'account', account], { input: b64 });
305
+ await exec('secret-tool', ['store', '--label=skrr daemon master KEK', 'service', service, 'account', account], { input: b64 });
293
306
  }
294
307
  catch (err) {
295
308
  throw new KekUnavailableError(KIND_LIBSECRET, 'secret-tool store failed', { cause: err });
@@ -55,13 +55,4 @@ export interface LegacyStateReport {
55
55
  * real `~` passes or fails based on who runs it.
56
56
  */
57
57
  export declare function findLegacyLocalState(home?: string): LegacyStateReport;
58
- /**
59
- * The message shown when preflight refuses.
60
- *
61
- * Written for someone who has just been stopped and wants to know why and what
62
- * to do — so it names the exact paths, says explicitly that nothing was touched,
63
- * and points at the runbook rather than improvising a fix. It does NOT offer a
64
- * `--force`: the operator's next step is the backup the runbook specifies, and
65
- * a flag that skips a safety check is the flag everyone learns to paste.
66
- */
67
58
  export declare function describeLegacyState(report: LegacyStateReport, binaryName: string): string;
@@ -51,6 +51,13 @@ export function findLegacyLocalState(home = os.homedir()) {
51
51
  * `--force`: the operator's next step is the backup the runbook specifies, and
52
52
  * a flag that skips a safety check is the flag everyone learns to paste.
53
53
  */
54
+ /**
55
+ * The pre-rename runtime binary. Named in the refusal above because deregistering
56
+ * the OLD service is the one step the NEW binary cannot do: `uninstall` resolves
57
+ * the current profile's label (`ai.skrr.daemon.*`), and the legacy sweep is wired
58
+ * only into the install path.
59
+ */
60
+ const LEGACY_BINARY_NAME = 'oversky';
54
61
  export function describeLegacyState(report, binaryName) {
55
62
  const lines = [];
56
63
  lines.push('Found state from the previous product identity on this machine:');
@@ -65,7 +72,17 @@ export function describeLegacyState(report, binaryName) {
65
72
  lines.push('Nothing above has been read, copied or removed. Follow the team cutover');
66
73
  lines.push('runbook — it takes a backup first — and then run this again:');
67
74
  lines.push('');
68
- lines.push(` ${binaryName} uninstall # stop and deregister the old service`);
75
+ // The OLD binary, deliberately: it is the one that knows the old service
76
+ // label. `<current> uninstall` resolves the CURRENT profile's label and would
77
+ // report success while leaving the pre-rename service registered. Guarded by
78
+ // "only if" rather than omitted, because this module inspects directories and
79
+ // has no way to know whether that binary is still on PATH.
80
+ lines.push(` ${LEGACY_BINARY_NAME} uninstall # only if the old runtime is still installed —`);
81
+ lines.push(` # it is the one that knows the old service label`);
69
82
  lines.push(' # then remove the directories listed above, per the runbook');
83
+ lines.push('');
84
+ lines.push(`Any pre-rename service still registered is swept by \`${binaryName} install\``);
85
+ lines.push('once those directories are gone, so re-running the install completes the');
86
+ lines.push('cutover either way.');
70
87
  return lines.join('\n');
71
88
  }
@@ -341,15 +341,15 @@ function htmlShell(title, accent, headline, body) {
341
341
  function successHtml() {
342
342
  const headline = `<div class="badge"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></div>
343
343
  <h1>Authorized</h1>`;
344
- return htmlShell('OverSky — Authorized', '#10b981', headline, 'You can close this tab and return to your terminal.');
344
+ return htmlShell('skrr — Authorized', '#10b981', headline, 'You can close this tab and return to your terminal.');
345
345
  }
346
346
  function declinedHtml() {
347
347
  const headline = `<div class="badge" style="background:#64748b"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></div>
348
348
  <h1>Authorization declined</h1>`;
349
- return htmlShell('OverSky — Authorization declined', '#64748b', headline, 'You can close this tab. Run the CLI command again to retry.');
349
+ return htmlShell('skrr — Authorization declined', '#64748b', headline, 'You can close this tab. Run the CLI command again to retry.');
350
350
  }
351
351
  function genericFailureHtml() {
352
352
  const headline = `<div class="badge" style="background:#ef4444"><svg viewBox="0 0 24 24"><path d="M12 8v4M12 16h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div>
353
353
  <h1>Authorization error</h1>`;
354
- return htmlShell('OverSky — Authorization error', '#ef4444', headline, 'Something went wrong. You can close this tab and retry from the terminal.');
354
+ return htmlShell('skrr — Authorization error', '#ef4444', headline, 'Something went wrong. You can close this tab and retry from the terminal.');
355
355
  }
@@ -14,8 +14,8 @@ const REASON_HUMAN = {
14
14
  SESSION_REVOKED: 'Your session was ended (you signed out elsewhere or your password changed).',
15
15
  TOKEN_EXPIRED: 'Your access token has expired.',
16
16
  TOKEN_INVALID: 'Your saved credentials are no longer valid.',
17
- UNKNOWN_401: 'OverSky needs you to sign in again.',
18
- UNKNOWN: 'OverSky needs you to sign in again.',
17
+ UNKNOWN_401: 'skrr needs you to sign in again.',
18
+ UNKNOWN: 'skrr needs you to sign in again.',
19
19
  };
20
20
  export function formatReauthMessage(reason) {
21
21
  const norm = REASON_HUMAN[reason] || REASON_HUMAN.UNKNOWN;
@@ -24,7 +24,7 @@ export function formatReauthMessage(reason) {
24
24
  const cmd = isHeadless ? `${bin} login --device` : `${bin} login`;
25
25
  return [
26
26
  '',
27
- 'OverSky cannot connect — re-authentication required.',
27
+ 'skrr cannot connect — re-authentication required.',
28
28
  ` Reason: ${norm}`,
29
29
  '',
30
30
  ` To continue, run: ${cmd}`,
@@ -49,9 +49,13 @@ export interface AuthCoreConfig {
49
49
  * per-profile and silently killed the `legacyDaemonUuids()` fold safety net
50
50
  * (it scanned `<profileDir>/profiles/*`, which never exists). See OSK-1478.
51
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`.
52
+ * Default: falls back to `configDir()` when unset. BOTH callers now wire it
53
+ * explicitly the daemon to its root `CONFIG_DIR` in `auth/configure.ts`,
54
+ * and the CLI in `auth-core-init.ts`. The CLI's used to need no wiring
55
+ * because its `configDir` was unconditionally the machine root; that stopped
56
+ * being true when `configDir` became profile-aware, and an unwired default
57
+ * would have drifted `cliId` per profile — which the server rejects, because
58
+ * it binds that value into the JWT `did` claim.
55
59
  */
56
60
  machineConfigDir?: () => string;
57
61
  /** Logger used by all stateful helpers. */
@@ -0,0 +1,96 @@
1
+ /**
2
+ * skrr Code update channels — the vocabulary, in the ONE place both readers resolve.
3
+ *
4
+ * It lived in `daemon/src/sky-code/channels.ts`, whose only importer was the
5
+ * daemon's own installer. That was fine while the daemon was the only thing that
6
+ * needed it, and it stopped being fine the moment a user could be told to go
7
+ * looking: `skrr code`'s not-installed message promised that `skrr code doctor`
8
+ * would name "which channel this machine follows and whether that channel has a
9
+ * published release", and the CLI had no access to any of it — so the sentence
10
+ * described a diagnosis nobody had written (dogfood OSK-274).
11
+ *
12
+ * A MIRROR in the CLI was the obvious alternative and is the wrong answer here,
13
+ * with precedent: OSK-3894 shipped exactly that for the harness trust tiers and
14
+ * OSK-3897 removed it, because a mirror proves two copies agree rather than
15
+ * proving there is one copy. `harnessTrust.ts` moved into this package for that
16
+ * reason, the daemon re-exports it, and a static check keeps the daemon from
17
+ * quietly redeclaring. This follows that road rather than reopening the one it
18
+ * replaced.
19
+ *
20
+ * Only the VOCABULARY moves — channel names, the env var, the default, and the
21
+ * URL shape. The installer, the signature verification and the promotion
22
+ * machinery stay in the daemon, which is the only process that performs them.
23
+ */
24
+ /** Channels in promotion order. Index order IS the promotion order. */
25
+ export declare const SKY_CODE_CHANNELS: readonly ["internal", "canary", "beta", "stable"];
26
+ export type SkyCodeChannel = (typeof SKY_CODE_CHANNELS)[number];
27
+ /**
28
+ * The channel a build without an explicit setting follows.
29
+ *
30
+ * `stable`, and never inferred from anything else. A default that drifted with
31
+ * the build (say, dev builds silently on `canary`) would mean a user's update
32
+ * path depended on how their binary happened to be produced.
33
+ */
34
+ export declare const DEFAULT_SKY_CODE_CHANNEL: SkyCodeChannel;
35
+ /** Env var selecting the channel. Explicit opt-in; never set implicitly. */
36
+ export declare const SKY_CODE_CHANNEL_ENV = "OVERSKY_SKY_CODE_CHANNEL";
37
+ /** Manifest filename, under a channel prefix or an immutable release prefix. */
38
+ export declare const SKY_CODE_MANIFEST_FILE = "sky-code-version.json";
39
+ export declare function isSkyCodeChannel(value: unknown): value is SkyCodeChannel;
40
+ /**
41
+ * The channel this process should follow.
42
+ *
43
+ * An unrecognised value falls back to the default rather than throwing. A
44
+ * typo'd channel must not brick updates altogether — that turns a harmless
45
+ * mistake into an un-updatable machine, which is the failure the updater exists
46
+ * to prevent. The caller is handed `invalid` so it can say so out loud.
47
+ */
48
+ export declare function resolveSkyCodeChannel(env?: NodeJS.ProcessEnv): {
49
+ channel: SkyCodeChannel;
50
+ invalid?: string;
51
+ };
52
+ /**
53
+ * Where a channel's signed manifest lives.
54
+ *
55
+ * `stable` is NOT special-cased to `latest/`. Two paths that must always agree
56
+ * is a pair that eventually will not, and the one that disagrees silently is
57
+ * the one serving production. `latest/` remains for the daemon feed and for
58
+ * anything already pointing at it; skrr Code channel clients read
59
+ * `channels/<name>/` uniformly.
60
+ */
61
+ export declare function skyCodeChannelPrefix(channel: SkyCodeChannel): string;
62
+ /**
63
+ * Manifest URL for a channel, or for a pinned version.
64
+ *
65
+ * A pinned version wins over the channel and reads its immutable per-release
66
+ * copy: pinning means "this exact build", and a channel pointer that moved
67
+ * underneath would make the pin a suggestion.
68
+ *
69
+ * Builds only MANIFEST urls, never artifact ones. `buildManifestMessage` signs
70
+ * version, minimum, forceUpdate and the per-platform sha256s; `buildArtifactMessage`
71
+ * signs the artifact URL. Neither signs a channel — so artifact URLs stay
72
+ * channel-independent and promotion is a COPY OF AN ALREADY-SIGNED OBJECT that
73
+ * never needs the release key. Put the channel in the artifact URL instead and
74
+ * every promotion becomes a re-signing ceremony, which decides whether the
75
+ * ed25519 key has to be reachable from routine automation. A helper here that
76
+ * adjusted artifact urls would silently reintroduce exactly that coupling.
77
+ */
78
+ export declare function resolveChannelManifestUrl(input: {
79
+ base: string;
80
+ manifestFilename: string;
81
+ channel: SkyCodeChannel;
82
+ pinnedVersion?: string | null;
83
+ }): string;
84
+ /** Default public feed. Mirrors the daemon's `updates.oversky.ai/daemon` prefix. */
85
+ export declare const DEFAULT_SKY_CODE_FEED_BASE = "https://updates.oversky.ai/sky-code";
86
+ /**
87
+ * Feed override, read at call time rather than module load so a test or an
88
+ * operator can repoint it without re-importing.
89
+ *
90
+ * Explicit-only: a production user must never be silently enrolled onto a
91
+ * different feed. Selecting a feed changes WHICH signed manifest is read, never
92
+ * WHETHER the signature is checked — every artifact is verified against the
93
+ * pinned keys regardless of where it came from.
94
+ */
95
+ export declare const SKY_CODE_FEED_BASE_ENV = "OVERSKY_SKY_CODE_UPDATE_FEED_BASE";
96
+ export declare function skyCodeFeedBase(env?: NodeJS.ProcessEnv): string;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * skrr Code update channels — the vocabulary, in the ONE place both readers resolve.
3
+ *
4
+ * It lived in `daemon/src/sky-code/channels.ts`, whose only importer was the
5
+ * daemon's own installer. That was fine while the daemon was the only thing that
6
+ * needed it, and it stopped being fine the moment a user could be told to go
7
+ * looking: `skrr code`'s not-installed message promised that `skrr code doctor`
8
+ * would name "which channel this machine follows and whether that channel has a
9
+ * published release", and the CLI had no access to any of it — so the sentence
10
+ * described a diagnosis nobody had written (dogfood OSK-274).
11
+ *
12
+ * A MIRROR in the CLI was the obvious alternative and is the wrong answer here,
13
+ * with precedent: OSK-3894 shipped exactly that for the harness trust tiers and
14
+ * OSK-3897 removed it, because a mirror proves two copies agree rather than
15
+ * proving there is one copy. `harnessTrust.ts` moved into this package for that
16
+ * reason, the daemon re-exports it, and a static check keeps the daemon from
17
+ * quietly redeclaring. This follows that road rather than reopening the one it
18
+ * replaced.
19
+ *
20
+ * Only the VOCABULARY moves — channel names, the env var, the default, and the
21
+ * URL shape. The installer, the signature verification and the promotion
22
+ * machinery stay in the daemon, which is the only process that performs them.
23
+ */
24
+ /** Channels in promotion order. Index order IS the promotion order. */
25
+ export const SKY_CODE_CHANNELS = ['internal', 'canary', 'beta', 'stable'];
26
+ /**
27
+ * The channel a build without an explicit setting follows.
28
+ *
29
+ * `stable`, and never inferred from anything else. A default that drifted with
30
+ * the build (say, dev builds silently on `canary`) would mean a user's update
31
+ * path depended on how their binary happened to be produced.
32
+ */
33
+ export const DEFAULT_SKY_CODE_CHANNEL = 'stable';
34
+ /** Env var selecting the channel. Explicit opt-in; never set implicitly. */
35
+ export const SKY_CODE_CHANNEL_ENV = 'OVERSKY_SKY_CODE_CHANNEL';
36
+ /** Manifest filename, under a channel prefix or an immutable release prefix. */
37
+ export const SKY_CODE_MANIFEST_FILE = 'sky-code-version.json';
38
+ export function isSkyCodeChannel(value) {
39
+ return typeof value === 'string' && SKY_CODE_CHANNELS.includes(value);
40
+ }
41
+ /**
42
+ * The channel this process should follow.
43
+ *
44
+ * An unrecognised value falls back to the default rather than throwing. A
45
+ * typo'd channel must not brick updates altogether — that turns a harmless
46
+ * mistake into an un-updatable machine, which is the failure the updater exists
47
+ * to prevent. The caller is handed `invalid` so it can say so out loud.
48
+ */
49
+ export function resolveSkyCodeChannel(env = process.env) {
50
+ const raw = env[SKY_CODE_CHANNEL_ENV]?.trim();
51
+ if (!raw)
52
+ return { channel: DEFAULT_SKY_CODE_CHANNEL };
53
+ if (isSkyCodeChannel(raw))
54
+ return { channel: raw };
55
+ return { channel: DEFAULT_SKY_CODE_CHANNEL, invalid: raw };
56
+ }
57
+ /**
58
+ * Where a channel's signed manifest lives.
59
+ *
60
+ * `stable` is NOT special-cased to `latest/`. Two paths that must always agree
61
+ * is a pair that eventually will not, and the one that disagrees silently is
62
+ * the one serving production. `latest/` remains for the daemon feed and for
63
+ * anything already pointing at it; skrr Code channel clients read
64
+ * `channels/<name>/` uniformly.
65
+ */
66
+ export function skyCodeChannelPrefix(channel) {
67
+ return `channels/${channel}/`;
68
+ }
69
+ /**
70
+ * Manifest URL for a channel, or for a pinned version.
71
+ *
72
+ * A pinned version wins over the channel and reads its immutable per-release
73
+ * copy: pinning means "this exact build", and a channel pointer that moved
74
+ * underneath would make the pin a suggestion.
75
+ *
76
+ * Builds only MANIFEST urls, never artifact ones. `buildManifestMessage` signs
77
+ * version, minimum, forceUpdate and the per-platform sha256s; `buildArtifactMessage`
78
+ * signs the artifact URL. Neither signs a channel — so artifact URLs stay
79
+ * channel-independent and promotion is a COPY OF AN ALREADY-SIGNED OBJECT that
80
+ * never needs the release key. Put the channel in the artifact URL instead and
81
+ * every promotion becomes a re-signing ceremony, which decides whether the
82
+ * ed25519 key has to be reachable from routine automation. A helper here that
83
+ * adjusted artifact urls would silently reintroduce exactly that coupling.
84
+ */
85
+ export function resolveChannelManifestUrl(input) {
86
+ const base = input.base.replace(/\/+$/, '');
87
+ if (input.pinnedVersion) {
88
+ return `${base}/releases/${input.pinnedVersion}/${input.manifestFilename}`;
89
+ }
90
+ return `${base}/${skyCodeChannelPrefix(input.channel)}${input.manifestFilename}`;
91
+ }
92
+ /** Default public feed. Mirrors the daemon's `updates.oversky.ai/daemon` prefix. */
93
+ export const DEFAULT_SKY_CODE_FEED_BASE = 'https://updates.oversky.ai/sky-code';
94
+ /**
95
+ * Feed override, read at call time rather than module load so a test or an
96
+ * operator can repoint it without re-importing.
97
+ *
98
+ * Explicit-only: a production user must never be silently enrolled onto a
99
+ * different feed. Selecting a feed changes WHICH signed manifest is read, never
100
+ * WHETHER the signature is checked — every artifact is verified against the
101
+ * pinned keys regardless of where it came from.
102
+ */
103
+ export const SKY_CODE_FEED_BASE_ENV = 'OVERSKY_SKY_CODE_UPDATE_FEED_BASE';
104
+ export function skyCodeFeedBase(env = process.env) {
105
+ const override = (env[SKY_CODE_FEED_BASE_ENV] || '').trim();
106
+ return (override || DEFAULT_SKY_CODE_FEED_BASE).replace(/\/+$/, '');
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skrr-ai/auth-core",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Shared auth substrate (token store, refresh, scheduler, fd handoff) for the OverSky daemon and CLI.",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "license": "UNLICENSED",
65
65
  "author": "OverSky",
66
- "homepage": "https://oversky.ai",
66
+ "homepage": "https://skrr.ai",
67
67
  "bugs": {
68
68
  "url": "https://github.com/dush1023/OverSky/issues"
69
69
  },