@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.
- package/dist/auth.d.mts +590 -0
- package/dist/auth.d.ts +590 -0
- package/dist/auth.js +842 -0
- package/dist/auth.js.map +1 -0
- package/dist/auth.mjs +804 -0
- package/dist/auth.mjs.map +1 -0
- package/dist/device.d.mts +456 -0
- package/dist/device.d.ts +456 -0
- package/dist/device.js +696 -0
- package/dist/device.js.map +1 -0
- package/dist/device.mjs +647 -0
- package/dist/device.mjs.map +1 -0
- package/dist/index-CrNC-aao.d.ts +277 -0
- package/dist/index-Dmyy4urr.d.mts +277 -0
- package/dist/index.d.mts +1372 -27
- package/dist/index.d.ts +1372 -27
- package/dist/index.js +5125 -888
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5022 -906
- package/dist/index.mjs.map +1 -0
- package/dist/skill-linker.d.mts +188 -0
- package/dist/skill-linker.d.ts +188 -0
- package/dist/skill-linker.js +374 -0
- package/dist/skill-linker.js.map +1 -0
- package/dist/skill-linker.mjs +330 -0
- package/dist/skill-linker.mjs.map +1 -0
- package/dist/skill-store.d.mts +35 -0
- package/dist/skill-store.d.ts +35 -0
- package/dist/skill-store.js +291 -0
- package/dist/skill-store.js.map +1 -0
- package/dist/skill-store.mjs +254 -0
- package/dist/skill-store.mjs.map +1 -0
- package/dist/submit.d.mts +3 -0
- package/dist/submit.d.ts +3 -0
- package/dist/submit.js +166 -0
- package/dist/submit.js.map +1 -0
- package/dist/submit.mjs +130 -0
- package/dist/submit.mjs.map +1 -0
- package/dist/toolbox.d.mts +240 -0
- package/dist/toolbox.d.ts +240 -0
- package/dist/toolbox.js +530 -0
- package/dist/toolbox.js.map +1 -0
- package/dist/toolbox.mjs +482 -0
- package/dist/toolbox.mjs.map +1 -0
- package/dist/types-B9gk3dXH.d.mts +62 -0
- package/dist/types-B9gk3dXH.d.ts +62 -0
- package/package.json +50 -5
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { DeviceBindingState, DeviceEnrollResult, DeviceStatus, DeviceRotateSecretResult } from '@serviceme/devtools-protocol';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* deviceAuth — Pure HMAC-SHA-256 signing helpers for device auth headers.
|
|
5
|
+
*
|
|
6
|
+
* Ported byte-for-byte from
|
|
7
|
+
* `apps/extension/src/services/device/deviceAuth.ts:11-26` (the wire
|
|
8
|
+
* algorithm is locked by the server-side `device-signature-guard.ts`
|
|
9
|
+
* verifier, see `docs/architecture/phase-5-device-header-spec.md` §5).
|
|
10
|
+
*
|
|
11
|
+
* The basis is `METHOD\nPATH\nTIMESTAMP\nBODY\nSECRET` (LF-joined,
|
|
12
|
+
* NOT JSON). Output is lowercase hex SHA-256.
|
|
13
|
+
*
|
|
14
|
+
* Refs:
|
|
15
|
+
* - 4.功能规划.md §2.2 — `deviceAuth.ts HMAC 签名(纯 node:crypto,直接搬)`
|
|
16
|
+
* - `docs/architecture/phase-5-device-header-spec.md` §5 (signature format)
|
|
17
|
+
*/
|
|
18
|
+
/** Canonical header names — MUST match server's `device-signature-guard.ts:9-15`. */
|
|
19
|
+
declare const DeviceAuthHeaders: {
|
|
20
|
+
readonly deviceId: "x-ms-device-id";
|
|
21
|
+
readonly deviceSecret: "x-ms-device-secret";
|
|
22
|
+
readonly signature: "x-ms-device-signature";
|
|
23
|
+
readonly timestamp: "x-ms-device-timestamp";
|
|
24
|
+
readonly secretVersion: "x-ms-device-secret-version";
|
|
25
|
+
};
|
|
26
|
+
interface DeviceRequestSignatureParams {
|
|
27
|
+
method: string;
|
|
28
|
+
path: string;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
body: string;
|
|
31
|
+
secret: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute the HMAC-SHA-256 hex digest of the canonical basis.
|
|
35
|
+
*
|
|
36
|
+
* Server contract (`apps/server/src/lib/auth/device-signature-guard.ts:46-62`)
|
|
37
|
+
* is line-for-line identical: same LF-joined basis, same lowercase
|
|
38
|
+
* hex output. Any divergence breaks `device-signature-guard.test.ts`.
|
|
39
|
+
*/
|
|
40
|
+
declare function createDeviceRequestSignature(params: DeviceRequestSignatureParams): string;
|
|
41
|
+
/** 5-header map consumed by `fetch()` callers (CLI bridge, Extension). */
|
|
42
|
+
interface DeviceSignedHeaders {
|
|
43
|
+
[DeviceAuthHeaders.deviceId]: string;
|
|
44
|
+
[DeviceAuthHeaders.deviceSecret]: string;
|
|
45
|
+
[DeviceAuthHeaders.signature]: string;
|
|
46
|
+
[DeviceAuthHeaders.timestamp]: string;
|
|
47
|
+
[DeviceAuthHeaders.secretVersion]: string;
|
|
48
|
+
}
|
|
49
|
+
interface BuildSignedHeadersParams {
|
|
50
|
+
method: string;
|
|
51
|
+
path: string;
|
|
52
|
+
body: string;
|
|
53
|
+
publicId: string;
|
|
54
|
+
deviceSecret: string;
|
|
55
|
+
secretVersion: number;
|
|
56
|
+
/** Override for deterministic tests. */
|
|
57
|
+
timestamp?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the canonical 5-header map. The `body` parameter MUST be the
|
|
61
|
+
* exact byte sequence sent on the wire (no whitespace re-canonicalization
|
|
62
|
+
* between client serialization and signature basis construction).
|
|
63
|
+
*/
|
|
64
|
+
declare function buildSignedHeaders(params: BuildSignedHeadersParams): DeviceSignedHeaders;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Internal types for the device domain.
|
|
68
|
+
*
|
|
69
|
+
* These types are NOT re-exported from the protocol package — they are
|
|
70
|
+
* implementation details of the IdentityStore + Enroller. Public data
|
|
71
|
+
* models (the bridge wire shape) live in `@serviceme/devtools-protocol`'s
|
|
72
|
+
* `device.ts`.
|
|
73
|
+
*
|
|
74
|
+
* Refs:
|
|
75
|
+
* - 4.功能规划.md §2.2 — `device/types.ts`
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Schema version of the on-disk `device.json` file. Bumped when the
|
|
80
|
+
* shape changes incompatibly. IdentityStore checks this on read and
|
|
81
|
+
* either migrates (versions ≤ 1) or refuses (versions > supported).
|
|
82
|
+
*/
|
|
83
|
+
declare const DEVICE_JSON_SCHEMA_VERSION = 1;
|
|
84
|
+
/** Internal representation of the persisted identity file. */
|
|
85
|
+
interface PersistedDeviceIdentity {
|
|
86
|
+
version: number;
|
|
87
|
+
/** Stable per-machine id (UUID v4 shape) — survives secret rotates. */
|
|
88
|
+
installationId: string;
|
|
89
|
+
/** Raw `os.hostname()` for diagnostics. */
|
|
90
|
+
machineId: string;
|
|
91
|
+
/** Platform string (e.g. "darwin"). */
|
|
92
|
+
platform: string;
|
|
93
|
+
/** Optional hostname override for environments where `os.hostname()` is unstable. */
|
|
94
|
+
hostname?: string;
|
|
95
|
+
/** Public, non-secret id returned by the server. 32-char hex. */
|
|
96
|
+
publicId: string;
|
|
97
|
+
/** Monotonic secret version counter, starts at 1 after first enroll. */
|
|
98
|
+
secretVersion: number;
|
|
99
|
+
/** Current binding state — drives the re-enroll matrix. */
|
|
100
|
+
bindingState: DeviceBindingState;
|
|
101
|
+
/** HMAC secret (32 bytes hex-encoded = 64 chars). Persisted per `device-header-spec.md` §3.1. */
|
|
102
|
+
deviceSecret: string;
|
|
103
|
+
/** Optional: previous secret retained during the grace window (rotation). */
|
|
104
|
+
previousDeviceSecret?: string;
|
|
105
|
+
/** Optional: ISO timestamp at which the previous secret stops being accepted. */
|
|
106
|
+
previousSecretExpiresAt?: string;
|
|
107
|
+
/** ISO timestamp of the most recent successful enroll / rotate. */
|
|
108
|
+
lastEnrollAt: string;
|
|
109
|
+
/** Optional ISO timestamp of the most recent server sync. */
|
|
110
|
+
lastSyncAt?: string;
|
|
111
|
+
/** Optional human-readable message for the last sync error. */
|
|
112
|
+
lastSyncError?: string;
|
|
113
|
+
}
|
|
114
|
+
/** Result of a single atomic write. */
|
|
115
|
+
interface AtomicWriteResult {
|
|
116
|
+
bytesWritten: number;
|
|
117
|
+
/** Path to the temp file (post-rename it no longer exists; useful for diagnostics). */
|
|
118
|
+
tmpPath: string;
|
|
119
|
+
}
|
|
120
|
+
/** Hook called before/after every identity write — used by tests to assert concurrency safety. */
|
|
121
|
+
interface IdentityStoreHooks {
|
|
122
|
+
beforeWrite?: (next: PersistedDeviceIdentity) => void | Promise<void>;
|
|
123
|
+
afterWrite?: (next: PersistedDeviceIdentity) => void | Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* IdentityStore — Atomic JSON persistence for the device identity file.
|
|
128
|
+
*
|
|
129
|
+
* Stores the `PersistedDeviceIdentity` (incl. the HMAC secret cleartext)
|
|
130
|
+
* at `~/.serviceme/device.json` (per `phase-5-device-header-spec.md`
|
|
131
|
+
* §3.1). Writes are atomic via `write-tmp + fsync + rename`, matching
|
|
132
|
+
* the `SkillStore` / `ToolboxStore` precedent. Concurrent writes are
|
|
133
|
+
* serialized with a mkdir-based file lock (POSIX-atomic) — proper
|
|
134
|
+
* cross-process locking is deferred to Phase 6+ per the open spec.
|
|
135
|
+
*
|
|
136
|
+
* The file mode is `0600` (owner read/write only) so the cleartext
|
|
137
|
+
* secret stays safe at rest. On Windows the mode hint is a no-op
|
|
138
|
+
* (Windows uses ACLs) but `writeFile` still succeeds.
|
|
139
|
+
*
|
|
140
|
+
* Migration — IdentityStore auto-detects a v0-shape (pre-Phase-5.2)
|
|
141
|
+
* file written by the Extension's old `globalState` blob:
|
|
142
|
+
* { version: 1, claimed: false, publicKeyFingerprint: null }
|
|
143
|
+
* In that case the file is migrated forward to the v1 schema on the
|
|
144
|
+
* next write (the data fields are empty and a fresh enroll is required).
|
|
145
|
+
* The full Extension `globalState` → JSON migration happens in the
|
|
146
|
+
* Phase 5.5 adapter (`apps/extension/.../DeviceService.ts`) since the
|
|
147
|
+
* adapter holds the live `globalState` access.
|
|
148
|
+
*
|
|
149
|
+
* Refs:
|
|
150
|
+
* - 4.功能规划.md §2.2 — `IdentityStore.ts 持久化到 ~/.config/serviceme/device.json, 原子写`
|
|
151
|
+
* - `docs/architecture/phase-5-device-header-spec.md` §3.1, §2.5
|
|
152
|
+
*/
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Minimal interface for reading + writing the persisted identity file.
|
|
156
|
+
* Default impl uses `getDeviceJsonPath()` (which honors `SERVICEME_HOME`),
|
|
157
|
+
* but tests can substitute a custom path for isolation.
|
|
158
|
+
*/
|
|
159
|
+
interface IdentityFileBackend {
|
|
160
|
+
read(filePath: string): Promise<PersistedDeviceIdentity | null>;
|
|
161
|
+
write(filePath: string, payload: PersistedDeviceIdentity): Promise<AtomicWriteResult>;
|
|
162
|
+
exists(filePath: string): Promise<boolean>;
|
|
163
|
+
delete(filePath: string): Promise<void>;
|
|
164
|
+
listDir?(dir: string): Promise<string[]>;
|
|
165
|
+
}
|
|
166
|
+
interface IdentityStoreOptions {
|
|
167
|
+
filePath?: string;
|
|
168
|
+
hooks?: IdentityStoreHooks;
|
|
169
|
+
lockTimeoutMs?: number;
|
|
170
|
+
lockRetryMs?: number;
|
|
171
|
+
/** Injectable clock for deterministic tests. */
|
|
172
|
+
now?: () => Date;
|
|
173
|
+
backend?: IdentityFileBackend;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Default file backend — uses `node:fs/promises` with the canonical
|
|
177
|
+
* tmp-then-rename atomic-write pattern.
|
|
178
|
+
*/
|
|
179
|
+
declare class FsIdentityFileBackend implements IdentityFileBackend {
|
|
180
|
+
exists(filePath: string): Promise<boolean>;
|
|
181
|
+
read(filePath: string): Promise<PersistedDeviceIdentity | null>;
|
|
182
|
+
write(filePath: string, payload: PersistedDeviceIdentity): Promise<AtomicWriteResult>;
|
|
183
|
+
delete(filePath: string): Promise<void>;
|
|
184
|
+
}
|
|
185
|
+
declare class IdentityStore {
|
|
186
|
+
private readonly filePath;
|
|
187
|
+
private readonly backend;
|
|
188
|
+
private readonly hooks;
|
|
189
|
+
private readonly lockTimeoutMs;
|
|
190
|
+
private readonly lockRetryMs;
|
|
191
|
+
private readonly now;
|
|
192
|
+
constructor(opts?: IdentityStoreOptions);
|
|
193
|
+
/** Absolute path to the underlying JSON file (test seam). */
|
|
194
|
+
getFilePath(): string;
|
|
195
|
+
/** True when the JSON file already exists on disk. */
|
|
196
|
+
exists(): Promise<boolean>;
|
|
197
|
+
/** Read the persisted identity; returns `null` when no identity is stored. */
|
|
198
|
+
read(): Promise<PersistedDeviceIdentity | null>;
|
|
199
|
+
/**
|
|
200
|
+
* Atomically write the given identity. Concurrent writers are
|
|
201
|
+
* serialized via the file lock; the read-modify-write happens
|
|
202
|
+
* inside the lock so callers can't see a partial state.
|
|
203
|
+
*/
|
|
204
|
+
write(next: PersistedDeviceIdentity): Promise<AtomicWriteResult>;
|
|
205
|
+
/**
|
|
206
|
+
* Read-modify-write under the same lock. The mutator receives the
|
|
207
|
+
* current identity (or `null` on first call) and returns the
|
|
208
|
+
* replacement. Throwing inside the mutator aborts the write.
|
|
209
|
+
*/
|
|
210
|
+
mutate<T>(mutator: (current: PersistedDeviceIdentity | null) => Promise<{
|
|
211
|
+
next: PersistedDeviceIdentity;
|
|
212
|
+
result?: T;
|
|
213
|
+
}>): Promise<{
|
|
214
|
+
result: T | undefined;
|
|
215
|
+
written: PersistedDeviceIdentity;
|
|
216
|
+
}>;
|
|
217
|
+
/** Wipe the persisted identity (used by `device.enroll --force`). */
|
|
218
|
+
clear(): Promise<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Resolve the installation metadata for the current machine.
|
|
221
|
+
* Pure helper — no I/O, just `os.*` calls.
|
|
222
|
+
*/
|
|
223
|
+
resolveInstallationMetadata(): Pick<PersistedDeviceIdentity, "installationId" | "machineId" | "platform">;
|
|
224
|
+
/**
|
|
225
|
+
* Ensure the parent directory exists (`~/.serviceme/`). Idempotent.
|
|
226
|
+
* Useful when the bootstrap phase5 placeholder wasn't run yet.
|
|
227
|
+
*/
|
|
228
|
+
ensureHome(): Promise<void>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Enroller — State machine for `device.enroll` and `device.rotate-secret`.
|
|
233
|
+
*
|
|
234
|
+
* States per `2.需求澄清.md` §1.2:
|
|
235
|
+
* anonymous → pending → claimed → expired
|
|
236
|
+
*
|
|
237
|
+
* - `anonymous` (initial): no device has ever enrolled. Server returns
|
|
238
|
+
* a fresh `publicId` + secret.
|
|
239
|
+
* - `pending`: enrollment HTTP call has been issued but the server
|
|
240
|
+
* hasn't confirmed yet. In-flight state — never persisted.
|
|
241
|
+
* - `claimed`: user has linked this device to their account (via
|
|
242
|
+
* `/api/v1/devices/claim`). Sticky binding locks future re-enrolls
|
|
243
|
+
* to the same `userId` (server-side matrix).
|
|
244
|
+
* - `expired`: server returned a device-expiry error. Forces a fresh
|
|
245
|
+
* enroll on next call.
|
|
246
|
+
*
|
|
247
|
+
* `--force` semantics: any non-anonymous state can be force-reset to
|
|
248
|
+
* `anonymous` by wiping the local identity file. The next enroll will
|
|
249
|
+
* be treated as a brand-new install by the server (no sticky binding).
|
|
250
|
+
*
|
|
251
|
+
* The Enroller is the **state machine**; the actual HTTP I/O is the
|
|
252
|
+
* caller's responsibility (the `DeviceSyncClient` in Phase 5.4 wires
|
|
253
|
+
* the server). This split keeps the Enroller unit-testable without
|
|
254
|
+
* a live server.
|
|
255
|
+
*
|
|
256
|
+
* Refs:
|
|
257
|
+
* - 4.功能规划.md §2.2 — `Enroller.ts anonymous → pending → claimed → expired`
|
|
258
|
+
* - `2.需求澄清.md` §1.2 — binding-state machine
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
interface EnrollerOptions {
|
|
262
|
+
identityStore: IdentityStore;
|
|
263
|
+
/** Injectable clock for deterministic tests. */
|
|
264
|
+
now?: () => Date;
|
|
265
|
+
/** Override the random source (tests). */
|
|
266
|
+
randomBytes?: (size: number) => Buffer;
|
|
267
|
+
/** Caller-supplied enroll HTTP function. Phase 5.4 wires the real one. */
|
|
268
|
+
enrollRequest?: EnrollRequestFn;
|
|
269
|
+
}
|
|
270
|
+
type EnrollRequestFn = (input: {
|
|
271
|
+
installationId: string;
|
|
272
|
+
machineId: string;
|
|
273
|
+
platform: string;
|
|
274
|
+
existing: PersistedDeviceIdentity | null;
|
|
275
|
+
force: boolean;
|
|
276
|
+
requireAuth: boolean;
|
|
277
|
+
}) => Promise<EnrollResponse>;
|
|
278
|
+
interface EnrollResponse {
|
|
279
|
+
publicId: string;
|
|
280
|
+
deviceSecret: string;
|
|
281
|
+
secretVersion: number;
|
|
282
|
+
bindingState: DeviceBindingState;
|
|
283
|
+
expiresAt?: string;
|
|
284
|
+
}
|
|
285
|
+
/** Sentinel error — re-enroll on a claimed device without auth. */
|
|
286
|
+
declare class DeviceReenrollRequiresAuthError extends Error {
|
|
287
|
+
constructor(message?: string);
|
|
288
|
+
}
|
|
289
|
+
/** Sentinel error — server returned a 410 / version-mismatch after rotation. */
|
|
290
|
+
declare class DeviceSecretVersionMismatchError extends Error {
|
|
291
|
+
constructor(message?: string);
|
|
292
|
+
}
|
|
293
|
+
declare class Enroller {
|
|
294
|
+
private readonly identity;
|
|
295
|
+
private readonly now;
|
|
296
|
+
private readonly random;
|
|
297
|
+
private readonly enrollRequest?;
|
|
298
|
+
private inflight;
|
|
299
|
+
constructor(opts: EnrollerOptions);
|
|
300
|
+
/**
|
|
301
|
+
* Read the current binding state without touching the disk.
|
|
302
|
+
* Returns `anonymous` when no identity is stored.
|
|
303
|
+
*/
|
|
304
|
+
currentState(): Promise<DeviceBindingState>;
|
|
305
|
+
/**
|
|
306
|
+
* Drive the enrollment flow.
|
|
307
|
+
*
|
|
308
|
+
* @param force when true, drop the local identity and start fresh
|
|
309
|
+
* (server treats this as a brand-new install).
|
|
310
|
+
* @param requireAuth when true, refuse to silently re-enroll an
|
|
311
|
+
* existing claimed device — throw
|
|
312
|
+
* `DeviceReenrollRequiresAuthError` instead.
|
|
313
|
+
*/
|
|
314
|
+
/**
|
|
315
|
+
* Resolve when any in-flight enrollment completes. Returns immediately
|
|
316
|
+
* when no enrollment is in progress. Allows callers (e.g. the extension's
|
|
317
|
+
* `buildDeviceAuthHeaders`) to wait for a concurrent `syncDeviceInfo()`
|
|
318
|
+
* enrollment before attempting to read the identity from the store.
|
|
319
|
+
*/
|
|
320
|
+
waitForEnrollment(): Promise<void>;
|
|
321
|
+
enroll(opts?: {
|
|
322
|
+
force?: boolean;
|
|
323
|
+
requireAuth?: boolean;
|
|
324
|
+
}): Promise<DeviceEnrollResult>;
|
|
325
|
+
/** Test seam — surface the underlying identity store. */
|
|
326
|
+
getIdentityStore(): IdentityStore;
|
|
327
|
+
/** True when an enrollment is currently in-flight. Used by callers (e.g. the extension's `buildDeviceAuthHeaders`) to skip triggering a competing enrollment. */
|
|
328
|
+
isEnrolling(): boolean;
|
|
329
|
+
private runEnroll;
|
|
330
|
+
/**
|
|
331
|
+
* Rotate the HMAC secret. Keeps the previous secret for the grace
|
|
332
|
+
* window (default 7 days per `2.需求澄清.md` §1.2) — the
|
|
333
|
+
* `previousSecretExpiresAt` is stamped on the persisted identity.
|
|
334
|
+
*/
|
|
335
|
+
rotateSecret(opts?: {
|
|
336
|
+
gracePeriodDays?: number;
|
|
337
|
+
}): Promise<{
|
|
338
|
+
publicId: string;
|
|
339
|
+
secretVersion: number;
|
|
340
|
+
gracePeriodDays: number;
|
|
341
|
+
}>;
|
|
342
|
+
/**
|
|
343
|
+
* Mark the device as `expired`. Used when the server returns a
|
|
344
|
+
* device-expiry response; the next `enroll()` call forces a fresh
|
|
345
|
+
* round-trip.
|
|
346
|
+
*/
|
|
347
|
+
markExpired(): Promise<void>;
|
|
348
|
+
/**
|
|
349
|
+
* Mark the device as `claimed`. Called by the bridge after a
|
|
350
|
+
* successful `device.claim` server response.
|
|
351
|
+
*/
|
|
352
|
+
markClaimed(): Promise<void>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* DeviceCore — Main entry for the device domain.
|
|
357
|
+
*
|
|
358
|
+
* Aggregates `IdentityStore` + `Enroller` + signer helpers into a single
|
|
359
|
+
* surface that the CLI / Extension / Bridge can call. Pure orchestration
|
|
360
|
+
* — no HTTP of its own (the actual `POST /api/v1/devices/enroll` lives
|
|
361
|
+
* behind `EnrollerOptions.enrollRequest`, wired in Phase 5.4).
|
|
362
|
+
*
|
|
363
|
+
* Refs:
|
|
364
|
+
* - 4.功能规划.md §2.2 — `DeviceCore.ts 主入口`
|
|
365
|
+
* - ADL-003 — data model in `@serviceme/devtools-protocol`
|
|
366
|
+
* - `docs/architecture/phase-5-device-header-spec.md` §3.1
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
interface DeviceCoreOptions {
|
|
370
|
+
identityStore?: IdentityStore;
|
|
371
|
+
enrollRequest?: EnrollRequestFn;
|
|
372
|
+
now?: () => Date;
|
|
373
|
+
/** Optional override for `deriveInstallationId` (used by tests for determinism). */
|
|
374
|
+
resolveInstallationId?: () => string;
|
|
375
|
+
}
|
|
376
|
+
declare class DeviceCore {
|
|
377
|
+
private readonly identity;
|
|
378
|
+
private readonly enroller;
|
|
379
|
+
private readonly resolveInstallationId;
|
|
380
|
+
constructor(opts?: DeviceCoreOptions);
|
|
381
|
+
/** Read-only snapshot of the device status (matches `device.status` wire shape). */
|
|
382
|
+
status(): Promise<DeviceStatus>;
|
|
383
|
+
/** Enroll (or re-enroll) the device. */
|
|
384
|
+
enroll(opts?: {
|
|
385
|
+
force?: boolean;
|
|
386
|
+
requireAuth?: boolean;
|
|
387
|
+
}): Promise<DeviceEnrollResult>;
|
|
388
|
+
/** 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. */
|
|
389
|
+
waitForEnrollment(): Promise<void>;
|
|
390
|
+
/** True when an enrollment is currently in-flight. Used by callers to skip triggering a competing enrollment. */
|
|
391
|
+
isEnrolling(): boolean;
|
|
392
|
+
/** Rotate the HMAC secret while keeping the previous one for the grace window. */
|
|
393
|
+
rotateSecret(opts?: {
|
|
394
|
+
gracePeriodDays?: number;
|
|
395
|
+
}): Promise<DeviceRotateSecretResult>;
|
|
396
|
+
/** Build the canonical 5-header map for an outbound signed request. */
|
|
397
|
+
buildSignedHeaders(input: {
|
|
398
|
+
method: string;
|
|
399
|
+
path: string;
|
|
400
|
+
body: string;
|
|
401
|
+
}): Promise<DeviceSignedHeaders | null>;
|
|
402
|
+
/** Raw stored identity (CLI/extension internal use). Test seam too. */
|
|
403
|
+
readIdentity(): Promise<PersistedDeviceIdentity | null>;
|
|
404
|
+
/** Wipe the local identity (the `--force` path before re-enroll). */
|
|
405
|
+
clear(): Promise<void>;
|
|
406
|
+
/** Mark the device as claimed (called by the bridge after a successful claim). */
|
|
407
|
+
markClaimed(): Promise<void>;
|
|
408
|
+
/** Mark the device as expired (server returned an expiry response). */
|
|
409
|
+
markExpired(): Promise<void>;
|
|
410
|
+
/** Expose the identity store (CLI uses it for direct file access in tests). */
|
|
411
|
+
getIdentityStore(): IdentityStore;
|
|
412
|
+
/** Expose the enroller (CLI uses it for state inspection). */
|
|
413
|
+
getEnroller(): Enroller;
|
|
414
|
+
/** Header name constants — re-exported from `deviceAuth.ts`. */
|
|
415
|
+
getHeaderNames(): typeof DeviceAuthHeaders;
|
|
416
|
+
/** Compute the installation id for the current machine. */
|
|
417
|
+
getInstallationId(): string;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* InstallationId — Derive a stable per-machine identifier from
|
|
422
|
+
* `os.hostname()` + `os.userInfo()`.
|
|
423
|
+
*
|
|
424
|
+
* Per `docs/requirements/phase-5-auth-device-toolbox/3.功能拆分.md` B2,
|
|
425
|
+
* `installationId` MUST survive Extension re-installs but vary across
|
|
426
|
+
* machines. We compute a UUID v5-style hash over hostname + username +
|
|
427
|
+
* platform so the result is:
|
|
428
|
+
* - deterministic (same machine → same id)
|
|
429
|
+
* - collision-resistant (SHA-256, 128-bit truncated)
|
|
430
|
+
* - browser-safe (no PII survives — username never enters output)
|
|
431
|
+
*
|
|
432
|
+
* Note: this intentionally differs from `vscode.env.machineId`, which
|
|
433
|
+
* is per-Extension-install and uses a different algorithm. The two
|
|
434
|
+
* coexist: `installationId` is what gets sent to the server, while
|
|
435
|
+
* `machineId` (raw `os.hostname()`) is for diagnostics.
|
|
436
|
+
*
|
|
437
|
+
* Refs:
|
|
438
|
+
* - 4.功能规划.md §2.2 — `InstallationId.ts os.hostname() + os.userInfo() 哈希生成`
|
|
439
|
+
* - 3.功能拆分.md B2 — installationId semantics
|
|
440
|
+
*/
|
|
441
|
+
/**
|
|
442
|
+
* Returns a deterministic installation id for the current machine.
|
|
443
|
+
* Use this when you need an id that survives Extension reinstalls
|
|
444
|
+
* but stays stable across restarts on the same machine.
|
|
445
|
+
*/
|
|
446
|
+
declare function deriveInstallationId(): string;
|
|
447
|
+
/**
|
|
448
|
+
* Returns a random installation id (UUID v4). Use this for fresh
|
|
449
|
+
* installs when no fingerprint input is available (e.g. containerized
|
|
450
|
+
* CI runners where `os.hostname()` is meaningless).
|
|
451
|
+
*/
|
|
452
|
+
declare function randomInstallationId(): string;
|
|
453
|
+
/** SHA-256 fingerprint material exposed for tests + diagnostics. */
|
|
454
|
+
declare function fingerprintSource(): string;
|
|
455
|
+
|
|
456
|
+
export { type AtomicWriteResult, type BuildSignedHeadersParams, DEVICE_JSON_SCHEMA_VERSION, DeviceAuthHeaders, DeviceCore, type DeviceCoreOptions, DeviceReenrollRequiresAuthError, type DeviceRequestSignatureParams, DeviceSecretVersionMismatchError, type DeviceSignedHeaders, type EnrollRequestFn, type EnrollResponse, Enroller, type EnrollerOptions, FsIdentityFileBackend, type IdentityFileBackend, IdentityStore, type IdentityStoreHooks, type IdentityStoreOptions, type PersistedDeviceIdentity, buildSignedHeaders, createDeviceRequestSignature, deriveInstallationId, fingerprintSource, randomInstallationId };
|