@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 @@
1
+ {"version":3,"sources":["../src/auth/AccessControl.ts","../src/auth/AuthStateManager.ts","../src/auth/ProviderRegistry.ts","../src/auth/AuthCore.ts","../src/auth/KeychainAuthTokenStore.ts","../src/auth/utils/githubUserEmail.ts","../src/auth/providers/GitHubAuthProvider.ts","../src/auth/providers/MicrosoftAuthProvider.ts"],"sourcesContent":["/**\n * AccessControl — Pure logic for org-membership gating.\n *\n * Ported from `apps/extension/src/services/auth/AccessControlService.ts`\n * but stripped of all VSCode dependencies:\n * - No `vscode.window.showWarningMessage` — callers render the notice.\n * - No `vscode.context.globalState` — callers inject a `KeyValueStore`.\n * - No `vscode.Event` — uses a plain `onDidChange` callback.\n *\n * The class is generic over the membership-fetcher function so it can\n * run identically with the CLI's `@serviceme/shared` `getGitHubOrgMembership`\n * or with the Extension's bundled copy. ADL-003 forbids Core from\n * importing `@serviceme/shared` directly, so the org membership\n * result type is defined locally and the runtime adapter wraps the\n * shared helper at the boundary (Phase 5.3+).\n *\n * Refs:\n * - 4.功能规划.md §2.1 — `AccessControl.ts 从 AccessControlService 改写`\n * - ADL-003 — `@serviceme/shared` boundary (Core MUST NOT import shared)\n */\n\nimport type { AuthAccountMeta, AuthProvider } from \"@serviceme/devtools-protocol\";\n\n/** Core-local mirror of `GitHubOrgMembershipCheckResult` (defined in `@serviceme/shared`). */\nexport interface CoreOrgMembershipResult {\n\tstatus: \"active\" | \"pending\" | \"not_member\" | \"unknown\";\n\thttpStatus?: number;\n\trole?: string;\n\tdirectMembership?: boolean;\n}\n\n/**\n * Upstream org-membership fetcher. CLI / Extension adapters wrap\n * `getGitHubOrgMembership` (or the local extension copy) to match\n * this signature.\n */\nexport type OrgMembershipFetcher = (token: string, org: string) => Promise<CoreOrgMembershipResult>;\n\n/** Pluggable key/value store. Extension injects `globalState`-backed impl; CLI injects a file-backed impl. */\nexport interface KeyValueStore {\n\tget<T>(key: string): T | undefined;\n\tupdate<T>(key: string, value: T): Promise<void>;\n}\n\nexport interface AccessCheckResult {\n\tallowed: boolean;\n\treason?: \"not_authenticated\" | \"not_member\";\n\tusername?: string;\n}\n\nexport interface AccessControlOptions {\n\torg: string;\n\t/** Domains that auto-qualify Microsoft users without a GitHub link. */\n\tmicrosoftEmailDomains?: readonly string[];\n\t/** TTL for in-memory membership cache (ms). */\n\tcacheTtlMs?: number;\n\t/** TTL for persisted membership cache (ms). */\n\tpersistentCacheTtlMs?: number;\n\t/** Tokens whose bearer can be supplied to `OrgMembershipFetcher`. */\n\ttokenFetcher: (provider: AuthProvider) => Promise<string | null>;\n\t/** Upstream org-membership fetcher (typically `getGitHubOrgMembership` from `@serviceme/shared`). */\n\torgFetcher: OrgMembershipFetcher;\n\t/** Clock seam for tests. */\n\tnow?: () => number;\n}\n\nconst DEFAULT_CACHE_TTL_MS = 10 * 60 * 1000;\nconst DEFAULT_PERSISTENT_CACHE_TTL_MS = 24 * 60 * 60 * 1000;\nconst KEY_GRANTED_USERS = \"msDevTools.accessControl.grantedUsers\";\nconst KEY_MEMBERSHIP_CACHE = \"msDevTools.accessControl.membershipCache\";\nconst KEY_SERVER_IN_ORG = \"msDevTools.accessControl.serverInOrg\";\n\ninterface CacheEntry {\n\tisMember: boolean;\n\tat: number;\n}\n\ninterface PersistentCache {\n\t[username: string]: CacheEntry;\n}\n\nexport class AccessControl {\n\tprivate readonly memCache = new Map<string, CacheEntry>();\n\tprivate readonly serverInOrgMem = new Map<string, boolean>();\n\tprivate readonly listeners = new Set<() => void>();\n\tprivate readonly opts: Required<\n\t\tOmit<AccessControlOptions, \"now\" | \"tokenFetcher\" | \"microsoftEmailDomains\" | \"orgFetcher\">\n\t> & {\n\t\ttokenFetcher: AccessControlOptions[\"tokenFetcher\"];\n\t\torgFetcher: OrgMembershipFetcher;\n\t\tmicrosoftEmailDomains: readonly string[];\n\t\tnow: () => number;\n\t};\n\n\tconstructor(\n\t\tprivate readonly kv: KeyValueStore,\n\t\toptions: AccessControlOptions\n\t) {\n\t\tthis.opts = {\n\t\t\torg: options.org,\n\t\t\ttokenFetcher: options.tokenFetcher,\n\t\t\torgFetcher: options.orgFetcher,\n\t\t\tmicrosoftEmailDomains: options.microsoftEmailDomains ?? [],\n\t\t\tcacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,\n\t\t\tpersistentCacheTtlMs: options.persistentCacheTtlMs ?? DEFAULT_PERSISTENT_CACHE_TTL_MS,\n\t\t\tnow: options.now ?? (() => Date.now()),\n\t\t};\n\t\t// Hydrate `serverInOrg` from persistent storage so the gate can run instantly on restart.\n\t\tconst persistedServerInOrg = this.kv.get<Record<string, boolean>>(KEY_SERVER_IN_ORG);\n\t\tif (persistedServerInOrg) {\n\t\t\tfor (const [key, value] of Object.entries(persistedServerInOrg)) {\n\t\t\t\tthis.serverInOrgMem.set(key, value);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Subscribe to inOrg changes (used by extension to refresh access-gated UI). */\n\tonDidChange(listener: () => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => this.listeners.delete(listener);\n\t}\n\n\t/** Full check: requires authentication + org membership / admin grant. */\n\tasync checkAccess(user: AuthAccountMeta | null): Promise<AccessCheckResult> {\n\t\tif (!user) {\n\t\t\treturn { allowed: false, reason: \"not_authenticated\" };\n\t\t}\n\n\t\t// Microsoft users: server-confirmed inOrg takes priority over GitHub link.\n\t\tif (user.provider === \"microsoft\" && user.login) {\n\t\t\tconst inOrg = this.serverInOrgMem.get(user.login.toLowerCase());\n\t\t\tif (inOrg === true) return { allowed: true };\n\t\t\tif (inOrg === false) {\n\t\t\t\treturn { allowed: false, reason: \"not_member\", username: user.login };\n\t\t\t}\n\t\t}\n\n\t\t// Find a GitHub account if available (linked for Microsoft users).\n\t\tconst githubAccount = user.provider === \"github\" ? user : null;\n\t\tif (!githubAccount && user.provider === \"microsoft\" && user.login) {\n\t\t\tconst [, domain = \"\"] = user.login.split(\"@\");\n\t\t\tconst isDomainMember =\n\t\t\t\tdomain.length > 0 && this.opts.microsoftEmailDomains.includes(domain.toLowerCase());\n\t\t\tif (isDomainMember) return { allowed: true };\n\t\t\t// No GitHub link + unknown domain — deny until the next server-confirmed claim.\n\t\t\treturn { allowed: false, reason: \"not_member\", username: user.login };\n\t\t}\n\t\tif (!githubAccount) {\n\t\t\t// No identity at all — treat as authenticated for now (caller may have other gating).\n\t\t\treturn { allowed: true };\n\t\t}\n\n\t\tconst accessUsername = githubAccount.login ?? user.login ?? \"\";\n\t\tif (accessUsername.length === 0) {\n\t\t\treturn { allowed: false, reason: \"not_member\", username: \"\" };\n\t\t}\n\n\t\t// Admin-granted users bypass the org check.\n\t\tconst granted = this.getGrantedUsers();\n\t\tif (granted.includes(accessUsername.toLowerCase())) {\n\t\t\treturn { allowed: true };\n\t\t}\n\n\t\tconst isMember = await this.checkOrgMembership(accessUsername);\n\t\treturn isMember\n\t\t\t? { allowed: true }\n\t\t\t: { allowed: false, reason: \"not_member\", username: accessUsername };\n\t}\n\n\t/** Admin: grant access to a GitHub username (bypasses org check). */\n\tasync grantAccess(username: string): Promise<void> {\n\t\tconst list = this.getGrantedUsers();\n\t\tconst key = username.toLowerCase();\n\t\tif (!list.includes(key)) {\n\t\t\tlist.push(key);\n\t\t\tawait this.kv.update(KEY_GRANTED_USERS, list);\n\t\t}\n\t}\n\n\t/** Admin: revoke a previously granted access. */\n\tasync revokeAccess(username: string): Promise<void> {\n\t\tconst updated = this.getGrantedUsers().filter((u) => u !== username.toLowerCase());\n\t\tawait this.kv.update(KEY_GRANTED_USERS, updated);\n\t}\n\n\t/** Snapshot of all admin-granted usernames (lowercased). */\n\tgetGrantedUsers(): string[] {\n\t\treturn this.kv.get<string[]>(KEY_GRANTED_USERS) ?? [];\n\t}\n\n\t/** Cached membership lookup with both in-memory + persistent layers. */\n\tasync checkOrgMembership(username: string): Promise<boolean> {\n\t\tconst now = this.opts.now();\n\n\t\t// 1. In-memory cache.\n\t\tconst inMem = this.memCache.get(username);\n\t\tif (inMem && now - inMem.at < this.opts.cacheTtlMs) {\n\t\t\treturn inMem.isMember;\n\t\t}\n\n\t\t// 2. Persistent cache.\n\t\tconst persistent = this.kv.get<PersistentCache>(KEY_MEMBERSHIP_CACHE) ?? {};\n\t\tconst persistentEntry = persistent[username];\n\t\tif (persistentEntry && now - persistentEntry.at < this.opts.persistentCacheTtlMs) {\n\t\t\tthis.memCache.set(username, persistentEntry);\n\t\t\treturn persistentEntry.isMember;\n\t\t}\n\n\t\t// 3. Hit upstream GitHub API via the injected fetcher.\n\t\tconst token = await this.opts.tokenFetcher(\"github\");\n\t\tif (!token) return false;\n\n\t\ttry {\n\t\t\tconst result = await this.opts.orgFetcher(token, this.opts.org);\n\t\t\tconst decision = this.resolveDecision(username, result);\n\t\t\tif (typeof decision === \"boolean\") {\n\t\t\t\tthis.updateCache(username, decision);\n\t\t\t\treturn decision;\n\t\t\t}\n\t\t\t// Indeterminate — fail-open.\n\t\t\treturn true;\n\t\t} catch {\n\t\t\t// Fail-open on transient network errors.\n\t\t\treturn true;\n\t\t}\n\t}\n\n\t/** Server-confirmed inOrg status (set by device claim flow). */\n\tsetServerInOrg(username: string, inOrg: boolean): void {\n\t\tconst key = username.toLowerCase();\n\t\tthis.serverInOrgMem.set(key, inOrg);\n\t\tconst persisted = this.kv.get<Record<string, boolean>>(KEY_SERVER_IN_ORG) ?? {};\n\t\tpersisted[key] = inOrg;\n\t\tvoid this.kv.update(KEY_SERVER_IN_ORG, persisted);\n\t\tthis.notifyListeners();\n\t}\n\n\t/** Inspect server-confirmed inOrg for a username; returns undefined when unknown. */\n\tgetServerInOrg(username: string): boolean | undefined {\n\t\treturn this.serverInOrgMem.get(username.toLowerCase());\n\t}\n\n\tprivate resolveDecision(\n\t\t_username: string,\n\t\tmembership: CoreOrgMembershipResult\n\t): boolean | undefined {\n\t\tif (membership.status === \"active\" || membership.status === \"pending\") return true;\n\t\tif (membership.status === \"not_member\") return false;\n\t\treturn undefined;\n\t}\n\n\tprivate updateCache(username: string, isMember: boolean): void {\n\t\tconst at = this.opts.now();\n\t\tthis.memCache.set(username, { isMember, at });\n\t\tconst persistent = this.kv.get<PersistentCache>(KEY_MEMBERSHIP_CACHE) ?? {};\n\t\tpersistent[username] = { isMember, at };\n\t\tvoid this.kv.update(KEY_MEMBERSHIP_CACHE, persistent);\n\t}\n\n\tprivate notifyListeners(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\ttry {\n\t\t\t\tlistener();\n\t\t\t} catch {\n\t\t\t\t// listener errors must not propagate; AccessControl is best-effort.\n\t\t\t}\n\t\t}\n\t}\n}\n","/**\n * AuthStateManager — In-memory mirror of the persisted auth state.\n *\n * Holds the multi-account map (provider + accountId keyed) plus the\n * \"active\" provider/account pair. Emits change events via Node's\n * built-in `events.EventEmitter` so listeners stay decoupled.\n *\n * Important: this class does NOT touch `vscode.SecretStorage` or any\n * file — it only keeps the metadata (`AuthAccountMeta`) and the\n * `activeProvider` selection. Token bytes are looked up via the\n * `KeychainAuthTokenStore` on demand. This separation is what lets\n * `AuthCore` run identically in CLI + Extension.\n *\n * Refs:\n * - 4.功能规划.md §2.1 — `AuthStateManager.ts events.EventEmitter, NO VSCode dep`\n * - ADL-004 — token bytes never enter Core state\n */\n\nimport { EventEmitter } from \"node:events\";\n\nimport type { AuthAccountMeta, AuthProvider, AuthStatus } from \"@serviceme/devtools-protocol\";\n\nexport interface AuthStateManagerEvents {\n\tchange: () => void;\n}\n\nexport interface AuthStateManagerOptions {\n\t/** EventEmitter listener cap; defaults to 32 (Node default) but raised for hot test paths. */\n\tmaxListeners?: number;\n}\n\nexport class AuthStateManager {\n\tprivate readonly emitter = new EventEmitter();\n\tprivate accounts: AuthAccountMeta[] = [];\n\tprivate activeProvider: AuthProvider | null = null;\n\tprivate activeAccountId: string | null = null;\n\tprivate lastError: string | null = null;\n\n\tconstructor(opts: AuthStateManagerOptions = {}) {\n\t\tif (opts.maxListeners !== undefined) {\n\t\t\tthis.emitter.setMaxListeners(opts.maxListeners);\n\t\t}\n\t}\n\n\t/** Subscribe to state-change events. Returns a disposer. */\n\tonDidChange(listener: () => void): () => void {\n\t\tthis.emitter.on(\"change\", listener);\n\t\treturn () => this.emitter.off(\"change\", listener);\n\t}\n\n\t/** Snapshot of all known accounts (immutable copy). */\n\tlistAccounts(): AuthAccountMeta[] {\n\t\treturn [...this.accounts];\n\t}\n\n\t/** Find an account by `(provider, accountId)` tuple; returns `null` when absent. */\n\tfindAccount(provider: AuthProvider, accountId: string): AuthAccountMeta | null {\n\t\treturn this.accounts.find((a) => a.provider === provider && a.id === accountId) ?? null;\n\t}\n\n\t/** First account for the requested provider — used for \"switch to GitHub\" UX. */\n\tfindFirstForProvider(provider: AuthProvider): AuthAccountMeta | null {\n\t\treturn this.accounts.find((a) => a.provider === provider) ?? null;\n\t}\n\n\t/** Insert or update an account entry. New accounts land at the head of the list. */\n\tupsertAccount(meta: AuthAccountMeta): void {\n\t\tconst idx = this.accounts.findIndex((a) => a.provider === meta.provider && a.id === meta.id);\n\t\tif (idx >= 0) {\n\t\t\tthis.accounts[idx] = meta;\n\t\t} else {\n\t\t\tthis.accounts.unshift(meta);\n\t\t}\n\t\tthis.fire();\n\t}\n\n\t/** Remove an account entry. Returns `true` when an entry was removed. */\n\tremoveAccount(provider: AuthProvider, accountId: string): boolean {\n\t\tconst before = this.accounts.length;\n\t\tthis.accounts = this.accounts.filter((a) => !(a.provider === provider && a.id === accountId));\n\t\tconst removed = this.accounts.length !== before;\n\t\t// If we removed the active provider's account, clear the active marker.\n\t\tif (removed && this.activeProvider === provider && this.activeAccountId === accountId) {\n\t\t\tconst stillHasSameProvider = this.accounts.some((a) => a.provider === provider);\n\t\t\tif (!stillHasSameProvider) {\n\t\t\t\tthis.activeProvider = null;\n\t\t\t\tthis.activeAccountId = null;\n\t\t\t} else {\n\t\t\t\tconst next = this.accounts.find((a) => a.provider === provider);\n\t\t\t\tif (next) {\n\t\t\t\t\tthis.activeAccountId = next.id;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (removed) this.fire();\n\t\treturn removed;\n\t}\n\n\t/** Set the active provider. When `accountId` is omitted, picks the first account for that provider. */\n\tsetActive(provider: AuthProvider, accountId?: string): boolean {\n\t\tconst match =\n\t\t\taccountId !== undefined\n\t\t\t\t? this.accounts.find((a) => a.provider === provider && a.id === accountId)\n\t\t\t\t: this.accounts.find((a) => a.provider === provider);\n\t\tif (!match) return false;\n\t\tthis.activeProvider = provider;\n\t\tthis.activeAccountId = match.id;\n\t\tthis.fire();\n\t\treturn true;\n\t}\n\n\t/** Currently active provider — `null` when no session is active. */\n\tgetActiveProvider(): AuthProvider | null {\n\t\treturn this.activeProvider;\n\t}\n\n\t/** Currently active account metadata — `null` when none. */\n\tgetActiveAccount(): AuthAccountMeta | null {\n\t\tif (!this.activeProvider || !this.activeAccountId) return null;\n\t\treturn (\n\t\t\tthis.accounts.find(\n\t\t\t\t(a) => a.provider === this.activeProvider && a.id === this.activeAccountId\n\t\t\t) ?? null\n\t\t);\n\t}\n\n\t/** True when at least one provider has an account entry. */\n\thasAnySession(): boolean {\n\t\treturn this.accounts.length > 0 && this.activeProvider !== null;\n\t}\n\n\t/** Snapshot of the state for `auth.status` and bridge serialization. */\n\tgetStatus(): AuthStatus {\n\t\tconst grouped: Partial<Record<AuthProvider, AuthAccountMeta>> = {};\n\t\tfor (const account of this.accounts) {\n\t\t\tgrouped[account.provider] = account;\n\t\t}\n\t\treturn {\n\t\t\tactiveProvider: this.activeProvider,\n\t\t\taccounts: grouped,\n\t\t\thasAnySession: this.hasAnySession(),\n\t\t\tlastError: this.lastError ?? undefined,\n\t\t};\n\t}\n\n\t/** Record a non-fatal error from the last login/refresh attempt. */\n\trecordError(message: string): void {\n\t\tthis.lastError = message;\n\t\tthis.fire();\n\t}\n\n\t/** Clear the recorded error (e.g. after a successful login). */\n\tclearError(): void {\n\t\tif (this.lastError === null) return;\n\t\tthis.lastError = null;\n\t\tthis.fire();\n\t}\n\n\t/** Drop every account — used by `auth.logout` with no provider. */\n\tclearAll(): void {\n\t\tthis.accounts = [];\n\t\tthis.activeProvider = null;\n\t\tthis.activeAccountId = null;\n\t\tthis.lastError = null;\n\t\tthis.fire();\n\t}\n\n\tprivate fire(): void {\n\t\tthis.emitter.emit(\"change\");\n\t}\n}\n","/**\n * ProviderRegistry — Maps `AuthProvider` ids to `IAuthProvider` instances.\n *\n * `AuthCore` looks up providers by `AuthProvider` enum (string union\n * per the protocol). The registry is mutable so that callers can\n * replace a provider (e.g. swap a real keyring-backed implementation\n * in tests), but providers must be registered before `AuthCore.login()`\n * is called.\n *\n * Refs:\n * - 4.功能规划.md §2.1 — `ProviderRegistry.ts`\n */\n\nimport type { AuthProvider } from \"@serviceme/devtools-protocol\";\n\nimport type { IAuthProvider } from \"./providers/IAuthProvider\";\n\nexport class ProviderRegistry {\n\tprivate readonly providers = new Map<AuthProvider, IAuthProvider>();\n\n\t/** Register or replace a provider implementation. */\n\tregister(provider: IAuthProvider): void {\n\t\tthis.providers.set(provider.providerId, provider);\n\t}\n\n\t/** Look up a registered provider by id; throws when missing. */\n\tget(providerId: AuthProvider): IAuthProvider {\n\t\tconst p = this.providers.get(providerId);\n\t\tif (!p) {\n\t\t\tthrow new Error(`Auth provider not registered: ${providerId}`);\n\t\t}\n\t\treturn p;\n\t}\n\n\t/** Non-throwing lookup; returns `undefined` when the provider is unknown. */\n\ttryGet(providerId: AuthProvider): IAuthProvider | undefined {\n\t\treturn this.providers.get(providerId);\n\t}\n\n\t/** Return all registered provider ids — used by `auth.status` and bridge capability hints. */\n\tlist(): AuthProvider[] {\n\t\treturn [...this.providers.keys()];\n\t}\n\n\t/** True when a provider has been registered for this id. */\n\thas(providerId: AuthProvider): boolean {\n\t\treturn this.providers.has(providerId);\n\t}\n}\n","/**\n * AuthCore — Main entry for the auth domain.\n *\n * Owns the `ProviderRegistry` + `AuthStateManager` + `KeychainAuthTokenStore`.\n * Provides a small, side-effectful surface that CLI / Extension / Bridge\n * handlers can call:\n *\n * - `login(provider)` → drives Device Flow, persists token via store.\n * - `logout(provider?)` → clears the active session (and token bytes).\n * - `status()` → snapshot for `auth.status`.\n * - `whoami()` → minimal identity for `auth.whoami`.\n * - `switchProvider(p)` → flip the active provider (multi-account).\n *\n * Core NEVER holds token bytes past the `KeychainAuthTokenStore.set()`\n * boundary — callers pass the token to the store immediately, then\n * drop the local reference. Per ADL-004 the token bytes only live in\n * (a) SecretStorage / (b) keyring / (c) HTTP Authorization header.\n *\n * Refs:\n * - 4.功能规划.md §2.1 — `AuthCore.ts 主入口`\n * - ADL-002 — Device Flow OAuth\n * - ADL-004 — Auth token storage\n */\n\nimport type {\n\tAuthAccountMeta,\n\tAuthLoginResult,\n\tAuthProvider,\n\tAuthStatus,\n\tAuthSwitchResult,\n\tAuthWhoamiResult,\n} from \"@serviceme/devtools-protocol\";\n\nimport type { AccessCheckResult, AccessControl } from \"./AccessControl\";\nimport { AuthStateManager } from \"./AuthStateManager\";\nimport type { KeychainAuthTokenStore } from \"./KeychainAuthTokenStore\";\nimport { ProviderRegistry } from \"./ProviderRegistry\";\nimport type { IAuthProvider, IAuthProviderSession } from \"./providers/IAuthProvider\";\n\nexport interface AuthCoreOptions {\n\tproviders: IAuthProvider[];\n\ttokenStore: KeychainAuthTokenStore;\n\taccessControl?: AccessControl;\n\tstateManager?: AuthStateManager;\n}\n\n/**\n * Callback invoked once the user finishes the device-flow handshake but\n * BEFORE the token is written to the keychain. Lets the caller display\n * the `userCode` + `verificationUrl` to the user (Phase 5.3 CLI prints to\n * stdout; Phase 5.5 Extension pops a webview notification).\n */\nexport type DeviceFlowUiCallback = (result: AuthLoginResult) => void | Promise<void>;\n\n/**\n * Optional cancellation handle — when `shouldContinue()` returns false,\n * the polling loop exits with a clear error so callers can render\n * \"Login cancelled\" UX.\n */\nexport type CancellationCheck = () => boolean;\n\nexport class AuthCore {\n\tprivate readonly registry: ProviderRegistry;\n\tprivate readonly state: AuthStateManager;\n\tprivate readonly tokenStore: KeychainAuthTokenStore;\n\tprivate readonly accessControl?: AccessControl;\n\n\tconstructor(opts: AuthCoreOptions) {\n\t\tthis.registry = new ProviderRegistry();\n\t\tfor (const provider of opts.providers) {\n\t\t\tthis.registry.register(provider);\n\t\t}\n\t\tthis.state = opts.stateManager ?? new AuthStateManager();\n\t\tthis.tokenStore = opts.tokenStore;\n\t\tthis.accessControl = opts.accessControl;\n\t}\n\n\t/** Snapshot of every account, the active provider, and the last error. */\n\tstatus(): AuthStatus {\n\t\treturn this.state.getStatus();\n\t}\n\n\t/** List accounts (immutable copy). */\n\tlistAccounts(): AuthAccountMeta[] {\n\t\treturn this.state.listAccounts();\n\t}\n\n\t/**\n\t * Drive the device-flow login for `provider`.\n\t *\n\t * Sequence:\n\t * 1. Ask the provider for the user code + verification URL.\n\t * 2. Surface the code to the user (via `ui`).\n\t * 3. Poll until the user authorizes (or `shouldContinue` aborts).\n\t * 4. Persist the token via `KeychainAuthTokenStore.set()`.\n\t * 5. Insert the resulting `AuthAccountMeta` into state and mark active.\n\t */\n\tasync login(\n\t\tprovider: AuthProvider,\n\t\tui: DeviceFlowUiCallback,\n\t\tshouldContinue?: CancellationCheck\n\t): Promise<AuthLoginResult> {\n\t\tconst providerImpl = this.registry.get(provider);\n\t\ttry {\n\t\t\tconst initial = await providerImpl.requestDeviceFlow();\n\t\t\tawait ui(initial);\n\t\t\tconst session = await providerImpl.completeDeviceFlow(\n\t\t\t\tinitial.userCode ? ((initial as unknown as { deviceCode?: string }).deviceCode ?? \"\") : \"\",\n\t\t\t\tshouldContinue\n\t\t\t);\n\t\t\treturn await this.persistSession(providerImpl, session);\n\t\t} catch (err) {\n\t\t\tthis.state.recordError(err instanceof Error ? err.message : String(err));\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t/**\n\t * Complete the device-flow handshake given a pre-fetched user code.\n\t * Useful when the caller (Phase 5.3 CLI) wants to fetch the code,\n\t * print the URL, and then poll on a subsequent invocation.\n\t */\n\tasync completeLogin(\n\t\tprovider: AuthProvider,\n\t\tdeviceCode: string,\n\t\tshouldContinue?: CancellationCheck\n\t): Promise<AuthLoginResult> {\n\t\tconst providerImpl = this.registry.get(provider);\n\t\ttry {\n\t\t\tconst session = await providerImpl.completeDeviceFlow(deviceCode, shouldContinue);\n\t\t\treturn await this.persistSession(providerImpl, session);\n\t\t} catch (err) {\n\t\t\tthis.state.recordError(err instanceof Error ? err.message : String(err));\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t/** Resolve the provider + token for the active session; returns null when no session is active. */\n\tasync resolveActiveToken(): Promise<{\n\t\tprovider: AuthProvider;\n\t\taccount: AuthAccountMeta;\n\t\ttoken: string;\n\t} | null> {\n\t\tconst provider = this.state.getActiveProvider();\n\t\tif (!provider) return null;\n\t\tconst account = this.state.getActiveAccount();\n\t\tif (!account) return null;\n\t\tconst envelope = await this.tokenStore.get({ provider, accountId: account.id });\n\t\tif (!envelope) return null;\n\t\treturn { provider, account, token: envelope.token };\n\t}\n\n\t/**\n\t * Logout: removes the token bytes from the keychain + drops the\n\t * account entry from state. When `provider` is omitted, clears\n\t * every account.\n\t */\n\tasync logout(\n\t\tprovider?: AuthProvider\n\t): Promise<{ provider: AuthProvider | null; success: boolean }> {\n\t\tif (!provider) {\n\t\t\t// Clear every account's token bytes + metadata.\n\t\t\tfor (const account of this.state.listAccounts()) {\n\t\t\t\tawait this.tokenStore.delete({ provider: account.provider, accountId: account.id });\n\t\t\t\tthis.state.removeAccount(account.provider, account.id);\n\t\t\t}\n\t\t\tthis.state.clearAll();\n\t\t\treturn { provider: null, success: true };\n\t\t}\n\n\t\tconst accountsForProvider = this.state.listAccounts().filter((a) => a.provider === provider);\n\t\tlet removed = false;\n\t\tfor (const account of accountsForProvider) {\n\t\t\tawait this.tokenStore.delete({ provider, accountId: account.id });\n\t\t\tconst didRemove = this.state.removeAccount(provider, account.id);\n\t\t\tremoved = removed || didRemove;\n\t\t}\n\t\tif (this.state.getActiveProvider() === provider) {\n\t\t\t// Reset active if we just removed the active provider's account.\n\t\t\tconst firstRemaining = this.state.listAccounts()[0];\n\t\t\tif (firstRemaining) {\n\t\t\t\tthis.state.setActive(firstRemaining.provider);\n\t\t\t}\n\t\t}\n\t\treturn { provider, success: removed };\n\t}\n\n\t/** Minimal identity for `auth.whoami`. */\n\tasync whoami(): Promise<AuthWhoamiResult> {\n\t\tconst provider = this.state.getActiveProvider();\n\t\tconst account = this.state.getActiveAccount();\n\t\tif (!provider || !account) {\n\t\t\treturn { provider: null };\n\t\t}\n\t\treturn {\n\t\t\tprovider,\n\t\t\tlogin: account.login,\n\t\t\tname: account.displayName,\n\t\t\tavatarUrl: account.avatarUrl,\n\t\t\temail: account.email,\n\t\t};\n\t}\n\n\t/** Switch the active provider. Returns the resulting active account. */\n\tswitchProvider(provider: AuthProvider, accountId?: string): AuthSwitchResult {\n\t\tconst ok = this.state.setActive(provider, accountId);\n\t\tconst account = this.state.getActiveAccount();\n\t\treturn {\n\t\t\tactiveProvider: ok ? provider : this.state.getActiveProvider(),\n\t\t\taccount,\n\t\t};\n\t}\n\n\t/** Run the access-control check against the active account (optional, requires AccessControl). */\n\tasync checkAccess(): Promise<AccessCheckResult | null> {\n\t\tif (!this.accessControl) return null;\n\t\tconst account = this.state.getActiveAccount();\n\t\treturn this.accessControl.checkAccess(account);\n\t}\n\n\t/** Expose the state manager for test inspection (not for mutation). */\n\tgetStateManager(): AuthStateManager {\n\t\treturn this.state;\n\t}\n\n\t/** Expose the provider registry for test inspection. */\n\tgetProviderRegistry(): ProviderRegistry {\n\t\treturn this.registry;\n\t}\n\n\t/** Expose the access control (or undefined when not configured). */\n\tgetAccessControl(): AccessControl | undefined {\n\t\treturn this.accessControl;\n\t}\n\n\tprivate async persistSession(\n\t\tproviderImpl: IAuthProvider,\n\t\tsession: IAuthProviderSession\n\t): Promise<AuthLoginResult> {\n\t\tconst expiresAt = session.expiresIn ? Date.now() + session.expiresIn * 1000 : null;\n\t\tconst meta = await providerImpl.fetchAccountMeta(session.token);\n\t\tconst accountMeta: AuthAccountMeta = {\n\t\t\tid: meta.id,\n\t\t\tprovider: meta.provider,\n\t\t\tdisplayName: meta.displayName,\n\t\t\tlogin: meta.login,\n\t\t\temail: meta.email,\n\t\t\tavatarUrl: meta.avatarUrl,\n\t\t\texpiresAt: meta.expiresAt ?? expiresAt,\n\t\t};\n\n\t\tawait this.tokenStore.set({ provider: meta.provider, accountId: meta.id }, session.token, {\n\t\t\texpiresAt,\n\t\t});\n\t\tthis.state.upsertAccount(accountMeta);\n\t\tthis.state.setActive(meta.provider, meta.id);\n\t\tthis.state.clearError();\n\t\treturn {\n\t\t\tprovider: meta.provider,\n\t\t\tmessage: `Logged in as ${meta.login ?? meta.id}`,\n\t\t};\n\t}\n}\n","/**\n * KeychainAuthTokenStore — abstract adapter for token byte persistence.\n *\n * Per ADL-004 the OAuth token bytes live in one of three places:\n * (a) VSCode `SecretStorage` (Extension process)\n * (b) `@napi-rs/keyring` Entry (CLI process)\n * (c) HTTP request `Authorization` header\n *\n * `AuthCore` MUST NOT call any of these directly. Instead it receives\n * a `KeychainAuthTokenStore` via DI; CLI provides a `@napi-rs/keyring`\n * adapter, Extension provides a `SecretStorage` adapter. This keeps\n * Core free of native-binding concerns and lets the same business\n * logic power both runtimes.\n *\n * Implementations:\n * - CLI: `apps/serviceme-cli/src/.../KeyringTokenStore.ts` (Phase 5.3)\n * - Extension: `apps/extension/src/.../SecretStorageTokenStore.ts` (Phase 5.5)\n *\n * Failure semantics — `get()` returns `null` when no token is stored\n * (cold-start), throws when the underlying keychain is unavailable\n * (rare on Linux without libsecret). Callers should surface the throw\n * as `AUTH_KEYRING_UNAVAILABLE` rather than silently falling back to\n * in-memory storage (per ADL-004 §不变量).\n *\n * Refs:\n * - ADL-004 — CLI 端 Auth Token 存储选型\n * - 4.功能规划.md §2.1 — \"Core MUST NOT directly depend on `@napi-rs/keyring`\"\n */\n\n/** Opaque account identifier (provider + upstream user id). */\nexport interface KeychainAccountKey {\n\tprovider: string;\n\taccountId: string;\n}\n\n/** Non-secret metadata returned alongside a `get()` so callers can audit. */\nexport interface KeychainTokenMetadata {\n\tprovider: string;\n\taccountId: string;\n\texpiresAt?: number | null;\n\tstoredAt?: number;\n}\n\n/**\n * Result envelope — callers receive the token bytes (for immediate use\n * in an HTTP `Authorization` header) plus optional non-secret metadata.\n *\n * IMPORTANT: the `token` field is plain `string` here, not the\n * `SecretToken` brand. The provider -> store -> HTTP-header pipeline\n * runs inside a single trust boundary; crossing that boundary requires\n * `KeychainTokenEnvelope<SecretToken>` and the `auth.tokenRead`\n * capability gate (see ADL-004 §不变量).\n */\nexport interface KeychainTokenEnvelope {\n\ttoken: string;\n\tmetadata: KeychainTokenMetadata;\n}\n\n/**\n * Abstract token-storage adapter. All methods are async because the\n * keyring / SecretStorage backends are async-by-nature.\n *\n * Thread-safety: implementations MUST be safe for concurrent calls;\n * `AuthCore` will multiplex over multiple providers on the same\n * runtime.\n */\nexport interface KeychainAuthTokenStore {\n\t/** Persist token bytes for the given provider + account pair. */\n\tset(\n\t\tkey: KeychainAccountKey,\n\t\ttoken: string,\n\t\topts?: { expiresAt?: number | null },\n\t): Promise<void>;\n\n\t/** Fetch the token bytes; returns `null` when none is stored. */\n\tget(key: KeychainAccountKey): Promise<KeychainTokenEnvelope | null>;\n\n\t/** Remove the token entry. Idempotent — removing a missing entry is not an error. */\n\tdelete(key: KeychainAccountKey): Promise<void>;\n\n\t/**\n\t * List all stored token keys (metadata only — never the token bytes).\n\t * Useful for multi-account enumeration and bridge `auth.status`.\n\t */\n\tlist(): Promise<KeychainAccountKey[]>;\n\n\t/**\n\t * Health probe — returns `false` when the keychain is unreachable\n\t * (e.g. Linux without libsecret). Callers SHOULD probe on first\n\t * use and surface `AUTH_KEYRING_UNAVAILABLE` rather than degrade.\n\t */\n\tisAvailable(): Promise<boolean>;\n}\n\n/**\n * Sentinel error thrown by `KeychainAuthTokenStore.get()` / `.set()`\n * when the underlying keychain is unavailable. Callers map this to\n * `AUTH_KEYRING_UNAVAILABLE` (Phase 5.3 error code).\n */\nexport class KeychainUnavailableError extends Error {\n\tconstructor(message: string, cause?: unknown) {\n\t\t// Use the native ES2022 `Error` two-arg form instead of redeclaring\n\t\t// `cause` as a class field — a field redeclaration shadows the\n\t\t// inherited member and requires an `override` modifier under\n\t\t// `noImplicitOverride` (downstream consumers like the Extension),\n\t\t// which in turn fails THIS package's own build if its `lib` doesn't\n\t\t// also include `cause` on `Error`. Relying on the built-in\n\t\t// constructor option avoids the mismatch entirely (this package's\n\t\t// `tsconfig.json` targets `lib: [\"ES2022\", \"DOM\"]`, so `cause` is\n\t\t// recognized here too).\n\t\tsuper(message, cause !== undefined ? { cause } : undefined);\n\t\tthis.name = \"KeychainUnavailableError\";\n\t}\n}\n\n/**\n * In-memory `KeychainAuthTokenStore` — test/dev fallback. NEVER use\n * in production: tokens live in process memory and disappear on exit.\n * Production wiring is `KeyringTokenStore` (CLI) / `SecretStorageTokenStore`\n * (Extension).\n */\nexport class InMemoryKeychainAuthTokenStore implements KeychainAuthTokenStore {\n\tprivate readonly entries = new Map<\n\t\tstring,\n\t\t{ token: string; expiresAt: number | null; storedAt: number }\n\t>();\n\n\tprivate compositeKey(key: KeychainAccountKey): string {\n\t\treturn `${key.provider}:${key.accountId}`;\n\t}\n\n\tasync set(\n\t\tkey: KeychainAccountKey,\n\t\ttoken: string,\n\t\topts?: { expiresAt?: number | null },\n\t): Promise<void> {\n\t\tthis.entries.set(this.compositeKey(key), {\n\t\t\ttoken,\n\t\t\texpiresAt: opts?.expiresAt ?? null,\n\t\t\tstoredAt: Date.now(),\n\t\t});\n\t}\n\n\tasync get(key: KeychainAccountKey): Promise<KeychainTokenEnvelope | null> {\n\t\tconst entry = this.entries.get(this.compositeKey(key));\n\t\tif (!entry) return null;\n\t\treturn {\n\t\t\ttoken: entry.token,\n\t\t\tmetadata: {\n\t\t\t\tprovider: key.provider,\n\t\t\t\taccountId: key.accountId,\n\t\t\t\texpiresAt: entry.expiresAt,\n\t\t\t\tstoredAt: entry.storedAt,\n\t\t\t},\n\t\t};\n\t}\n\n\tasync delete(key: KeychainAccountKey): Promise<void> {\n\t\tthis.entries.delete(this.compositeKey(key));\n\t}\n\n\tasync list(): Promise<KeychainAccountKey[]> {\n\t\tconst out: KeychainAccountKey[] = [];\n\t\tfor (const composite of this.entries.keys()) {\n\t\t\tconst sepIdx = composite.indexOf(\":\");\n\t\t\tif (sepIdx <= 0) continue;\n\t\t\tout.push({\n\t\t\t\tprovider: composite.slice(0, sepIdx),\n\t\t\t\taccountId: composite.slice(sepIdx + 1),\n\t\t\t});\n\t\t}\n\t\treturn out;\n\t}\n\n\tasync isAvailable(): Promise<boolean> {\n\t\treturn true;\n\t}\n}\n","/**\n * AuthCore — Pure local utilities for GitHub local email handling.\n *\n * Copied verbatim from `@serviceme/shared/src/github-user-email.ts` (15 LOC)\n * because ADL-003 forbids Core from depending on `@serviceme/shared`. These\n * helpers are pure functions with zero side effects, so the duplication is\n * trivial to keep in sync.\n *\n * Refs:\n * - 4.功能规划.md §2.1 — \"utils/githubUserEmail.ts 纯函数,直接搬\"\n * - ADL-003 — `@serviceme/shared` boundary decision\n */\n\n/**\n * Returns true when the supplied address is a synthetic GitHub \"local\" email\n * (suffixed with `@github.local`). Real GitHub OAuth clients often return\n * `null` for `email` (user kept it private) and the application substitutes\n * `<login>@github.local` to keep the field non-null.\n */\nexport function isGitHubLocalEmail(email: string | null | undefined): boolean {\n\treturn typeof email === \"string\" && email.trim().toLowerCase().endsWith(\"@github.local\");\n}\n\n/** Build a synthetic `<login>@github.local` address. */\nexport function buildGitHubLocalEmail(login: string): string {\n\treturn `${login}@github.local`;\n}\n\n/**\n * Resolve the user's primary email address — falling back to the synthetic\n * `<login>@github.local` form when the upstream email is missing or blank.\n */\nexport function resolvePrimaryEmail(login: string, email: string | null | undefined): string {\n\tconst normalizedEmail = email?.trim();\n\treturn normalizedEmail || buildGitHubLocalEmail(login);\n}\n","/**\n * GitHubAuthProvider — Device Flow OAuth implementation.\n *\n * Implements the GitHub OAuth Device Flow per the official spec\n * (https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow).\n *\n * Per ADL-002 the device flow is the locked authentication strategy for\n * SERVICEME — no localhost callback server, no PAT. This class is a\n * pure HTTP client (uses native `fetch`, no vscode dependency) that\n * returns the token bytes for the caller's `KeychainAuthTokenStore` to\n * persist immediately.\n *\n * Token bytes NEVER enter logs / errors / debug output. The provider\n * exposes only the user-visible code + verification URL during the\n * device-flow handshake.\n *\n * Refs:\n * - ADL-002 — Device Flow OAuth (locked)\n * - 4.功能规划.md §2.1 — `providers/GitHubAuthProvider.ts`\n */\n\nimport type { AuthAccountMeta, AuthLoginResult, AuthProvider } from \"@serviceme/devtools-protocol\";\nimport { buildGitHubLocalEmail } from \"../utils/githubUserEmail\";\nimport type { IAuthProvider, IAuthProviderSession } from \"./IAuthProvider\";\n\nconst DEFAULT_DEVICE_CODE_URL = \"https://github.com/login/device/code\";\nconst DEFAULT_TOKEN_URL = \"https://github.com/login/oauth/access_token\";\nconst DEFAULT_USER_URL = \"https://api.github.com/user\";\nconst DEFAULT_SCOPE = \"read:user user:email\";\n\n/** Tunable provider config — exposed for tests + forks. */\nexport interface GitHubAuthProviderConfig {\n\tclientId: string;\n\tdeviceCodeUrl?: string;\n\ttokenUrl?: string;\n\tuserUrl?: string;\n\tscope?: string;\n\t/** Polling interval (ms) floor. The device-flow response's `interval` wins when larger. */\n\tminPollIntervalMs?: number;\n\t/** Maximum poll interval after exponential back-off. */\n\tmaxPollIntervalMs?: number;\n\t/** Fetch override (for tests + DI). */\n\tfetchImpl?: typeof fetch;\n\t/** Maximum wall-clock time to wait for the user before giving up (ms). Defaults to `expires_in * 1000`. */\n\tmaxWaitMs?: number;\n}\n\ninterface DeviceCodeResponse {\n\tdevice_code: string;\n\tuser_code: string;\n\tverification_uri: string;\n\texpires_in: number;\n\tinterval: number;\n}\n\ninterface TokenPollResponse {\n\taccess_token?: string;\n\trefresh_token?: string;\n\texpires_in?: number;\n\ttoken_type?: string;\n\tscope?: string;\n\terror?: string;\n\terror_description?: string;\n\terror_uri?: string;\n}\n\ninterface GitHubUserResponse {\n\tid: number;\n\tlogin: string;\n\tname?: string | null;\n\temail?: string | null;\n\tavatar_url?: string | null;\n}\n\nexport class GitHubAuthProvider implements IAuthProvider {\n\treadonly providerId: AuthProvider = \"github\";\n\tprivate readonly cfg: Required<Omit<GitHubAuthProviderConfig, \"maxWaitMs\" | \"fetchImpl\">> & {\n\t\tmaxWaitMs?: number;\n\t\tfetchImpl: typeof fetch;\n\t};\n\n\tconstructor(config: GitHubAuthProviderConfig) {\n\t\tif (!config.clientId || config.clientId.length === 0) {\n\t\t\tthrow new Error(\"GitHubAuthProvider requires a non-empty `clientId`\");\n\t\t}\n\t\tthis.cfg = {\n\t\t\tclientId: config.clientId,\n\t\t\tdeviceCodeUrl: config.deviceCodeUrl ?? DEFAULT_DEVICE_CODE_URL,\n\t\t\ttokenUrl: config.tokenUrl ?? DEFAULT_TOKEN_URL,\n\t\t\tuserUrl: config.userUrl ?? DEFAULT_USER_URL,\n\t\t\tscope: config.scope ?? DEFAULT_SCOPE,\n\t\t\tminPollIntervalMs: config.minPollIntervalMs ?? 1000,\n\t\t\tmaxPollIntervalMs: config.maxPollIntervalMs ?? 15000,\n\t\t\tmaxWaitMs: config.maxWaitMs,\n\t\t\tfetchImpl: config.fetchImpl ?? fetch,\n\t\t};\n\t}\n\n\tasync requestDeviceFlow(opts?: { scope?: string }): Promise<AuthLoginResult> {\n\t\tconst body = JSON.stringify({\n\t\t\tclient_id: this.cfg.clientId,\n\t\t\tscope: opts?.scope ?? this.cfg.scope,\n\t\t});\n\t\tconst resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"User-Agent\": \"serviceme-core\",\n\t\t\t},\n\t\t\tbody,\n\t\t});\n\t\tif (!resp.ok) {\n\t\t\tthrow new Error(`GitHub device-code request failed: HTTP ${resp.status}`);\n\t\t}\n\t\tconst data = (await resp.json()) as DeviceCodeResponse;\n\t\tif (!data.device_code || !data.user_code || !data.verification_uri) {\n\t\t\tthrow new Error(\"GitHub device-code response missing required fields\");\n\t\t}\n\t\treturn {\n\t\t\tprovider: this.providerId,\n\t\t\tuserCode: data.user_code,\n\t\t\tverificationUrl: data.verification_uri,\n\t\t\texpiresAt: Date.now() + data.expires_in * 1000,\n\t\t\tmessage: `Open ${data.verification_uri} and enter ${data.user_code}`,\n\t\t};\n\t}\n\n\tasync completeDeviceFlow(\n\t\tdeviceCode: string,\n\t\tshouldContinue?: () => boolean\n\t): Promise<IAuthProviderSession> {\n\t\tconst start = Date.now();\n\t\tlet pollIntervalMs = this.cfg.minPollIntervalMs;\n\t\tlet consecutiveSlowDown = 0;\n\n\t\twhile (true) {\n\t\t\tif (shouldContinue && !shouldContinue()) {\n\t\t\t\tthrow new Error(\"GitHub device flow cancelled by caller\");\n\t\t\t}\n\t\t\tconst elapsed = Date.now() - start;\n\t\t\tif (this.cfg.maxWaitMs !== undefined && elapsed > this.cfg.maxWaitMs) {\n\t\t\t\tthrow new Error(\"GitHub device flow exceeded max wait time\");\n\t\t\t}\n\t\t\tawait sleep(pollIntervalMs);\n\t\t\tif (shouldContinue && !shouldContinue()) {\n\t\t\t\tthrow new Error(\"GitHub device flow cancelled by caller\");\n\t\t\t}\n\n\t\t\tconst resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\"User-Agent\": \"serviceme-core\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tclient_id: this.cfg.clientId,\n\t\t\t\t\tdevice_code: deviceCode,\n\t\t\t\t\tgrant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n\t\t\t\t}),\n\t\t\t});\n\t\t\tif (!resp.ok) {\n\t\t\t\t// Transient HTTP failure — back off and retry until the user acts.\n\t\t\t\tpollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst data = (await resp.json()) as TokenPollResponse;\n\t\t\tif (data.access_token) {\n\t\t\t\tconst rawUser = await this.fetchGitHubUser(data.access_token);\n\t\t\t\tconst email = await this.resolveEmail(data.access_token, rawUser);\n\t\t\t\treturn {\n\t\t\t\t\ttoken: data.access_token,\n\t\t\t\t\trefreshToken: data.refresh_token,\n\t\t\t\t\texpiresIn: data.expires_in,\n\t\t\t\t\tuser: {\n\t\t\t\t\t\tid: String(rawUser.id),\n\t\t\t\t\t\tlogin: rawUser.login,\n\t\t\t\t\t\tname: rawUser.name ?? undefined,\n\t\t\t\t\t\temail: email ?? null,\n\t\t\t\t\t\tavatarUrl: rawUser.avatar_url ?? undefined,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (data.error === \"authorization_pending\") {\n\t\t\t\t// User hasn't acted yet — keep polling without back-off inflation.\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (data.error === \"slow_down\") {\n\t\t\t\tconsecutiveSlowDown++;\n\t\t\t\tpollIntervalMs = Math.min(\n\t\t\t\t\tMath.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2000)),\n\t\t\t\t\tthis.cfg.maxPollIntervalMs\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (data.error === \"access_denied\") {\n\t\t\t\tthrow new Error(\"User denied authorization\");\n\t\t\t}\n\t\t\tif (data.error === \"expired_token\") {\n\t\t\t\tthrow new Error(\"GitHub device code expired — restart the flow\");\n\t\t\t}\n\t\t\tthrow new Error(`GitHub device flow error: ${data.error ?? \"unknown\"}`);\n\t\t}\n\t}\n\n\tasync refreshAccessToken(refreshToken: string): Promise<IAuthProviderSession> {\n\t\tif (!refreshToken) {\n\t\t\tthrow new Error(\"refreshAccessToken requires a non-empty refresh token\");\n\t\t}\n\t\tconst resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"User-Agent\": \"serviceme-core\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tclient_id: this.cfg.clientId,\n\t\t\t\tgrant_type: \"refresh_token\",\n\t\t\t\trefresh_token: refreshToken,\n\t\t\t}),\n\t\t});\n\t\tif (!resp.ok) {\n\t\t\tthrow new Error(`GitHub refresh failed: HTTP ${resp.status}`);\n\t\t}\n\t\tconst data = (await resp.json()) as TokenPollResponse;\n\t\tif (!data.access_token) {\n\t\t\tthrow new Error(`GitHub refresh failed: ${data.error ?? \"no_access_token\"}`);\n\t\t}\n\t\tconst rawUser = await this.fetchGitHubUser(data.access_token);\n\t\tconst email = await this.resolveEmail(data.access_token, rawUser);\n\t\treturn {\n\t\t\ttoken: data.access_token,\n\t\t\trefreshToken: data.refresh_token ?? refreshToken,\n\t\t\texpiresIn: data.expires_in,\n\t\t\tuser: {\n\t\t\t\tid: String(rawUser.id),\n\t\t\t\tlogin: rawUser.login,\n\t\t\t\tname: rawUser.name ?? undefined,\n\t\t\t\temail: email ?? null,\n\t\t\t\tavatarUrl: rawUser.avatar_url ?? undefined,\n\t\t\t},\n\t\t};\n\t}\n\n\tasync validateToken(token: string): Promise<boolean> {\n\t\ttry {\n\t\t\tconst resp = await this.cfg.fetchImpl(this.cfg.userUrl, {\n\t\t\t\theaders: {\n\t\t\t\t\tAccept: \"application/vnd.github+json\",\n\t\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t\t\"User-Agent\": \"serviceme-core\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tif (resp.status === 401) return false;\n\t\t\tif (!resp.ok) {\n\t\t\t\tthrow new Error(`GitHub /user returned HTTP ${resp.status}`);\n\t\t\t}\n\t\t\treturn true;\n\t\t} catch (err) {\n\t\t\t// Network / parse errors are surfaced; only 401 means invalid.\n\t\t\tif (err instanceof Error && err.message.includes(\"401\")) return false;\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\tasync fetchAccountMeta(token: string): Promise<AuthAccountMeta> {\n\t\tconst user = await this.fetchGitHubUser(token);\n\t\tconst email = await this.resolveEmail(token, user);\n\t\treturn {\n\t\t\tid: String(user.id),\n\t\t\tprovider: this.providerId,\n\t\t\tdisplayName: user.name ?? user.login,\n\t\t\tlogin: user.login,\n\t\t\temail: email ?? buildGitHubLocalEmail(user.login),\n\t\t\tavatarUrl: user.avatar_url ?? undefined,\n\t\t\texpiresAt: null,\n\t\t};\n\t}\n\n\tprivate async fetchGitHubUser(token: string): Promise<GitHubUserResponse> {\n\t\tconst resp = await this.cfg.fetchImpl(this.cfg.userUrl, {\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/vnd.github+json\",\n\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t\"User-Agent\": \"serviceme-core\",\n\t\t\t},\n\t\t});\n\t\tif (!resp.ok) {\n\t\t\tthrow new Error(`GitHub /user failed: HTTP ${resp.status}`);\n\t\t}\n\t\treturn (await resp.json()) as GitHubUserResponse;\n\t}\n\n\t/**\n\t * GitHub's `/user` endpoint returns `null` when the user kept their\n\t * email private. The `/user/emails` endpoint reveals verified emails;\n\t * we pick the primary one, falling back to the synthetic\n\t * `<login>@github.local` form so the account is never email-less.\n\t */\n\tprivate async resolveEmail(token: string, user: GitHubUserResponse): Promise<string | null> {\n\t\tif (user.email) return user.email;\n\t\tconst emailsResp = await this.cfg.fetchImpl(\"https://api.github.com/user/emails\", {\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/vnd.github+json\",\n\t\t\t\tAuthorization: `Bearer ${token}`,\n\t\t\t\t\"User-Agent\": \"serviceme-core\",\n\t\t\t},\n\t\t});\n\t\tif (!emailsResp.ok) {\n\t\t\treturn null;\n\t\t}\n\t\tconst emails = (await emailsResp.json()) as Array<{\n\t\t\temail: string;\n\t\t\tprimary: boolean;\n\t\t\tvisibility: string | null;\n\t\t\tverified: boolean;\n\t\t}>;\n\t\tconst primary = emails.find((e) => e.primary && e.verified);\n\t\treturn primary?.email ?? null;\n\t}\n}\n\nfunction sleep(ms: number): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * MicrosoftAuthProvider — Microsoft Account (MSA) OAuth Device Flow stub.\n *\n * The Extension's existing `MicrosoftAuthProvider` (`apps/extension/src/services/auth/providers/MicrosoftAuthProvider.ts`)\n * delegates to VSCode's built-in `vscode.authentication.getSession()` API\n * — there's no direct Microsoft device-flow endpoint exposed for\n * first-party apps. To keep Core usable from the CLI without VSCode,\n * we expose a minimal stub that throws \"not implemented in Core\" so\n * callers can detect and route back to the Extension adapter.\n *\n * The reason this lives in Core at all (instead of being purely\n * Extension-side) is the cross-runtime registry: `AuthCore` needs to\n * know which providers it MIGHT support, even if only the Extension\n * process can actually fulfil the Microsoft login.\n *\n * Refs:\n * - 4.功能规划.md §2.1 — `providers/MicrosoftAuthProvider.ts 从 extension 整体迁移`\n * - ADL-002 — Device Flow OAuth (locked, GitHub-only)\n */\n\nimport type { AuthAccountMeta, AuthLoginResult, AuthProvider } from \"@serviceme/devtools-protocol\";\n\nimport type { IAuthProvider, IAuthProviderSession } from \"./IAuthProvider\";\n\n/**\n * Error thrown when callers invoke Microsoft auth from a non-Extension\n * runtime (CLI / Bridge). The CLI maps this to `AUTH_PROVIDER_REQUIRES_HOST`\n * so the user is prompted to retry inside VSCode.\n */\nexport class MicrosoftProviderNotHostedError extends Error {\n\tconstructor(\n\t\tmessage = \"Microsoft auth requires the VSCode host; CLI does not yet implement MSA device flow\"\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MicrosoftProviderNotHostedError\";\n\t}\n}\n\nexport class MicrosoftAuthProvider implements IAuthProvider {\n\treadonly providerId: AuthProvider = \"microsoft\";\n\n\tasync requestDeviceFlow(): Promise<AuthLoginResult> {\n\t\tthrow new MicrosoftProviderNotHostedError();\n\t}\n\n\tasync completeDeviceFlow(\n\t\t_deviceCode: string,\n\t\t_shouldContinue?: () => boolean\n\t): Promise<IAuthProviderSession> {\n\t\tthrow new MicrosoftProviderNotHostedError();\n\t}\n\n\tasync refreshAccessToken(_refreshToken: string): Promise<IAuthProviderSession> {\n\t\tthrow new MicrosoftProviderNotHostedError();\n\t}\n\n\tasync validateToken(_token: string): Promise<boolean> {\n\t\tthrow new MicrosoftProviderNotHostedError();\n\t}\n\n\tasync fetchAccountMeta(_token: string): Promise<AuthAccountMeta> {\n\t\tthrow new MicrosoftProviderNotHostedError();\n\t}\n}\n"],"mappings":"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","names":[]}
@@ -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 };