@skrr-ai/cli 0.1.10 → 0.1.12

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 (120) hide show
  1. package/dist/base-command.d.ts +1 -13
  2. package/dist/base-command.js +78 -1
  3. package/dist/commands/browser/skill/show.js +7 -1
  4. package/dist/commands/code/index.d.ts +1 -0
  5. package/dist/commands/code/index.js +9 -1
  6. package/dist/commands/commitments/analytics/index.js +2 -0
  7. package/dist/commands/daemon/byok.d.ts +1 -0
  8. package/dist/commands/daemon/byok.js +2 -1
  9. package/dist/commands/daemon/install.d.ts +15 -0
  10. package/dist/commands/daemon/install.js +39 -2
  11. package/dist/commands/daemon/login.d.ts +31 -0
  12. package/dist/commands/daemon/login.js +56 -0
  13. package/dist/commands/daemon/restart.d.ts +9 -0
  14. package/dist/commands/daemon/restart.js +36 -0
  15. package/dist/commands/daemon/start.d.ts +1 -0
  16. package/dist/commands/daemon/start.js +2 -1
  17. package/dist/commands/daemon/status.d.ts +1 -0
  18. package/dist/commands/daemon/status.js +2 -1
  19. package/dist/commands/daemon/stop.d.ts +1 -0
  20. package/dist/commands/daemon/stop.js +2 -1
  21. package/dist/commands/daemon/uninstall.d.ts +1 -0
  22. package/dist/commands/daemon/uninstall.js +2 -1
  23. package/dist/commands/daemon/unlock.d.ts +9 -0
  24. package/dist/commands/daemon/unlock.js +33 -0
  25. package/dist/commands/goals/key-results/create.js +32 -1
  26. package/dist/commands/goals/key-results/update.d.ts +11 -0
  27. package/dist/commands/goals/key-results/update.js +80 -2
  28. package/dist/commands/goals/plan-now.d.ts +54 -2
  29. package/dist/commands/goals/plan-now.js +175 -18
  30. package/dist/commands/goals/planner-config.d.ts +60 -9
  31. package/dist/commands/goals/planner-config.js +82 -34
  32. package/dist/commands/goals/revisions.js +17 -0
  33. package/dist/commands/goals/show.d.ts +17 -0
  34. package/dist/commands/goals/show.js +90 -3
  35. package/dist/commands/login.js +53 -0
  36. package/dist/commands/spaces/create.js +2 -1
  37. package/dist/commands/spaces/index.js +9 -1
  38. package/dist/commands/spaces/list.d.ts +18 -0
  39. package/dist/commands/spaces/list.js +57 -7
  40. package/dist/commands/spaces/show.js +4 -1
  41. package/dist/commands/spaces/summary.d.ts +4 -0
  42. package/dist/commands/spaces/summary.js +77 -1
  43. package/dist/commands/spaces/update.d.ts +4 -0
  44. package/dist/commands/spaces/update.js +38 -1
  45. package/dist/commands/tasks/actionability.js +40 -1
  46. package/dist/commands/tasks/activity.d.ts +29 -0
  47. package/dist/commands/tasks/activity.js +47 -0
  48. package/dist/commands/tasks/complete.d.ts +47 -0
  49. package/dist/commands/tasks/complete.js +159 -12
  50. package/dist/commands/tasks/create.d.ts +26 -0
  51. package/dist/commands/tasks/create.js +60 -1
  52. package/dist/commands/tasks/events/append.d.ts +2 -0
  53. package/dist/commands/tasks/events/append.js +41 -10
  54. package/dist/commands/tasks/events/list.js +60 -9
  55. package/dist/commands/tasks/output.js +22 -2
  56. package/dist/commands/tasks/ready.d.ts +38 -0
  57. package/dist/commands/tasks/ready.js +37 -0
  58. package/dist/commands/tasks/runs.d.ts +22 -0
  59. package/dist/commands/tasks/runs.js +110 -2
  60. package/dist/commands/tasks/show.d.ts +28 -0
  61. package/dist/commands/tasks/show.js +61 -0
  62. package/dist/commands/tasks/timeline.d.ts +7 -0
  63. package/dist/commands/tasks/timeline.js +34 -3
  64. package/dist/commands/tasks/update.d.ts +20 -0
  65. package/dist/commands/tasks/update.js +38 -1
  66. package/dist/commands/whoami.d.ts +40 -0
  67. package/dist/commands/whoami.js +57 -9
  68. package/dist/commands/wiki/ls.d.ts +23 -0
  69. package/dist/commands/wiki/ls.js +63 -10
  70. package/dist/commands/wiki/mv.d.ts +70 -0
  71. package/dist/commands/wiki/mv.js +198 -4
  72. package/dist/commands/wiki/rm.js +12 -1
  73. package/dist/commands/wiki/write.js +35 -3
  74. package/dist/help.d.ts +27 -0
  75. package/dist/help.js +48 -0
  76. package/dist/hooks/command-not-found.d.ts +31 -0
  77. package/dist/hooks/command-not-found.js +12 -80
  78. package/dist/lib/command-miss.d.ts +60 -0
  79. package/dist/lib/command-miss.js +128 -0
  80. package/dist/lib/commitment-analytics.d.ts +10 -0
  81. package/dist/lib/commitment-analytics.js +10 -0
  82. package/dist/lib/config.js +6 -2
  83. package/dist/lib/daemon-installer.d.ts +62 -0
  84. package/dist/lib/daemon-installer.js +247 -0
  85. package/dist/lib/daemon-setup.d.ts +49 -0
  86. package/dist/lib/daemon-setup.js +103 -0
  87. package/dist/lib/daemonBroker.d.ts +7 -1
  88. package/dist/lib/daemonBroker.js +71 -0
  89. package/dist/lib/daemonHandoff.d.ts +47 -0
  90. package/dist/lib/daemonHandoff.js +304 -0
  91. package/dist/lib/exec-oversky.d.ts +30 -0
  92. package/dist/lib/exec-oversky.js +41 -0
  93. package/dist/lib/format.d.ts +7 -0
  94. package/dist/lib/format.js +27 -5
  95. package/dist/lib/login.js +22 -2
  96. package/dist/lib/sky-code.js +1 -1
  97. package/dist/lib/task-transcript.d.ts +9 -0
  98. package/dist/lib/task-transcript.js +13 -2
  99. package/dist/lib/tasks.d.ts +34 -0
  100. package/dist/lib/tasks.js +70 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +21 -1
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.d.ts +87 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.js +94 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +161 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +235 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -0
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.d.ts +87 -0
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.js +91 -0
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +161 -0
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +227 -0
  117. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  118. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  119. package/oclif.manifest.json +1831 -1598
  120. package/package.json +5 -3
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * releaseKeys.ts — ed25519 public keys pinned into the daemon binary AND the
4
+ * CLI that installs it.
5
+ *
6
+ * These are the trust anchors the self-updater (`update.ts`) uses to verify
7
+ * the signed release manifest (`daemon/latest/daemon-version.json`) and the
8
+ * downloaded binary artifact OFFLINE, before swapping its own binary. The
9
+ * matching PRIVATE half lives ONLY in CI as the Secrets Manager secret
10
+ * `OVERSKY_RELEASE_SIGNING_KEY` (ed25519, base64 PKCS8) — it is never checked
11
+ * into the repo and never shipped in the binary.
12
+ *
13
+ * CURRENT STATE: a key is pinned (`daemon-2026-07`, provisioned 2026-07-13), so
14
+ * self-update signature verification is FAIL-CLOSED — an unsigned or
15
+ * badly-signed manifest or binary aborts the update.
16
+ *
17
+ * READ THE NEXT PARAGRAPH BEFORE EMPTYING THIS ARRAY.
18
+ *
19
+ * The empty array is not a safe default; it is a DISABLED SECURITY CONTROL.
20
+ * With no key pinned, `update.ts` logs a warning and proceeds on TLS + SHA256
21
+ * only — and the SHA256 comes from the same manifest fetched over the same
22
+ * channel, so it defends against corruption in transit, not against a
23
+ * compromised or spoofed update feed. That posture (mirrored from the desktop
24
+ * `MANIFEST_PUBLIC_KEY=''` default) existed because the keypair had not been
25
+ * minted yet. It has been minted. Removing the last entry below silently
26
+ * re-disables offline verification on every daemon that ships the change, and
27
+ * nothing in the build fails to tell you.
28
+ *
29
+ * To add a key (rotation, or re-provisioning after a compromise) run
30
+ * `node scripts/generate-release-keypair.mjs`. It mints the ed25519 keypair,
31
+ * prints the ready-to-paste `{ keyId, publicKeySpkiB64 }` line for the array
32
+ * below, and prints the commands to store the PRIVATE half in the GitHub
33
+ * secret `OVERSKY_RELEASE_SIGNING_KEY` + AWS Secrets Manager. Never commit the
34
+ * private half; the CI signer reads `OVERSKY_RELEASE_SIGNING_KEY` and MUST
35
+ * import `buildArtifactMessage` / `buildManifestMessage` from
36
+ * `release-manifest.ts` rather than reimplementing them, so signer and verifier
37
+ * can never drift. Add the *next* key alongside the current one BEFORE
38
+ * rotating, so an in-flight rotation verifies against either — the full
39
+ * procedure is the rotation runbook on the array below.
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.OVERSKY_RELEASE_PUBLIC_KEYS = void 0;
43
+ /**
44
+ * Pinned release-signing public keys, newest first. Verification tries each
45
+ * key in turn (key rotation: keep current + next together during a rotation
46
+ * window). Non-empty → fail-closed, which is the state today. Empty →
47
+ * verification DISABLED with only a logged warning; see the file header before
48
+ * ever letting this array become empty.
49
+ *
50
+ * ZERO-DOWNTIME KEY-ROTATION RUNBOOK (config-only, no code change):
51
+ * 1. Mint the next keypair: `node scripts/generate-release-keypair.mjs --force`.
52
+ * 2. Pin its public half ABOVE the current key in the array below, and ship
53
+ * that daemon build so the fleet trusts BOTH keys.
54
+ * 3. Store the new private half as the GH Actions secret
55
+ * OVERSKY_RELEASE_SIGNING_KEY_NEXT (+ optional OVERSKY_RELEASE_KEY_ID_NEXT
56
+ * label) — the signer then DUAL-SIGNS every release, emitting the additive
57
+ * v2 `sigs[]` envelope (scalar `sig` stays = the current/primary key so
58
+ * daemons that haven't picked up step 2 yet still verify).
59
+ * 4. Cut one release during the overlap window; it verifies for daemons
60
+ * pinning the old key, the new key, or both (see release-manifest.test.ts
61
+ * "ROTATION headroom").
62
+ * 5. Once the new-key build has saturated the fleet: promote NEXT→primary
63
+ * (rename the secret to OVERSKY_RELEASE_SIGNING_KEY), drop the old key from
64
+ * the array, and remove OVERSKY_RELEASE_SIGNING_KEY_NEXT. No downtime, and
65
+ * no further code change — the multi-sig machinery is already shipped.
66
+ */
67
+ /**
68
+ * NOT ROTATED FOR THE skrr RENAME. Owner decision, 2026-08-31.
69
+ *
70
+ * A rename is not a compromise. Custody is already established (GH Actions
71
+ * secret `OVERSKY_RELEASE_SIGNING_KEY` + AWS Secrets Manager), the keyId
72
+ * `daemon-2026-07` is a date, not a brand, and no user ever sees any of it.
73
+ *
74
+ * The rotation procedure documented above is shipped and correct — it just has
75
+ * nothing to do here. Running it would cost a fleet-saturation window (step 5)
76
+ * and buy nothing, and the window is exactly when a self-update path is most
77
+ * fragile. Rotate on a compromise or an expiry, not on a rename.
78
+ *
79
+ * The signed message prefix (`oversky-daemon-manifest`) stays for the same
80
+ * reason: it lives inside the signed bytes, so changing it breaks verification
81
+ * between old and new peers with zero user-visible benefit — the same call made
82
+ * for `X-Oversky-Origin`.
83
+ */
84
+ exports.OVERSKY_RELEASE_PUBLIC_KEYS = [
85
+ // ── NEXT key goes ABOVE this line during a rotation (newest first). ──
86
+ // Provisioned 2026-07-13. Private half: GH Actions secret
87
+ // OVERSKY_RELEASE_SIGNING_KEY + AWS Secrets Manager
88
+ // (us-east-1: oversky/daemon/release-signing-key). Self-update signature
89
+ // verification is now FAIL-CLOSED.
90
+ {
91
+ keyId: 'daemon-2026-07',
92
+ publicKeySpkiB64: 'MCowBQYDK2VwAyEA7ArbRnkbRW354XZ4MNS1MRLCwrTFauQzsw4GP27mMfI=',
93
+ },
94
+ ];
@@ -0,0 +1,161 @@
1
+ import { type ReleaseKey } from './releaseKeys.js';
2
+ export type { ReleaseKey } from './releaseKeys.js';
3
+ /**
4
+ * One entry in a v2 multi-signature envelope. `sig` is a base64 ed25519
5
+ * signature over the EXACT SAME bytes as the v1 scalar `sig` (buildArtifact/
6
+ * ManifestMessage) — v2 is an envelope change, not a wire-format change. The
7
+ * `keyId` is an ADVISORY label only: verification trial-verifies every sig
8
+ * against every pinned key and NEVER consults keyId, so a mislabeled keyId can
9
+ * neither grant nor withhold trust (this structurally defeats keyId
10
+ * substitution). It exists for operator diagnostics ("which key signed this?").
11
+ */
12
+ export interface ManifestSignature {
13
+ keyId: string;
14
+ /** base64 ed25519 signature over the same bytes as the v1 `sig`. */
15
+ sig: string;
16
+ }
17
+ /**
18
+ * v-delta (optional): a differential-update patch that transforms a specific
19
+ * PRIOR version's binary into this release's binary — a brotli-compressed
20
+ * fossil delta. UNSIGNED, exactly like `keyId`/`size`/`sigs` (NOT part of
21
+ * buildArtifactMessage/buildManifestMessage), so it never changes the signed
22
+ * bytes and #782's envelope invariant holds. Trust comes SOLELY from verifying
23
+ * the RECONSTRUCTED binary against the signed `sha256` + ed25519 gates: a
24
+ * tampered/garbage delta yields a non-matching binary and the daemon falls back
25
+ * to the full signed download. `sha256` here is TRANSPORT integrity of the patch
26
+ * file only — NEVER an install gate.
27
+ */
28
+ export interface DeltaDescriptor {
29
+ /** Absolute public URL of the brotli-compressed fossil delta patch. */
30
+ url: string;
31
+ /** Lowercase hex sha256 of the patch file — transport integrity, not trust. */
32
+ sha256: string;
33
+ /** Size of the patch file in bytes. */
34
+ size: number;
35
+ }
36
+ /** One downloadable, immutable binary for a single `<platform>-<arch>` target. */
37
+ export interface PlatformArtifact {
38
+ /** Bare filename, e.g. `oversky-darwin-arm64`. */
39
+ file: string;
40
+ /** Absolute public URL under the CloudFront feed. */
41
+ url: string;
42
+ /** Lowercase hex sha256 of the binary bytes. */
43
+ sha256: string;
44
+ /** Size in bytes. */
45
+ size: number;
46
+ /** base64 ed25519 signature over buildArtifactMessage(...) — the PRIMARY key. */
47
+ sig: string;
48
+ /**
49
+ * v2 (optional): EVERY signature (primary + rotation key) over the same
50
+ * bytes as `sig`. Emitted ONLY during a key-rotation window (>1 signing key);
51
+ * absent in steady single-key state, so a normal manifest is byte-identical
52
+ * to v1. When present it is the authoritative set — see verifyWithKeysMulti.
53
+ */
54
+ sigs?: ManifestSignature[];
55
+ /**
56
+ * v-delta (optional, UNSIGNED): differential-update patches keyed by the
57
+ * from-version they apply to (e.g. `"0.8.0"`). Absent in steady state. See
58
+ * DeltaDescriptor — the reconstructed binary is verified against `sha256`
59
+ * above, so deltas add NO trust surface.
60
+ */
61
+ deltas?: Record<string, DeltaDescriptor>;
62
+ }
63
+ /**
64
+ * The signed release manifest. `platforms` is keyed by `<platform>-<arch>`
65
+ * (e.g. `darwin-arm64`, `linux-x64`). The top-level `sig` covers
66
+ * buildManifestMessage(manifest-without-sig).
67
+ */
68
+ export interface DaemonVersionManifest<TPolicy = unknown> {
69
+ schemaVersion: number;
70
+ version: string;
71
+ minimum: string;
72
+ forceUpdate: boolean;
73
+ keyId: string;
74
+ platforms: Record<string, PlatformArtifact>;
75
+ /**
76
+ * base64 ed25519 signature over buildManifestMessage(this-without-sig) — the
77
+ * PRIMARY key. Kept populated even in v2 so an OLD (v1-only) daemon still
78
+ * verifies against the key the lagging fleet trusts.
79
+ */
80
+ sig: string;
81
+ /**
82
+ * v2 (optional): EVERY manifest signature (primary + rotation key). Emitted
83
+ * ONLY during a rotation window; when present it is authoritative — see
84
+ * verifyWithKeysMulti. `schemaVersion` stays a reader hint (unsigned, inert);
85
+ * v2 is detected by `Array.isArray(sigs)`, not by the version number.
86
+ */
87
+ sigs?: ManifestSignature[];
88
+ /**
89
+ * Independently signed release decision envelope. It is intentionally not
90
+ * part of the v1 manifest message so existing daemons can ignore it; newer
91
+ * daemons verify this nested signature before honouring compatibility or
92
+ * rollout controls.
93
+ */
94
+ policy?: TPolicy;
95
+ }
96
+ /**
97
+ * Canonical bytes signed for a single platform artifact. Newline-joined, fixed
98
+ * field order, `v1` tag. `sha256` is lowercased so the message is stable
99
+ * regardless of the hex casing the caller supplies.
100
+ */
101
+ export declare function buildArtifactMessage(a: {
102
+ platform: string;
103
+ version: string;
104
+ sha256: string;
105
+ url: string;
106
+ }): string;
107
+ /**
108
+ * Canonical bytes signed for the whole manifest (excluding the top-level
109
+ * `sig`). Platform keys are sorted so the message is independent of JSON key
110
+ * order; each contributes `<platform>=<sha256-lower>`. `forceUpdate` is
111
+ * serialized strictly (`String(forceUpdate === true)` → `"true"`/`"false"`).
112
+ */
113
+ export declare function buildManifestMessage(m: {
114
+ version: string;
115
+ minimum: string;
116
+ forceUpdate: boolean;
117
+ platforms: Record<string, {
118
+ sha256: string;
119
+ }>;
120
+ }): string;
121
+ /**
122
+ * Discriminated verification result:
123
+ * - `{ ok: true }` — signature verified against a pinned key.
124
+ * - `{ ok: false, reason }` — keys are present but nothing verified (FAIL-CLOSED).
125
+ * - `{ disabled: true }` — no keys pinned; verification is intentionally off.
126
+ */
127
+ export type VerifyResult = {
128
+ ok: true;
129
+ } | {
130
+ ok: false;
131
+ reason: string;
132
+ } | {
133
+ disabled: true;
134
+ };
135
+ /**
136
+ * Upper bound on signatures we will trial-verify per artifact/manifest. A
137
+ * rotation window needs at most 2–3 (current + next). `sigs[]` is NOT part of
138
+ * the signed bytes, so an attacker can pad it; this cap bounds the verify work
139
+ * they can force to O(MAX_SIGS × pinnedKeys) — a few hundred microseconds even
140
+ * at the cap. A valid signature placed beyond the cap is treated as absent.
141
+ */
142
+ export declare const MAX_SIGS = 8;
143
+ /**
144
+ * Verify a single platform artifact's `sig` over buildArtifactMessage(...).
145
+ * `platform` is the `<platform>-<arch>` key; `version` is the manifest version
146
+ * the artifact belongs to. Defaults to the baked-in pinned keys.
147
+ */
148
+ export declare function verifyArtifact(platform: string, version: string, artifact: PlatformArtifact, keys?: readonly ReleaseKey[]): VerifyResult;
149
+ /**
150
+ * Verify the manifest's top-level `sig` over buildManifestMessage(this).
151
+ * Defaults to the baked-in pinned keys.
152
+ */
153
+ export declare function verifyManifest(manifest: DaemonVersionManifest, keys?: readonly ReleaseKey[]): VerifyResult;
154
+ /**
155
+ * Look up a delta patch that transforms `fromVersion`'s binary into this
156
+ * artifact's binary. Returns null when none is advertised or the descriptor is
157
+ * malformed — the caller then does a full download. PURE; validates shape only
158
+ * (the delta is UNSIGNED, so trust comes from verifying the RECONSTRUCTED binary
159
+ * against the signed `artifact.sha256`, not from this metadata).
160
+ */
161
+ export declare function getDeltaFor(artifact: PlatformArtifact, fromVersion: string): DeltaDescriptor | null;
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_SIGS = void 0;
4
+ exports.buildArtifactMessage = buildArtifactMessage;
5
+ exports.buildManifestMessage = buildManifestMessage;
6
+ exports.verifyArtifact = verifyArtifact;
7
+ exports.verifyManifest = verifyManifest;
8
+ exports.getDeltaFor = getDeltaFor;
9
+ /**
10
+ * release-manifest.ts — signed daemon release manifest: types, canonical
11
+ * signature messages, and offline ed25519 verification.
12
+ *
13
+ * PURE: depends only on `node:crypto` (plus the sibling `release-keys.ts` for
14
+ * the pinned trust anchors). No Electron, no logger, no fs — so it is
15
+ * unit-testable in isolation and safe to import from the CI release signer.
16
+ *
17
+ * SINGLE SOURCE OF TRUTH: `buildArtifactMessage` and `buildManifestMessage`
18
+ * define the exact bytes that get signed. The CI signer MUST import these
19
+ * functions rather than reimplement them — that is what eliminates
20
+ * signer/verifier drift. Any change here is a wire-format change and must bump
21
+ * the embedded `v1` tag.
22
+ *
23
+ * Mechanics mirror `desktop/src/main/updater/policySignature.ts`: the public
24
+ * key is base64 DER-SPKI, verified with `crypto.verify(null, ...)` (ed25519
25
+ * takes no digest algorithm).
26
+ *
27
+ * WHY IT LIVES IN auth-core NOW
28
+ *
29
+ * It lived in `daemon/src/release-manifest.ts`, and `skyCodeChannels.ts` beside
30
+ * it recorded the reason for stopping there: "the installer, the signature
31
+ * verification and the promotion machinery stay in the daemon, which is the
32
+ * only process that performs them." That premise held exactly until the CLI
33
+ * had to install the daemon — `skrr daemon install` on a machine with no
34
+ * `skrrd` cannot ask the daemon to verify the daemon.
35
+ *
36
+ * So this is the same road that move was on, not a reversal of it. A MIRROR in
37
+ * the CLI is the alternative and is the wrong one, with precedent: OSK-3894
38
+ * shipped a mirrored harness-trust table and OSK-3897 removed it, because a
39
+ * mirror proves two copies agree rather than proving there is one copy. For a
40
+ * TRUST ROOT that argument is not stylistic — a second copy of the pinned keys
41
+ * is a second thing that can be edited, and the edit that matters (emptying the
42
+ * array) disables verification silently.
43
+ *
44
+ * `SignedReleasePolicy` did NOT move. It is the daemon's own decision envelope,
45
+ * so the manifest is generic over it and the daemon narrows it back.
46
+ */
47
+ const node_crypto_1 = require("node:crypto");
48
+ const releaseKeys_js_1 = require("./releaseKeys.js");
49
+ // ---------------------------------------------------------------------------
50
+ // Canonical signature messages — THE contract. Do not reformat casually.
51
+ // ---------------------------------------------------------------------------
52
+ /**
53
+ * Canonical bytes signed for a single platform artifact. Newline-joined, fixed
54
+ * field order, `v1` tag. `sha256` is lowercased so the message is stable
55
+ * regardless of the hex casing the caller supplies.
56
+ */
57
+ function buildArtifactMessage(a) {
58
+ return [
59
+ 'oversky-daemon-artifact',
60
+ 'v1',
61
+ a.platform,
62
+ a.version,
63
+ a.sha256.toLowerCase(),
64
+ a.url,
65
+ ].join('\n');
66
+ }
67
+ /**
68
+ * Canonical bytes signed for the whole manifest (excluding the top-level
69
+ * `sig`). Platform keys are sorted so the message is independent of JSON key
70
+ * order; each contributes `<platform>=<sha256-lower>`. `forceUpdate` is
71
+ * serialized strictly (`String(forceUpdate === true)` → `"true"`/`"false"`).
72
+ */
73
+ function buildManifestMessage(m) {
74
+ return [
75
+ 'oversky-daemon-manifest',
76
+ 'v1',
77
+ m.version,
78
+ m.minimum,
79
+ String(m.forceUpdate === true),
80
+ ...Object.keys(m.platforms)
81
+ .sort()
82
+ .map((p) => p + '=' + m.platforms[p].sha256.toLowerCase()),
83
+ ].join('\n');
84
+ }
85
+ /**
86
+ * Try `sigB64` against every pinned key (supports key rotation: current +
87
+ * next). Never throws — a malformed key or signature is just a failed attempt.
88
+ * Empty key set ⇒ `{ disabled: true }` so the caller can warn-and-proceed
89
+ * (mirrors desktop `MANIFEST_PUBLIC_KEY=''`).
90
+ */
91
+ function verifyWithKeys(message, sigB64, keys) {
92
+ if (keys.length === 0)
93
+ return { disabled: true };
94
+ if (!sigB64)
95
+ return { ok: false, reason: 'missing signature' };
96
+ const msg = Buffer.from(message, 'utf8');
97
+ const sig = Buffer.from(sigB64, 'base64');
98
+ for (const k of keys) {
99
+ try {
100
+ const key = (0, node_crypto_1.createPublicKey)({
101
+ key: Buffer.from(k.publicKeySpkiB64, 'base64'),
102
+ format: 'der',
103
+ type: 'spki',
104
+ });
105
+ if ((0, node_crypto_1.verify)(null, msg, key, sig)) {
106
+ return { ok: true };
107
+ }
108
+ }
109
+ catch {
110
+ // Malformed key material or verify error → try the next pinned key.
111
+ }
112
+ }
113
+ return { ok: false, reason: 'signature did not verify against any pinned key' };
114
+ }
115
+ /**
116
+ * Upper bound on signatures we will trial-verify per artifact/manifest. A
117
+ * rotation window needs at most 2–3 (current + next). `sigs[]` is NOT part of
118
+ * the signed bytes, so an attacker can pad it; this cap bounds the verify work
119
+ * they can force to O(MAX_SIGS × pinnedKeys) — a few hundred microseconds even
120
+ * at the cap. A valid signature placed beyond the cap is treated as absent.
121
+ */
122
+ exports.MAX_SIGS = 8;
123
+ /**
124
+ * v2 acceptance: `{ ok: true }` if ANY provided signature verifies against ANY
125
+ * pinned key. This composes the two rotation axes — verifyWithKeys already
126
+ * loops every pinned KEY (key rotation); this adds the every-SIGNATURE loop —
127
+ * so a release dual-signed by [old, new] verifies for a daemon pinning only
128
+ * [old], only [new], or both. That is what makes rotation zero-downtime.
129
+ *
130
+ * Posture is identical to v1:
131
+ * - empty pinned-key set ⇒ `{ disabled: true }` (fail-OPEN, unconfigured);
132
+ * - keys present but nothing verifies ⇒ `{ ok: false }` (fail-CLOSED);
133
+ * and it never throws (per-key errors are swallowed inside verifyWithKeys).
134
+ *
135
+ * Security: `sigs[]` is attacker-malleable (not in the signed bytes), so trust
136
+ * comes SOLELY from a real ed25519 verify against a pinned key. Padding with
137
+ * garbage is inert; stripping every entry is at worst a denial-of-update
138
+ * (liveness), never a forged accept. keyId is never consulted → no keyId
139
+ * substitution. Each sig independently binds version/minimum/forceUpdate/
140
+ * platform-shas, so cross-manifest splicing is blocked exactly as in v1.
141
+ */
142
+ function verifyWithKeysMulti(message, sigs, keys) {
143
+ if (keys.length === 0)
144
+ return { disabled: true };
145
+ let sawSignature = false;
146
+ // Bound attacker-supplied work; a real rotation never needs more than a few.
147
+ for (const entry of sigs.slice(0, exports.MAX_SIGS)) {
148
+ const sigB64 = typeof entry?.sig === 'string' ? entry.sig : '';
149
+ if (!sigB64)
150
+ continue;
151
+ sawSignature = true;
152
+ const r = verifyWithKeys(message, sigB64, keys); // keys non-empty ⇒ never {disabled}
153
+ if ('ok' in r && r.ok)
154
+ return { ok: true };
155
+ }
156
+ return {
157
+ ok: false,
158
+ reason: sawSignature
159
+ ? 'no provided signature verified against any pinned key'
160
+ : 'missing signature',
161
+ };
162
+ }
163
+ /**
164
+ * Verify a single platform artifact's `sig` over buildArtifactMessage(...).
165
+ * `platform` is the `<platform>-<arch>` key; `version` is the manifest version
166
+ * the artifact belongs to. Defaults to the baked-in pinned keys.
167
+ */
168
+ function verifyArtifact(platform, version, artifact, keys = releaseKeys_js_1.OVERSKY_RELEASE_PUBLIC_KEYS) {
169
+ let message;
170
+ try {
171
+ message = buildArtifactMessage({
172
+ platform,
173
+ version,
174
+ sha256: artifact.sha256,
175
+ url: artifact.url,
176
+ });
177
+ }
178
+ catch (err) {
179
+ // A malformed artifact (missing string fields) must fail-closed as a clean
180
+ // verification failure, never an uncaught throw.
181
+ return { ok: false, reason: `malformed artifact: ${err.message}` };
182
+ }
183
+ // v2: a present `sigs[]` is authoritative (any-of-N). Otherwise the v1 scalar
184
+ // path — byte-for-byte the prior behavior.
185
+ if (Array.isArray(artifact.sigs) && artifact.sigs.length > 0) {
186
+ return verifyWithKeysMulti(message, artifact.sigs, keys);
187
+ }
188
+ return verifyWithKeys(message, artifact.sig, keys);
189
+ }
190
+ /**
191
+ * Verify the manifest's top-level `sig` over buildManifestMessage(this).
192
+ * Defaults to the baked-in pinned keys.
193
+ */
194
+ function verifyManifest(manifest, keys = releaseKeys_js_1.OVERSKY_RELEASE_PUBLIC_KEYS) {
195
+ let message;
196
+ try {
197
+ message = buildManifestMessage(manifest);
198
+ }
199
+ catch (err) {
200
+ // A malformed manifest (e.g. a platform entry missing sha256) must
201
+ // fail-closed as a clean verification failure, never an uncaught throw.
202
+ return { ok: false, reason: `malformed manifest: ${err.message}` };
203
+ }
204
+ // v2: a present `sigs[]` is authoritative (any-of-N). Otherwise the v1 scalar
205
+ // path — byte-for-byte the prior behavior.
206
+ if (Array.isArray(manifest.sigs) && manifest.sigs.length > 0) {
207
+ return verifyWithKeysMulti(message, manifest.sigs, keys);
208
+ }
209
+ return verifyWithKeys(message, manifest.sig, keys);
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // Differential updates (delta metadata lookup) — UNSIGNED, optimization only.
213
+ // ---------------------------------------------------------------------------
214
+ /**
215
+ * Look up a delta patch that transforms `fromVersion`'s binary into this
216
+ * artifact's binary. Returns null when none is advertised or the descriptor is
217
+ * malformed — the caller then does a full download. PURE; validates shape only
218
+ * (the delta is UNSIGNED, so trust comes from verifying the RECONSTRUCTED binary
219
+ * against the signed `artifact.sha256`, not from this metadata).
220
+ */
221
+ function getDeltaFor(artifact, fromVersion) {
222
+ const deltas = artifact.deltas;
223
+ if (!deltas || typeof deltas !== 'object')
224
+ return null;
225
+ const d = deltas[fromVersion];
226
+ if (!d ||
227
+ typeof d.url !== 'string' ||
228
+ typeof d.sha256 !== 'string' ||
229
+ typeof d.size !== 'number' ||
230
+ !Number.isFinite(d.size) ||
231
+ d.size <= 0) {
232
+ return null;
233
+ }
234
+ return d;
235
+ }
@@ -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
+ }
@@ -36,3 +36,6 @@ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE
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
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 { OVERSKY_RELEASE_PUBLIC_KEYS, type ReleaseKey } from './releaseKeys.js';
40
+ export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
41
+ export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
@@ -143,3 +143,13 @@ export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATI
143
143
  reads, copies or removes what it finds (§7.2, §8.4). */
144
144
  export { findLegacyLocalState, describeLegacyState, } from './legacyStatePreflight.js';
145
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
+ // Daemon release trust: the pinned ed25519 keys and the canonical signature
147
+ // messages. Shared because BOTH the daemon (self-update) and the CLI
148
+ // (`skrr daemon install`, which runs when there is no daemon to ask) verify the
149
+ // same manifest against the same root. See releaseManifest.ts for why a mirror
150
+ // would be the wrong shape here.
151
+ export { OVERSKY_RELEASE_PUBLIC_KEYS } from './releaseKeys.js';
152
+ export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, } from './releaseManifest.js';
153
+ // The one definition of the skrr root, shared by `skrr` and `skrrd`. See the
154
+ // module for why re-deriving it kept producing divergences.
155
+ export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';