@serviceme/devtools-core 0.1.8 → 0.2.0

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 (47) hide show
  1. package/dist/auth.d.mts +590 -0
  2. package/dist/auth.d.ts +590 -0
  3. package/dist/auth.js +842 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/auth.mjs +804 -0
  6. package/dist/auth.mjs.map +1 -0
  7. package/dist/device.d.mts +456 -0
  8. package/dist/device.d.ts +456 -0
  9. package/dist/device.js +696 -0
  10. package/dist/device.js.map +1 -0
  11. package/dist/device.mjs +647 -0
  12. package/dist/device.mjs.map +1 -0
  13. package/dist/index-CrNC-aao.d.ts +277 -0
  14. package/dist/index-Dmyy4urr.d.mts +277 -0
  15. package/dist/index.d.mts +1372 -27
  16. package/dist/index.d.ts +1372 -27
  17. package/dist/index.js +5125 -888
  18. package/dist/index.js.map +1 -0
  19. package/dist/index.mjs +5022 -906
  20. package/dist/index.mjs.map +1 -0
  21. package/dist/skill-linker.d.mts +188 -0
  22. package/dist/skill-linker.d.ts +188 -0
  23. package/dist/skill-linker.js +374 -0
  24. package/dist/skill-linker.js.map +1 -0
  25. package/dist/skill-linker.mjs +330 -0
  26. package/dist/skill-linker.mjs.map +1 -0
  27. package/dist/skill-store.d.mts +35 -0
  28. package/dist/skill-store.d.ts +35 -0
  29. package/dist/skill-store.js +291 -0
  30. package/dist/skill-store.js.map +1 -0
  31. package/dist/skill-store.mjs +254 -0
  32. package/dist/skill-store.mjs.map +1 -0
  33. package/dist/submit.d.mts +3 -0
  34. package/dist/submit.d.ts +3 -0
  35. package/dist/submit.js +166 -0
  36. package/dist/submit.js.map +1 -0
  37. package/dist/submit.mjs +130 -0
  38. package/dist/submit.mjs.map +1 -0
  39. package/dist/toolbox.d.mts +240 -0
  40. package/dist/toolbox.d.ts +240 -0
  41. package/dist/toolbox.js +530 -0
  42. package/dist/toolbox.js.map +1 -0
  43. package/dist/toolbox.mjs +482 -0
  44. package/dist/toolbox.mjs.map +1 -0
  45. package/dist/types-B9gk3dXH.d.mts +62 -0
  46. package/dist/types-B9gk3dXH.d.ts +62 -0
  47. package/package.json +50 -5
@@ -0,0 +1,647 @@
1
+ // src/device/deviceAuth.ts
2
+ import { createHash } from "crypto";
3
+ var DeviceAuthHeaders = {
4
+ deviceId: "x-ms-device-id",
5
+ deviceSecret: "x-ms-device-secret",
6
+ signature: "x-ms-device-signature",
7
+ timestamp: "x-ms-device-timestamp",
8
+ secretVersion: "x-ms-device-secret-version"
9
+ };
10
+ function createDeviceRequestSignature(params) {
11
+ const basis = [
12
+ params.method.toUpperCase(),
13
+ params.path,
14
+ String(params.timestamp),
15
+ params.body,
16
+ params.secret
17
+ ].join("\n");
18
+ return createHash("sha256").update(basis).digest("hex");
19
+ }
20
+ function buildSignedHeaders(params) {
21
+ const timestamp = params.timestamp ?? Date.now();
22
+ const signature = createDeviceRequestSignature({
23
+ method: params.method,
24
+ path: params.path,
25
+ timestamp,
26
+ body: params.body,
27
+ secret: params.deviceSecret
28
+ });
29
+ return {
30
+ [DeviceAuthHeaders.deviceId]: params.publicId,
31
+ [DeviceAuthHeaders.deviceSecret]: params.deviceSecret,
32
+ [DeviceAuthHeaders.signature]: signature,
33
+ [DeviceAuthHeaders.timestamp]: String(timestamp),
34
+ [DeviceAuthHeaders.secretVersion]: String(params.secretVersion)
35
+ };
36
+ }
37
+
38
+ // src/device/Enroller.ts
39
+ import { randomBytes } from "crypto";
40
+
41
+ // src/device/InstallationId.ts
42
+ import { createHash as createHash2, randomUUID } from "crypto";
43
+ import * as os from "os";
44
+ function fingerprintMaterial() {
45
+ let username = "unknown";
46
+ try {
47
+ username = os.userInfo().username;
48
+ } catch {
49
+ username = process.env.USER ?? process.env.USERNAME ?? "unknown";
50
+ }
51
+ return [
52
+ os.hostname(),
53
+ username,
54
+ os.platform(),
55
+ os.arch(),
56
+ process.versions.node ?? "unknown"
57
+ ].join("|");
58
+ }
59
+ function deriveInstallationId() {
60
+ const material = fingerprintMaterial();
61
+ const digest = createHash2("sha256").update(material).digest("hex");
62
+ return formatAsV4(digest.slice(0, 32));
63
+ }
64
+ function randomInstallationId() {
65
+ return randomUUID();
66
+ }
67
+ function fingerprintSource() {
68
+ return fingerprintMaterial();
69
+ }
70
+ function formatAsV4(hex32) {
71
+ const chars = hex32.split("");
72
+ const versionIdx = 12;
73
+ const variantIdx = 16;
74
+ const versionChar = parseInt(chars[versionIdx] ?? "8", 16) & 0 | 4;
75
+ chars[versionIdx] = versionChar.toString(16);
76
+ const variantChar = parseInt(chars[variantIdx] ?? "8", 16) & 3 | 8;
77
+ chars[variantIdx] = variantChar.toString(16);
78
+ const formatted = chars.join("");
79
+ return `${formatted.slice(0, 8)}-${formatted.slice(8, 12)}-${formatted.slice(12, 16)}-${formatted.slice(16, 20)}-${formatted.slice(20, 32)}`;
80
+ }
81
+
82
+ // src/device/Enroller.ts
83
+ var SECRET_BYTES = 32;
84
+ var PUBLIC_ID_BYTES = 16;
85
+ var defaultRandomBytes = (size) => {
86
+ return randomBytes(size);
87
+ };
88
+ var DeviceReenrollRequiresAuthError = class extends Error {
89
+ constructor(message = "Re-enroll on a claimed device requires current device credentials or the bound user") {
90
+ super(message);
91
+ this.name = "DeviceReenrollRequiresAuthError";
92
+ }
93
+ };
94
+ var DeviceSecretVersionMismatchError = class extends Error {
95
+ constructor(message = "Device secret version mismatch \u2014 server has rotated past the local copy") {
96
+ super(message);
97
+ this.name = "DeviceSecretVersionMismatchError";
98
+ }
99
+ };
100
+ var Enroller = class {
101
+ constructor(opts) {
102
+ this.inflight = null;
103
+ this.identity = opts.identityStore;
104
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
105
+ this.random = opts.randomBytes ?? defaultRandomBytes;
106
+ this.enrollRequest = opts.enrollRequest;
107
+ }
108
+ /**
109
+ * Read the current binding state without touching the disk.
110
+ * Returns `anonymous` when no identity is stored.
111
+ */
112
+ async currentState() {
113
+ const stored = await this.identity.read();
114
+ return stored?.bindingState ?? "anonymous";
115
+ }
116
+ /**
117
+ * Drive the enrollment flow.
118
+ *
119
+ * @param force when true, drop the local identity and start fresh
120
+ * (server treats this as a brand-new install).
121
+ * @param requireAuth when true, refuse to silently re-enroll an
122
+ * existing claimed device — throw
123
+ * `DeviceReenrollRequiresAuthError` instead.
124
+ */
125
+ /**
126
+ * Resolve when any in-flight enrollment completes. Returns immediately
127
+ * when no enrollment is in progress. Allows callers (e.g. the extension's
128
+ * `buildDeviceAuthHeaders`) to wait for a concurrent `syncDeviceInfo()`
129
+ * enrollment before attempting to read the identity from the store.
130
+ */
131
+ async waitForEnrollment() {
132
+ if (this.inflight) {
133
+ await this.inflight;
134
+ }
135
+ }
136
+ async enroll(opts = {}) {
137
+ if (this.inflight) {
138
+ return this.inflight;
139
+ }
140
+ const promise = this.runEnroll(opts);
141
+ this.inflight = promise;
142
+ try {
143
+ return await promise;
144
+ } finally {
145
+ if (this.inflight === promise) this.inflight = null;
146
+ }
147
+ }
148
+ /** Test seam — surface the underlying identity store. */
149
+ getIdentityStore() {
150
+ return this.identity;
151
+ }
152
+ /** True when an enrollment is currently in-flight. Used by callers (e.g. the extension's `buildDeviceAuthHeaders`) to skip triggering a competing enrollment. */
153
+ isEnrolling() {
154
+ return this.inflight !== null;
155
+ }
156
+ async runEnroll(opts) {
157
+ const { written } = await this.identity.mutate(async (current) => {
158
+ const existing = opts.force ? null : current;
159
+ if (!opts.force && current) {
160
+ if (current.bindingState === "expired") {
161
+ } else if (opts.requireAuth && (current.bindingState === "claimed" || current.bindingState === "pending")) {
162
+ throw new DeviceReenrollRequiresAuthError();
163
+ }
164
+ }
165
+ const installationId = current?.installationId ?? deriveInstallationId();
166
+ const machineId = current?.machineId ?? "unknown";
167
+ const platform3 = current?.platform ?? "unknown";
168
+ let response;
169
+ if (this.enrollRequest) {
170
+ response = await this.enrollRequest({
171
+ installationId,
172
+ machineId,
173
+ platform: platform3,
174
+ existing,
175
+ force: Boolean(opts.force),
176
+ requireAuth: Boolean(opts.requireAuth)
177
+ });
178
+ } else {
179
+ response = synthesizeEnrollResponse(this.random, existing);
180
+ }
181
+ const next = {
182
+ version: current?.version ?? 1,
183
+ installationId,
184
+ machineId,
185
+ platform: platform3,
186
+ hostname: current?.hostname,
187
+ publicId: response.publicId,
188
+ secretVersion: response.secretVersion,
189
+ bindingState: response.bindingState,
190
+ deviceSecret: response.deviceSecret,
191
+ previousDeviceSecret: existing?.deviceSecret,
192
+ previousSecretExpiresAt: opts.force || response.secretVersion === (existing?.secretVersion ?? 0) + 1 ? void 0 : existing?.previousSecretExpiresAt,
193
+ lastEnrollAt: this.now().toISOString(),
194
+ lastSyncAt: existing?.lastSyncAt,
195
+ lastSyncError: void 0
196
+ };
197
+ return { next };
198
+ });
199
+ return {
200
+ publicId: written.publicId,
201
+ bindingState: written.bindingState,
202
+ expiresAt: deriveExpiresAt(written, this.now)
203
+ };
204
+ }
205
+ /**
206
+ * Rotate the HMAC secret. Keeps the previous secret for the grace
207
+ * window (default 7 days per `2.需求澄清.md` §1.2) — the
208
+ * `previousSecretExpiresAt` is stamped on the persisted identity.
209
+ */
210
+ async rotateSecret(opts = {}) {
211
+ const gracePeriodDays = opts.gracePeriodDays ?? 7;
212
+ const now = this.now();
213
+ const newSecret = this.random(SECRET_BYTES).toString("hex");
214
+ const { written } = await this.identity.mutate(async (current) => {
215
+ if (!current) {
216
+ throw new Error("Cannot rotate-secret without a prior enrollment");
217
+ }
218
+ const graceExpiresAt = new Date(now.getTime() + gracePeriodDays * 24 * 60 * 60 * 1e3);
219
+ const next = {
220
+ ...current,
221
+ deviceSecret: newSecret,
222
+ previousDeviceSecret: current.deviceSecret,
223
+ previousSecretExpiresAt: graceExpiresAt.toISOString(),
224
+ secretVersion: current.secretVersion + 1,
225
+ lastEnrollAt: now.toISOString()
226
+ };
227
+ return { next };
228
+ });
229
+ return {
230
+ publicId: written.publicId,
231
+ secretVersion: written.secretVersion,
232
+ gracePeriodDays
233
+ };
234
+ }
235
+ /**
236
+ * Mark the device as `expired`. Used when the server returns a
237
+ * device-expiry response; the next `enroll()` call forces a fresh
238
+ * round-trip.
239
+ */
240
+ async markExpired() {
241
+ await this.identity.mutate(async (current) => {
242
+ if (!current) {
243
+ return { next: current ?? await emptyIdentity(this.random), result: void 0 };
244
+ }
245
+ const next = { ...current, bindingState: "expired" };
246
+ return { next };
247
+ });
248
+ }
249
+ /**
250
+ * Mark the device as `claimed`. Called by the bridge after a
251
+ * successful `device.claim` server response.
252
+ */
253
+ async markClaimed() {
254
+ await this.identity.mutate(async (current) => {
255
+ if (!current) {
256
+ throw new Error("Cannot mark-claimed without a prior enrollment");
257
+ }
258
+ const next = {
259
+ ...current,
260
+ bindingState: "claimed",
261
+ lastSyncAt: this.now().toISOString(),
262
+ lastSyncError: void 0
263
+ };
264
+ return { next };
265
+ });
266
+ }
267
+ };
268
+ function deriveExpiresAt(_identity, _now) {
269
+ return void 0;
270
+ }
271
+ function synthesizeEnrollResponse(random, existing) {
272
+ const publicId = existing?.publicId ?? random(PUBLIC_ID_BYTES).toString("hex");
273
+ const secretVersion = (existing?.secretVersion ?? 0) + 1;
274
+ return {
275
+ publicId,
276
+ deviceSecret: random(SECRET_BYTES).toString("hex"),
277
+ secretVersion,
278
+ bindingState: existing?.bindingState === "claimed" ? "claimed" : "anonymous"
279
+ };
280
+ }
281
+ async function emptyIdentity(random) {
282
+ return {
283
+ version: 1,
284
+ installationId: deriveInstallationId(),
285
+ machineId: "unknown",
286
+ platform: "unknown",
287
+ publicId: random(PUBLIC_ID_BYTES).toString("hex"),
288
+ secretVersion: 1,
289
+ bindingState: "anonymous",
290
+ deviceSecret: random(SECRET_BYTES).toString("hex"),
291
+ lastEnrollAt: (/* @__PURE__ */ new Date()).toISOString()
292
+ };
293
+ }
294
+
295
+ // src/device/IdentityStore.ts
296
+ import * as fsp from "fs/promises";
297
+ import * as os3 from "os";
298
+ import * as path2 from "path";
299
+ import { setTimeout as delay } from "timers/promises";
300
+
301
+ // src/paths/userHome.ts
302
+ import * as os2 from "os";
303
+ import * as path from "path";
304
+ var SERVICEME_DIR_NAME = ".serviceme";
305
+ var SERVICEME_HOME_ENV = "SERVICEME_HOME";
306
+ var activeOverrides = {};
307
+ function resolveHomeDir() {
308
+ const injected = activeOverrides.homeDir;
309
+ if (injected !== void 0) {
310
+ return injected;
311
+ }
312
+ return os2.homedir();
313
+ }
314
+ function resolveServicemeHomeEnv() {
315
+ const injected = activeOverrides.servicemeHomeEnv;
316
+ if (injected !== void 0) {
317
+ return injected.length > 0 ? injected : void 0;
318
+ }
319
+ const envValue = process.env[SERVICEME_HOME_ENV];
320
+ return envValue && envValue.length > 0 ? envValue : void 0;
321
+ }
322
+ function getServicemeHome() {
323
+ const override = resolveServicemeHomeEnv();
324
+ if (override !== void 0) {
325
+ return path.resolve(override);
326
+ }
327
+ return path.join(resolveHomeDir(), SERVICEME_DIR_NAME);
328
+ }
329
+ var DEVICE_JSON_FILENAME = "device.json";
330
+ function getDeviceJsonPath() {
331
+ return path.join(getServicemeHome(), DEVICE_JSON_FILENAME);
332
+ }
333
+
334
+ // src/device/types.ts
335
+ var DEVICE_JSON_SCHEMA_VERSION = 1;
336
+
337
+ // src/device/IdentityStore.ts
338
+ var FILE_MODE = 384;
339
+ var LOCK_DIR_MODE = 448;
340
+ var DEFAULT_LOCK_TIMEOUT_MS = 5e3;
341
+ var DEFAULT_LOCK_RETRY_MS = 25;
342
+ var TMP_SUFFIX = ".tmp";
343
+ var FsIdentityFileBackend = class {
344
+ async exists(filePath) {
345
+ try {
346
+ await fsp.access(filePath);
347
+ return true;
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+ async read(filePath) {
353
+ try {
354
+ const buf = await fsp.readFile(filePath, "utf8");
355
+ const parsed = JSON.parse(buf);
356
+ return migratePersistedIdentity(parsed);
357
+ } catch (err) {
358
+ if (isNodeError(err) && err.code === "ENOENT") return null;
359
+ throw err;
360
+ }
361
+ }
362
+ async write(filePath, payload) {
363
+ await fsp.mkdir(path2.dirname(filePath), { recursive: true });
364
+ const tmpPath = `${filePath}${TMP_SUFFIX}`;
365
+ const bytes = Buffer.from(JSON.stringify(payload, null, " "), "utf8");
366
+ await fsp.rm(tmpPath, { force: true });
367
+ const handle = await fsp.open(tmpPath, "w", FILE_MODE);
368
+ try {
369
+ await handle.writeFile(bytes);
370
+ await handle.sync();
371
+ } finally {
372
+ await handle.close();
373
+ }
374
+ await fsp.rename(tmpPath, filePath);
375
+ await fsp.chmod(filePath, FILE_MODE).catch(() => void 0);
376
+ return { bytesWritten: bytes.byteLength, tmpPath };
377
+ }
378
+ async delete(filePath) {
379
+ await fsp.rm(filePath, { force: true });
380
+ }
381
+ };
382
+ function migratePersistedIdentity(parsed) {
383
+ if (!isRecord(parsed)) {
384
+ throw new Error("device.json: top-level must be an object");
385
+ }
386
+ const version = parsed.version;
387
+ if (version === DEVICE_JSON_SCHEMA_VERSION) {
388
+ if (parsed.publicId === void 0 && parsed.deviceSecret === void 0 && ("claimed" in parsed || "publicKeyFingerprint" in parsed)) {
389
+ return null;
390
+ }
391
+ return parsed;
392
+ }
393
+ if (typeof version === "number" && version < DEVICE_JSON_SCHEMA_VERSION) {
394
+ return null;
395
+ }
396
+ throw new Error(`device.json: unsupported schema version ${String(version)}`);
397
+ }
398
+ function isRecord(value) {
399
+ return typeof value === "object" && value !== null;
400
+ }
401
+ function isNodeError(value) {
402
+ return value instanceof Error && typeof value.code === "string";
403
+ }
404
+ var FileLock = class {
405
+ constructor(filePath, timeoutMs, retryMs) {
406
+ this.acquired = false;
407
+ this.dirPath = `${filePath}.lock`;
408
+ this.timeoutMs = timeoutMs;
409
+ this.retryMs = retryMs;
410
+ }
411
+ async acquire() {
412
+ const start = Date.now();
413
+ while (true) {
414
+ try {
415
+ await fsp.mkdir(this.dirPath, { mode: LOCK_DIR_MODE });
416
+ this.acquired = true;
417
+ return;
418
+ } catch (err) {
419
+ if (!isNodeError(err) || err.code !== "EEXIST") {
420
+ throw err;
421
+ }
422
+ if (Date.now() - start >= this.timeoutMs) {
423
+ throw new Error(`IdentityStore lock acquisition timed out for ${this.dirPath}`);
424
+ }
425
+ await delay(this.retryMs);
426
+ }
427
+ }
428
+ }
429
+ async release() {
430
+ if (!this.acquired) return;
431
+ this.acquired = false;
432
+ await fsp.rm(this.dirPath, { recursive: true, force: true });
433
+ }
434
+ };
435
+ var IdentityStore = class {
436
+ constructor(opts = {}) {
437
+ this.filePath = opts.filePath ?? getDeviceJsonPath();
438
+ this.backend = opts.backend ?? new FsIdentityFileBackend();
439
+ this.hooks = opts.hooks ?? {};
440
+ this.lockTimeoutMs = opts.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
441
+ this.lockRetryMs = opts.lockRetryMs ?? DEFAULT_LOCK_RETRY_MS;
442
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
443
+ }
444
+ /** Absolute path to the underlying JSON file (test seam). */
445
+ getFilePath() {
446
+ return this.filePath;
447
+ }
448
+ /** True when the JSON file already exists on disk. */
449
+ async exists() {
450
+ return this.backend.exists(this.filePath);
451
+ }
452
+ /** Read the persisted identity; returns `null` when no identity is stored. */
453
+ async read() {
454
+ return this.backend.read(this.filePath);
455
+ }
456
+ /**
457
+ * Atomically write the given identity. Concurrent writers are
458
+ * serialized via the file lock; the read-modify-write happens
459
+ * inside the lock so callers can't see a partial state.
460
+ */
461
+ async write(next) {
462
+ await this.hooks.beforeWrite?.(next);
463
+ const lock = new FileLock(this.filePath, this.lockTimeoutMs, this.lockRetryMs);
464
+ await lock.acquire();
465
+ try {
466
+ const stamped = {
467
+ ...next,
468
+ version: DEVICE_JSON_SCHEMA_VERSION
469
+ };
470
+ const result = await this.backend.write(this.filePath, stamped);
471
+ await this.hooks.afterWrite?.(stamped);
472
+ return result;
473
+ } finally {
474
+ await lock.release();
475
+ }
476
+ }
477
+ /**
478
+ * Read-modify-write under the same lock. The mutator receives the
479
+ * current identity (or `null` on first call) and returns the
480
+ * replacement. Throwing inside the mutator aborts the write.
481
+ */
482
+ async mutate(mutator) {
483
+ const lock = new FileLock(this.filePath, this.lockTimeoutMs, this.lockRetryMs);
484
+ await lock.acquire();
485
+ try {
486
+ const current = await this.backend.read(this.filePath);
487
+ const { next, result } = await mutator(current);
488
+ const stamped = {
489
+ ...next,
490
+ version: DEVICE_JSON_SCHEMA_VERSION
491
+ };
492
+ await this.hooks.beforeWrite?.(stamped);
493
+ await this.backend.write(this.filePath, stamped);
494
+ await this.hooks.afterWrite?.(stamped);
495
+ return { result, written: stamped };
496
+ } finally {
497
+ await lock.release();
498
+ }
499
+ }
500
+ /** Wipe the persisted identity (used by `device.enroll --force`). */
501
+ async clear() {
502
+ await this.backend.delete(this.filePath);
503
+ }
504
+ /**
505
+ * Resolve the installation metadata for the current machine.
506
+ * Pure helper — no I/O, just `os.*` calls.
507
+ */
508
+ resolveInstallationMetadata() {
509
+ const machineId = os3.hostname();
510
+ const platform3 = os3.platform();
511
+ return {
512
+ installationId: "",
513
+ // intentionally empty; caller fills via deriveInstallationId()
514
+ machineId,
515
+ platform: platform3
516
+ };
517
+ }
518
+ /**
519
+ * Ensure the parent directory exists (`~/.serviceme/`). Idempotent.
520
+ * Useful when the bootstrap phase5 placeholder wasn't run yet.
521
+ */
522
+ async ensureHome() {
523
+ await fsp.mkdir(getServicemeHome(), { recursive: true });
524
+ await fsp.mkdir(path2.dirname(this.filePath), { recursive: true });
525
+ }
526
+ };
527
+
528
+ // src/device/DeviceCore.ts
529
+ var DeviceCore = class {
530
+ constructor(opts = {}) {
531
+ this.identity = opts.identityStore ?? new IdentityStore();
532
+ this.enroller = new Enroller({
533
+ identityStore: this.identity,
534
+ enrollRequest: opts.enrollRequest,
535
+ now: opts.now
536
+ });
537
+ this.resolveInstallationId = opts.resolveInstallationId ?? deriveInstallationId;
538
+ }
539
+ /** Read-only snapshot of the device status (matches `device.status` wire shape). */
540
+ async status() {
541
+ const stored = await this.identity.read();
542
+ return stored ? {
543
+ bindingState: stored.bindingState,
544
+ identity: projectIdentity(stored),
545
+ metadata: projectMetadata(stored),
546
+ lastSyncAt: stored.lastSyncAt,
547
+ lastSyncError: stored.lastSyncError
548
+ } : { bindingState: "anonymous" };
549
+ }
550
+ /** Enroll (or re-enroll) the device. */
551
+ async enroll(opts = {}) {
552
+ return this.enroller.enroll(opts);
553
+ }
554
+ /** Wait for any in-flight enrollment to finish. Use before `buildSignedHeaders` so that a concurrent `syncDeviceInfo` enrollment has time to write the identity to the store. */
555
+ async waitForEnrollment() {
556
+ await this.enroller.waitForEnrollment();
557
+ }
558
+ /** True when an enrollment is currently in-flight. Used by callers to skip triggering a competing enrollment. */
559
+ isEnrolling() {
560
+ return this.enroller.isEnrolling();
561
+ }
562
+ /** Rotate the HMAC secret while keeping the previous one for the grace window. */
563
+ async rotateSecret(opts = {}) {
564
+ const result = await this.enroller.rotateSecret(opts);
565
+ const stored = await this.identity.read();
566
+ return {
567
+ ...result,
568
+ gracePeriodEndsAt: stored?.previousSecretExpiresAt
569
+ };
570
+ }
571
+ /** Build the canonical 5-header map for an outbound signed request. */
572
+ async buildSignedHeaders(input) {
573
+ const stored = await this.identity.read();
574
+ if (!stored) return null;
575
+ return buildSignedHeaders({
576
+ method: input.method,
577
+ path: input.path,
578
+ body: input.body,
579
+ publicId: stored.publicId,
580
+ deviceSecret: stored.deviceSecret,
581
+ secretVersion: stored.secretVersion
582
+ });
583
+ }
584
+ /** Raw stored identity (CLI/extension internal use). Test seam too. */
585
+ async readIdentity() {
586
+ return this.identity.read();
587
+ }
588
+ /** Wipe the local identity (the `--force` path before re-enroll). */
589
+ async clear() {
590
+ await this.identity.clear();
591
+ }
592
+ /** Mark the device as claimed (called by the bridge after a successful claim). */
593
+ async markClaimed() {
594
+ await this.enroller.markClaimed();
595
+ }
596
+ /** Mark the device as expired (server returned an expiry response). */
597
+ async markExpired() {
598
+ await this.enroller.markExpired();
599
+ }
600
+ /** Expose the identity store (CLI uses it for direct file access in tests). */
601
+ getIdentityStore() {
602
+ return this.identity;
603
+ }
604
+ /** Expose the enroller (CLI uses it for state inspection). */
605
+ getEnroller() {
606
+ return this.enroller;
607
+ }
608
+ /** Header name constants — re-exported from `deviceAuth.ts`. */
609
+ getHeaderNames() {
610
+ return DeviceAuthHeaders;
611
+ }
612
+ /** Compute the installation id for the current machine. */
613
+ getInstallationId() {
614
+ return this.resolveInstallationId();
615
+ }
616
+ };
617
+ function projectIdentity(stored) {
618
+ return {
619
+ publicId: stored.publicId,
620
+ secretVersion: stored.secretVersion,
621
+ bindingState: stored.bindingState
622
+ };
623
+ }
624
+ function projectMetadata(stored) {
625
+ return {
626
+ installationId: stored.installationId,
627
+ machineId: stored.machineId,
628
+ platform: stored.platform,
629
+ hostname: stored.hostname
630
+ };
631
+ }
632
+ export {
633
+ DEVICE_JSON_SCHEMA_VERSION,
634
+ DeviceAuthHeaders,
635
+ DeviceCore,
636
+ DeviceReenrollRequiresAuthError,
637
+ DeviceSecretVersionMismatchError,
638
+ Enroller,
639
+ FsIdentityFileBackend,
640
+ IdentityStore,
641
+ buildSignedHeaders,
642
+ createDeviceRequestSignature,
643
+ deriveInstallationId,
644
+ fingerprintSource,
645
+ randomInstallationId
646
+ };
647
+ //# sourceMappingURL=device.mjs.map