@skrr-ai/cli 0.1.123 → 0.1.125

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 (89) hide show
  1. package/dist/commands/agents/chat.d.ts +26 -0
  2. package/dist/commands/agents/chat.js +48 -10
  3. package/dist/commands/agents/delete.d.ts +8 -0
  4. package/dist/commands/agents/delete.js +16 -0
  5. package/dist/commands/context.js +7 -0
  6. package/dist/commands/search/workspace.js +1 -1
  7. package/dist/commands/store/gallery-admissions/approve.js +1 -1
  8. package/dist/commands/store/gallery-admissions/preview.js +1 -1
  9. package/dist/commands/store/gallery-admissions/refuse.js +1 -1
  10. package/dist/commands/store/gallery-admissions/show.js +1 -1
  11. package/dist/generated/agent-guides.json +19 -19
  12. package/dist/lib/agentic-stream.js +15 -1
  13. package/dist/lib/auth-core-init.d.ts +15 -2
  14. package/dist/lib/auth-core-init.js +47 -6
  15. package/dist/lib/command-hygiene.js +30 -1
  16. package/dist/lib/daemonBrokerRefusal.js +5 -1
  17. package/dist/lib/keychain.js +41 -4
  18. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +5 -1
  19. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +1 -1
  20. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +13 -28
  21. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.d.ts +133 -4
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +387 -90
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.d.ts +106 -7
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKey.js +77 -19
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.d.ts +33 -0
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHandle.js +77 -0
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.d.ts +68 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyHardware.js +264 -0
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.d.ts +31 -0
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceKeyServerSupport.js +90 -0
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dpop.js +2 -0
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/independentMachineReservation.js +2 -0
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +7 -3
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +102 -0
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.d.ts +1 -1
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/index.js +6 -0
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.d.ts +18 -0
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/linux.js +8 -0
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.d.ts +22 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/macos.js +11 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.d.ts +43 -3
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/kek/masterKekService.js +33 -3
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.d.ts +154 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/liveState.js +281 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +6 -3
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +8 -5
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +7 -27
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +1 -1
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +12 -10
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.d.ts +31 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/stateFiles.js +89 -0
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +5 -1
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +4 -1
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +15 -37
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.d.ts +133 -4
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +536 -101
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.d.ts +106 -7
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKey.js +131 -27
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.d.ts +33 -0
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHandle.js +81 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.d.ts +68 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyHardware.js +317 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.d.ts +31 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceKeyServerSupport.js +86 -0
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dpop.js +4 -0
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/independentMachineReservation.js +4 -0
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +7 -3
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +13 -3
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.d.ts +1 -1
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/index.js +1 -1
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.d.ts +18 -0
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/linux.js +15 -0
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.d.ts +22 -0
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/macos.js +20 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.d.ts +43 -3
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/kek/masterKekService.js +104 -8
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.d.ts +154 -0
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/liveState.js +386 -0
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +6 -3
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +11 -5
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +6 -35
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +1 -1
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +14 -10
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.d.ts +31 -0
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/stateFiles.js +73 -0
  86. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  87. package/dist/node_modules/@skrr-ai/data-provider/index.js +1094 -1067
  88. package/oclif.manifest.json +16 -3
  89. package/package.json +4 -1
@@ -50,6 +50,33 @@ export interface Ed25519PublicJwk {
50
50
  export interface Ed25519PrivateJwk extends Ed25519PublicJwk {
51
51
  readonly d: string;
52
52
  }
53
+ /**
54
+ * NIST P-256 public JSON Web Key — RFC 7518 §6.2 ("Key Type 'EC'").
55
+ *
56
+ * The only curve a Secure Enclave or TPM will sign with, which is why a
57
+ * hardware-bound device key is a different `alg` (ES256) rather than a
58
+ * different storage backend for the same algorithm. `x` and `y` are the
59
+ * 32-byte affine coordinates in base64url-no-padding. There is deliberately
60
+ * no private form: a P-256 device key in this codebase is non-exportable, so
61
+ * nothing here ever holds its scalar.
62
+ */
63
+ export interface P256PublicJwk {
64
+ readonly kty: 'EC';
65
+ readonly crv: 'P-256';
66
+ readonly x: string;
67
+ readonly y: string;
68
+ }
69
+ /** Any public key a device proof may be signed with. */
70
+ export type DevicePublicJwk = Ed25519PublicJwk | P256PublicJwk;
71
+ /**
72
+ * The JWS algorithms a device proof may use. `EdDSA` is the software key every
73
+ * install has had since L11; `ES256` is the hardware-bound key. A server pins
74
+ * ONE per device and never requires a client to use an algorithm it did not
75
+ * announce.
76
+ */
77
+ export type DeviceKeyAlg = 'EdDSA' | 'ES256';
78
+ /** Every algorithm this build can verify, in the order a client prefers them. */
79
+ export declare const DEVICE_KEY_ALGS: readonly DeviceKeyAlg[];
53
80
  /**
54
81
  * The full keypair returned at generation time. Holding both forms here
55
82
  * is convenient for the daemon-side bridge — the public form is what we
@@ -57,6 +84,11 @@ export interface Ed25519PrivateJwk extends Ed25519PublicJwk {
57
84
  *
58
85
  * `thumbprint` is the RFC 7638 JWK thumbprint of the public key — the
59
86
  * canonical stable id we use as the device handle on the server.
87
+ *
88
+ * This is the SOFTWARE Ed25519 key only, and only the sealed-file store and
89
+ * key generation touch its private half. Everything that signs holds a
90
+ * `DeviceKeyHandle` (deviceKeyHandle.ts) instead, which cannot return the
91
+ * private scalar and so works unchanged for a key that has none.
60
92
  */
61
93
  export interface DeviceKeyPair {
62
94
  readonly publicJwk: Ed25519PublicJwk;
@@ -83,7 +115,23 @@ export declare function generateDeviceKeyPair(): DeviceKeyPair;
83
115
  * `kid` in JWS headers and as the registered identifier on the server's
84
116
  * `Daemon` table.
85
117
  */
86
- export declare function jwkThumbprint(publicJwk: Ed25519PublicJwk): string;
118
+ export declare function jwkThumbprint(publicJwk: DevicePublicJwk): string;
119
+ /**
120
+ * Structural check for a public device key: the right `kty`/`crv` pair, and
121
+ * coordinates that are exactly 32 bytes of base64url. It does not prove the
122
+ * point is on the curve (importing the key does that); it proves the value is
123
+ * safe to build a thumbprint from.
124
+ */
125
+ export declare function isDevicePublicJwk(value: unknown): value is DevicePublicJwk;
126
+ /** The JWS algorithm a public key signs with. */
127
+ export declare function algForJwk(jwk: DevicePublicJwk): DeviceKeyAlg;
128
+ /**
129
+ * A P-256 public JWK from the 64-byte `x || y` form a Secure Enclave reports
130
+ * (CryptoKit's `rawRepresentation`). Rejects any other length so a helper that
131
+ * returns an x9.63 point (65 bytes, leading 0x04) fails loudly instead of being
132
+ * mis-split into a key nothing can verify.
133
+ */
134
+ export declare function p256PublicJwkFromRaw(raw: Uint8Array): P256PublicJwk;
87
135
  /**
88
136
  * Construct an `Ed25519PublicJwk` from a base64url-encoded 32-byte
89
137
  * public point. Used by the persistence layer when reading a stored
@@ -124,13 +172,13 @@ export interface DeviceProofClaims {
124
172
  ath?: string;
125
173
  }
126
174
  export interface DeviceProofHeader {
127
- alg: 'EdDSA';
175
+ alg: DeviceKeyAlg;
128
176
  /** Custom `typ` so the server can refuse generic JWTs that happen
129
- * to be EdDSA-signed by a stolen device key. RFC 9449 uses
177
+ * to be signed by a stolen device key. RFC 9449 uses
130
178
  * `dpop+jwt`; we differentiate so OverSky proofs can't be
131
179
  * cross-replayed into a generic DPoP resource server. */
132
180
  typ: 'oversky-dpop+jwt';
133
- jwk: Ed25519PublicJwk;
181
+ jwk: DevicePublicJwk;
134
182
  }
135
183
  export interface BuildProofOptions {
136
184
  method: string;
@@ -172,11 +220,35 @@ export declare const DEVICE_PROOF_TYP = "oversky-dpop+jwt";
172
220
  * never verify as the other.
173
221
  */
174
222
  export declare function signEd25519Proof(privateKey: crypto.KeyObject, publicJwk: Ed25519PublicJwk, typ: string, opts: BuildProofOptions): string;
223
+ /**
224
+ * The `header.payload` half of a proof: everything the key signs. Split out so
225
+ * a key that cannot be handed to this process (a Secure Enclave key signs
226
+ * inside a helper) signs the very same bytes a software key does, and nothing
227
+ * about the wire format depends on where the signature came from.
228
+ *
229
+ * @internal Shared by `signEd25519Proof` and `signProofWith`.
230
+ */
231
+ export declare function proofSigningInput(alg: DeviceKeyAlg, publicJwk: DevicePublicJwk, typ: string, opts: BuildProofOptions): string;
232
+ /**
233
+ * Anything that can sign a device proof (a `DeviceKeySigner`): a software key held in this process,
234
+ * or a hardware key that signs elsewhere. `sign` returns the JWS signature for
235
+ * `alg` (RFC 7515 §A.3 / RFC 8037: raw r||s for ES256, the 64 bytes for EdDSA),
236
+ * never DER.
237
+ */
238
+ export interface DeviceKeySigner {
239
+ readonly alg: DeviceKeyAlg;
240
+ readonly publicJwk: DevicePublicJwk;
241
+ sign(signingInput: Uint8Array): Promise<Uint8Array>;
242
+ }
243
+ /** Sign a device-identity proof with any `DeviceKeySigner`. */
244
+ export declare function signProofWith(signer: DeviceKeySigner, opts: BuildProofOptions, typ?: string): Promise<string>;
175
245
  export type VerifyFailureReason = 'malformed' | 'bad_alg' | 'bad_typ' | 'bad_jwk' | 'sig_invalid' | 'iat_old' | 'iat_future' | 'htm_mismatch' | 'htu_mismatch' | 'rth_mismatch' | 'ath_mismatch' | 'pin_mismatch';
176
246
  export interface VerifySuccess {
177
247
  valid: true;
178
248
  claims: DeviceProofClaims;
179
- publicJwk: Ed25519PublicJwk;
249
+ publicJwk: DevicePublicJwk;
250
+ /** The algorithm the proof was signed with; always one `allowedAlgs` admitted. */
251
+ alg: DeviceKeyAlg;
180
252
  thumbprint: string;
181
253
  }
182
254
  export interface VerifyFailure {
@@ -199,8 +271,17 @@ export interface VerifyOptions {
199
271
  expectedAth?: string;
200
272
  /** Public JWK pinned at the server side. When provided, the proof's
201
273
  * inlined JWK MUST equal this — defends against a stolen valid
202
- * proof signed by a different (e.g., revoked) device. */
203
- pinnedPublicJwk?: Ed25519PublicJwk;
274
+ * proof signed by a different (e.g., revoked) device. Compared by
275
+ * thumbprint, so a pin on one algorithm never admits a key of another. */
276
+ pinnedPublicJwk?: DevicePublicJwk;
277
+ /**
278
+ * The algorithms this verification admits. Defaults to `['EdDSA']`: a caller
279
+ * that has not decided it accepts hardware-bound keys keeps exactly the
280
+ * behaviour it had before ES256 existed. A server that pins a device to ES256
281
+ * passes the algorithm of the PIN, not `DEVICE_KEY_ALGS`, so a proof cannot
282
+ * choose a weaker algorithm than the device enrolled with.
283
+ */
284
+ allowedAlgs?: readonly DeviceKeyAlg[];
204
285
  /** Override the wall clock — testing hook. Defaults to `Date.now()`. */
205
286
  now?: number;
206
287
  }
@@ -217,7 +298,25 @@ export declare function verifyProof(jws: string, opts?: VerifyOptions): VerifySu
217
298
  /**
218
299
  * Verify a DPoP-shaped Ed25519 proof whose header `typ` must equal `typ`.
219
300
  *
301
+ * Always EdDSA, whatever `opts.allowedAlgs` says: the RFC 9449 access-token
302
+ * proof (`dpop.ts`) is signed by the CLI's own in-memory Ed25519 key, which is a
303
+ * different key from the device key and deliberately stays software.
304
+ *
220
305
  * @internal See `signEd25519Proof`. `verifyProof` (device identity) and
221
306
  * `verifyDpopProof` (`dpop.ts`, RFC 9449) are the two public doors.
222
307
  */
223
308
  export declare function verifyEd25519Proof(jws: string, opts: VerifyOptions, typ: string): VerifySuccess | VerifyFailure;
309
+ /**
310
+ * Verify a DPoP-shaped proof signed by an Ed25519 or P-256 key, whose header
311
+ * `typ` must equal `typ` and whose `alg` must be in `opts.allowedAlgs`.
312
+ *
313
+ * The algorithm comes from the header and is checked twice before any key is
314
+ * built: it must be one the caller admitted, and it must be the algorithm the
315
+ * header's own key type signs with. Without the second check a header could
316
+ * pair `alg: ES256` with an Ed25519 key (or the reverse) and steer the
317
+ * verifier into a code path nobody tested; with it, the only thing an attacker
318
+ * controls about the algorithm is a choice among the caller's own allow-list.
319
+ *
320
+ * @internal
321
+ */
322
+ export declare function verifyDevicePublicKeyProof(jws: string, opts: VerifyOptions, typ: string): VerifySuccess | VerifyFailure;
@@ -28,19 +28,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
  var deviceKey_exports = {};
30
30
  __export(deviceKey_exports, {
31
+ DEVICE_KEY_ALGS: () => DEVICE_KEY_ALGS,
31
32
  DEVICE_PROOF_TYP: () => DEVICE_PROOF_TYP,
33
+ algForJwk: () => algForJwk,
32
34
  buildProof: () => buildProof,
33
35
  generateDeviceKeyPair: () => generateDeviceKeyPair,
36
+ isDevicePublicJwk: () => isDevicePublicJwk,
34
37
  jwkThumbprint: () => jwkThumbprint,
35
38
  normalizeHtu: () => normalizeHtu,
39
+ p256PublicJwkFromRaw: () => p256PublicJwkFromRaw,
40
+ proofSigningInput: () => proofSigningInput,
36
41
  publicJwkFromX: () => publicJwkFromX,
37
42
  signEd25519Proof: () => signEd25519Proof,
43
+ signProofWith: () => signProofWith,
38
44
  tokenHash: () => tokenHash,
45
+ verifyDevicePublicKeyProof: () => verifyDevicePublicKeyProof,
39
46
  verifyEd25519Proof: () => verifyEd25519Proof,
40
47
  verifyProof: () => verifyProof
41
48
  });
42
49
  module.exports = __toCommonJS(deviceKey_exports);
43
50
  var import_node_crypto = __toESM(require("node:crypto"), 1);
51
+ const DEVICE_KEY_ALGS = ["ES256", "EdDSA"];
44
52
  function b64u(buf) {
45
53
  return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
46
54
  }
@@ -64,9 +72,32 @@ function generateDeviceKeyPair() {
64
72
  };
65
73
  }
66
74
  function jwkThumbprint(publicJwk) {
67
- const canonical = `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}"}`;
75
+ const canonical = publicJwk.kty === "EC" ? `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}","y":"${publicJwk.y}"}` : `{"crv":"${publicJwk.crv}","kty":"${publicJwk.kty}","x":"${publicJwk.x}"}`;
68
76
  return b64u(import_node_crypto.default.createHash("sha256").update(canonical, "utf-8").digest());
69
77
  }
78
+ const B64URL_32 = /^[A-Za-z0-9_-]{43}$/;
79
+ function isDevicePublicJwk(value) {
80
+ if (!value || typeof value !== "object")
81
+ return false;
82
+ const jwk = value;
83
+ if (jwk.kty === "OKP") {
84
+ return jwk.crv === "Ed25519" && typeof jwk.x === "string" && B64URL_32.test(jwk.x);
85
+ }
86
+ if (jwk.kty === "EC") {
87
+ return jwk.crv === "P-256" && typeof jwk.x === "string" && typeof jwk.y === "string" && B64URL_32.test(jwk.x) && B64URL_32.test(jwk.y);
88
+ }
89
+ return false;
90
+ }
91
+ function algForJwk(jwk) {
92
+ return jwk.kty === "EC" ? "ES256" : "EdDSA";
93
+ }
94
+ function p256PublicJwkFromRaw(raw) {
95
+ if (raw.length !== 64) {
96
+ throw new Error(`p256PublicJwkFromRaw: expected 64 bytes (x||y), got ${raw.length}`);
97
+ }
98
+ const buf = Buffer.from(raw);
99
+ return { kty: "EC", crv: "P-256", x: b64u(buf.subarray(0, 32)), y: b64u(buf.subarray(32)) };
100
+ }
70
101
  function publicJwkFromX(x) {
71
102
  const raw = b64uDecode(x);
72
103
  if (raw.length !== 32) {
@@ -100,12 +131,20 @@ function buildProof(privateJwk, publicJwk, opts) {
100
131
  }
101
132
  const DEVICE_PROOF_TYP = "oversky-dpop+jwt";
102
133
  function signEd25519Proof(privateKey, publicJwk, typ, opts) {
134
+ const signingInput = proofSigningInput("EdDSA", publicJwk, typ, opts);
135
+ const signature = import_node_crypto.default.sign(null, Buffer.from(signingInput, "utf-8"), privateKey);
136
+ return `${signingInput}.${b64u(signature)}`;
137
+ }
138
+ function proofSigningInput(alg, publicJwk, typ, opts) {
103
139
  if (typeof opts.method !== "string" || opts.method.length === 0) {
104
140
  throw new Error("buildProof: method must be a non-empty string");
105
141
  }
106
142
  if (typeof opts.url !== "string" || opts.url.length === 0) {
107
143
  throw new Error("buildProof: url must be a non-empty string");
108
144
  }
145
+ if (algForJwk(publicJwk) !== alg) {
146
+ throw new Error(`buildProof: alg ${alg} does not match a ${publicJwk.kty} key`);
147
+ }
109
148
  const claims = {
110
149
  htm: opts.method.toUpperCase(),
111
150
  htu: normalizeHtu(opts.url),
@@ -116,17 +155,23 @@ function signEd25519Proof(privateKey, publicJwk, typ, opts) {
116
155
  claims.rth = tokenHash(opts.refreshToken);
117
156
  if (opts.accessToken !== void 0)
118
157
  claims.ath = tokenHash(opts.accessToken);
119
- const header = { alg: "EdDSA", typ, jwk: publicJwk };
158
+ const header = { alg, typ, jwk: publicJwk };
120
159
  const headerB64 = b64u(Buffer.from(JSON.stringify(header), "utf-8"));
121
160
  const payloadB64 = b64u(Buffer.from(JSON.stringify(claims), "utf-8"));
122
- const signingInput = `${headerB64}.${payloadB64}`;
123
- const signature = import_node_crypto.default.sign(null, Buffer.from(signingInput, "utf-8"), privateKey);
124
- return `${signingInput}.${b64u(signature)}`;
161
+ return `${headerB64}.${payloadB64}`;
162
+ }
163
+ async function signProofWith(signer, opts, typ = DEVICE_PROOF_TYP) {
164
+ const signingInput = proofSigningInput(signer.alg, signer.publicJwk, typ, opts);
165
+ const signature = await signer.sign(Buffer.from(signingInput, "utf-8"));
166
+ return `${signingInput}.${b64u(Buffer.from(signature))}`;
125
167
  }
126
168
  function verifyProof(jws, opts = {}) {
127
- return verifyEd25519Proof(jws, opts, DEVICE_PROOF_TYP);
169
+ return verifyDevicePublicKeyProof(jws, opts, DEVICE_PROOF_TYP);
128
170
  }
129
171
  function verifyEd25519Proof(jws, opts, typ) {
172
+ return verifyDevicePublicKeyProof(jws, { ...opts, allowedAlgs: ["EdDSA"] }, typ);
173
+ }
174
+ function verifyDevicePublicKeyProof(jws, opts, typ) {
130
175
  if (typeof jws !== "string" || jws.length === 0) {
131
176
  return { valid: false, reason: "malformed" };
132
177
  }
@@ -143,27 +188,24 @@ function verifyEd25519Proof(jws, opts, typ) {
143
188
  } catch {
144
189
  return { valid: false, reason: "malformed" };
145
190
  }
146
- if (!header || typeof header !== "object" || header.alg !== "EdDSA") {
191
+ const allowedAlgs = opts.allowedAlgs ?? ["EdDSA"];
192
+ const headerAlg = header && typeof header === "object" ? header.alg : void 0;
193
+ if (headerAlg !== "EdDSA" && headerAlg !== "ES256" || !allowedAlgs.includes(headerAlg)) {
147
194
  return { valid: false, reason: "bad_alg" };
148
195
  }
149
196
  if (header.typ !== typ) {
150
197
  return { valid: false, reason: "bad_typ" };
151
198
  }
152
199
  const headerJwk = header.jwk;
153
- if (!headerJwk || typeof headerJwk !== "object" || headerJwk.kty !== "OKP" || headerJwk.crv !== "Ed25519" || typeof headerJwk.x !== "string") {
200
+ if (!isDevicePublicJwk(headerJwk)) {
154
201
  return { valid: false, reason: "bad_jwk" };
155
202
  }
156
- const publicJwk = {
157
- kty: "OKP",
158
- crv: "Ed25519",
159
- x: headerJwk.x
160
- };
203
+ const publicJwk = headerJwk.kty === "EC" ? { kty: "EC", crv: "P-256", x: headerJwk.x, y: headerJwk.y } : { kty: "OKP", crv: "Ed25519", x: headerJwk.x };
204
+ if (algForJwk(publicJwk) !== headerAlg) {
205
+ return { valid: false, reason: "bad_alg" };
206
+ }
161
207
  let publicKeyObj;
162
208
  try {
163
- const raw = b64uDecode(publicJwk.x);
164
- if (raw.length !== 32) {
165
- return { valid: false, reason: "bad_jwk" };
166
- }
167
209
  publicKeyObj = import_node_crypto.default.createPublicKey({
168
210
  format: "jwk",
169
211
  key: publicJwk
@@ -171,7 +213,7 @@ function verifyEd25519Proof(jws, opts, typ) {
171
213
  } catch {
172
214
  return { valid: false, reason: "bad_jwk" };
173
215
  }
174
- if (opts.pinnedPublicJwk && opts.pinnedPublicJwk.x !== publicJwk.x) {
216
+ if (opts.pinnedPublicJwk && jwkThumbprint(opts.pinnedPublicJwk) !== jwkThumbprint(publicJwk)) {
175
217
  return { valid: false, reason: "pin_mismatch" };
176
218
  }
177
219
  const signingInput = Buffer.from(`${headerB64}.${payloadB64}`, "utf-8");
@@ -183,7 +225,15 @@ function verifyEd25519Proof(jws, opts, typ) {
183
225
  }
184
226
  let sigOk;
185
227
  try {
186
- sigOk = import_node_crypto.default.verify(null, signingInput, publicKeyObj, signature);
228
+ sigOk = headerAlg === "ES256" ? (
229
+ // JWS ES256 is the fixed-width r||s form, never DER (RFC 7518 §3.4).
230
+ // Node's 'ieee-p1363' encoding is that form and already refuses any
231
+ // other width (removing the explicit length check changes no test
232
+ // result), so the check below states the requirement instead of
233
+ // inheriting it: a DER or truncated signature must fail here rather
234
+ // than be re-encoded into validity, even under a Node that changes.
235
+ signature.length === 64 && import_node_crypto.default.verify("sha256", signingInput, { key: publicKeyObj, dsaEncoding: "ieee-p1363" }, signature)
236
+ ) : import_node_crypto.default.verify(null, signingInput, publicKeyObj, signature);
187
237
  } catch {
188
238
  sigOk = false;
189
239
  }
@@ -221,19 +271,27 @@ function verifyEd25519Proof(jws, opts, typ) {
221
271
  valid: true,
222
272
  claims,
223
273
  publicJwk,
274
+ alg: headerAlg,
224
275
  thumbprint: jwkThumbprint(publicJwk)
225
276
  };
226
277
  }
227
278
  // Annotate the CommonJS export names for ESM import in node:
228
279
  0 && (module.exports = {
280
+ DEVICE_KEY_ALGS,
229
281
  DEVICE_PROOF_TYP,
282
+ algForJwk,
230
283
  buildProof,
231
284
  generateDeviceKeyPair,
285
+ isDevicePublicJwk,
232
286
  jwkThumbprint,
233
287
  normalizeHtu,
288
+ p256PublicJwkFromRaw,
289
+ proofSigningInput,
234
290
  publicJwkFromX,
235
291
  signEd25519Proof,
292
+ signProofWith,
236
293
  tokenHash,
294
+ verifyDevicePublicKeyProof,
237
295
  verifyEd25519Proof,
238
296
  verifyProof
239
297
  });
@@ -0,0 +1,33 @@
1
+ import { type DeviceKeyPair, type DeviceKeySigner } from './deviceKey.js';
2
+ /**
3
+ * Where the private half lives.
4
+ *
5
+ * - `software`: an exportable scalar, sealed in a file under the credential
6
+ * envelope. Anything that can read that file and the envelope key owns the
7
+ * device identity.
8
+ * - `hardware`: a non-exportable key inside a Secure Enclave / TPM. A copy of
9
+ * every file on the disk is still not the key.
10
+ */
11
+ export type DeviceKeyCustody = 'software' | 'hardware';
12
+ export interface DeviceKeyHandle extends DeviceKeySigner {
13
+ readonly custody: DeviceKeyCustody;
14
+ /** RFC 7638 thumbprint of `publicJwk`; the id the server pins. */
15
+ readonly thumbprint: string;
16
+ }
17
+ /**
18
+ * A handle over a software Ed25519 key. Throws if the pair is internally
19
+ * inconsistent (public and private halves from different keys, or a private
20
+ * JWK Node cannot import) so a corrupted identity fails at load, where the
21
+ * caller already handles it, rather than at the first signature.
22
+ */
23
+ export declare function softwareEd25519Handle(pair: DeviceKeyPair): DeviceKeyHandle;
24
+ /**
25
+ * A software P-256 handle with a fresh key. NOT a device-key backend: a
26
+ * software P-256 key gives up exactly what P-256 is for, and nothing in the
27
+ * product creates one. It exists so the ES256 wire format (sign, verify, pin)
28
+ * can be exercised end to end without a Secure Enclave, and so a fake hardware
29
+ * helper in a test signs with the real algorithm instead of a stub.
30
+ *
31
+ * @internal
32
+ */
33
+ export declare function generateSoftwareP256Handle(): DeviceKeyHandle;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var deviceKeyHandle_exports = {};
30
+ __export(deviceKeyHandle_exports, {
31
+ generateSoftwareP256Handle: () => generateSoftwareP256Handle,
32
+ softwareEd25519Handle: () => softwareEd25519Handle
33
+ });
34
+ module.exports = __toCommonJS(deviceKeyHandle_exports);
35
+ var import_node_crypto = __toESM(require("node:crypto"), 1);
36
+ var import_deviceKey = require("./deviceKey.js");
37
+ function softwareEd25519Handle(pair) {
38
+ if (pair.publicJwk.x !== pair.privateJwk.x) {
39
+ throw new Error("softwareEd25519Handle: public/private JWK x mismatch (corrupted keypair)");
40
+ }
41
+ const privateKey = import_node_crypto.default.createPrivateKey({
42
+ format: "jwk",
43
+ key: pair.privateJwk
44
+ });
45
+ const publicJwk = pair.publicJwk;
46
+ return {
47
+ alg: "EdDSA",
48
+ custody: "software",
49
+ publicJwk,
50
+ thumbprint: (0, import_deviceKey.jwkThumbprint)(publicJwk),
51
+ // Ed25519 hashes the message itself, so Node takes `null` for the digest.
52
+ sign: async (signingInput) => new Uint8Array(import_node_crypto.default.sign(null, Buffer.from(signingInput), privateKey))
53
+ };
54
+ }
55
+ function generateSoftwareP256Handle() {
56
+ const { privateKey, publicKey } = import_node_crypto.default.generateKeyPairSync("ec", { namedCurve: "P-256" });
57
+ const jwk = publicKey.export({ format: "jwk" });
58
+ const publicJwk = { kty: "EC", crv: "P-256", x: jwk.x, y: jwk.y };
59
+ return {
60
+ alg: "ES256",
61
+ custody: "software",
62
+ publicJwk,
63
+ thumbprint: (0, import_deviceKey.jwkThumbprint)(publicJwk),
64
+ sign: async (signingInput) => new Uint8Array(
65
+ // 'ieee-p1363' is the JWS (r||s) form; the default would be DER.
66
+ import_node_crypto.default.sign("sha256", Buffer.from(signingInput), {
67
+ key: privateKey,
68
+ dsaEncoding: "ieee-p1363"
69
+ })
70
+ )
71
+ };
72
+ }
73
+ // Annotate the CommonJS export names for ESM import in node:
74
+ 0 && (module.exports = {
75
+ generateSoftwareP256Handle,
76
+ softwareEd25519Handle
77
+ });
@@ -0,0 +1,68 @@
1
+ import { type P256PublicJwk } from './deviceKey.js';
2
+ import type { DeviceKeyHandle } from './deviceKeyHandle.js';
3
+ /** File name of the helper next to the daemon binary and under `<machine root>/bin`. */
4
+ export declare const HARDWARE_KEY_HELPER_NAME = "skrr-native-device-key";
5
+ export declare const HARDWARE_KEY_PROTOCOL = 1;
6
+ export type HardwareDeviceKeyErrorCode = 'helper_missing' | 'helper_failed' | 'timeout' | 'bad_response' | 'key_mismatch' | 'unsupported';
7
+ export declare class HardwareDeviceKeyError extends Error {
8
+ readonly code: HardwareDeviceKeyErrorCode;
9
+ constructor(code: HardwareDeviceKeyErrorCode, message: string);
10
+ }
11
+ /**
12
+ * Tell auth-core where the helper is. The daemon calls this at boot with the
13
+ * path next to its own binary; a process that never does (the CLI, a test)
14
+ * falls back to the places the helper is installed: beside this process's
15
+ * executable, then `<machine root>/bin`. Pass `null` to forget a resolver.
16
+ */
17
+ export declare function configureHardwareDeviceKeyHelper(resolver: (() => string | null) | null): void;
18
+ /** Absolute path of an executable helper, or null when this machine has none. */
19
+ export declare function resolveHardwareDeviceKeyHelper(): string | null;
20
+ /** @internal Test seam — bound every helper call by `ms`. `null` restores the real limits. */
21
+ export declare function __setHardwareHelperTimeoutForTest(ms: number | null): void;
22
+ export type HardwareKeyCapability = {
23
+ available: true;
24
+ } | {
25
+ available: false;
26
+ reason: 'helper_missing' | 'no_secure_hardware' | 'probe_failed';
27
+ message?: string;
28
+ };
29
+ /**
30
+ * Can this machine create and use a hardware-bound device key right now? Looked
31
+ * up, never assumed: the answer needs both a helper that runs and hardware that
32
+ * answers. A positive result is kept for the life of the process; a negative one
33
+ * is retried after a minute, so a helper installed by an update is picked up
34
+ * without a restart and a broken one is not spawned on every proof.
35
+ */
36
+ export declare function probeHardwareDeviceKey(): Promise<HardwareKeyCapability>;
37
+ /** @internal Test seam — forget the cached capability. */
38
+ export declare function __resetHardwareProbeForTest(): void;
39
+ /** Contents of `device-key.se`. The blob is opaque and machine-bound; see the file header. */
40
+ export interface HardwareKeyRecord {
41
+ readonly blob: string;
42
+ readonly publicJwk: P256PublicJwk;
43
+ }
44
+ export declare function serializeHardwareKeyRecord(record: HardwareKeyRecord, createdAt?: Date): string;
45
+ /**
46
+ * Parse `device-key.se`. Null for anything that is not a well-formed v1 record
47
+ * whose stored thumbprint matches its own public key: a record is trusted for
48
+ * exactly what it proves about itself, so a half-written or hand-edited file
49
+ * reads as unreadable (which never replaces it) and not as a different key.
50
+ */
51
+ export declare function parseHardwareKeyRecord(text: string): HardwareKeyRecord | null;
52
+ /** A handle whose `sign` asks the helper, so no scalar is ever in this process. */
53
+ export declare function hardwareDeviceKeyHandle(record: HardwareKeyRecord): DeviceKeyHandle;
54
+ /**
55
+ * Create a hardware key and PROVE it before anyone relies on it: sign random
56
+ * bytes and verify them with Node's own crypto against the public key the helper
57
+ * reported. A helper that returns a mis-encoded signature, or a public key that
58
+ * is not the signer's, fails here with nothing yet persisted and nothing yet sent
59
+ * to the server, instead of as a rejected login that leaves a pin half-moved.
60
+ *
61
+ * The returned record is NOT persisted; the caller decides when it becomes the
62
+ * device's identity (see `commitDeviceKeyCandidate`). Dropping it destroys the
63
+ * key: the Secure Enclave keeps nothing the blob does not.
64
+ */
65
+ export declare function createHardwareDeviceKey(): Promise<{
66
+ record: HardwareKeyRecord;
67
+ handle: DeviceKeyHandle;
68
+ }>;