@serviceme/devtools-core 2.0.1 → 2.0.3

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 (82) hide show
  1. package/dist/auth-Bzss8zOv.mjs +995 -0
  2. package/dist/auth-Bzss8zOv.mjs.map +1 -0
  3. package/dist/auth-DXam3c4S.js +1078 -0
  4. package/dist/auth-DXam3c4S.js.map +1 -0
  5. package/dist/auth.d.mts +2 -2
  6. package/dist/auth.d.ts +2 -2
  7. package/dist/auth.js +14 -1029
  8. package/dist/auth.mjs +2 -991
  9. package/dist/device-Bn2NzajD.mjs +793 -0
  10. package/dist/device-Bn2NzajD.mjs.map +1 -0
  11. package/dist/device-ihxrFR-w.js +874 -0
  12. package/dist/device-ihxrFR-w.js.map +1 -0
  13. package/dist/device.d.mts +2 -455
  14. package/dist/device.d.ts +2 -455
  15. package/dist/device.js +15 -728
  16. package/dist/device.mjs +2 -679
  17. package/dist/index-2rX86yMq.d.mts +331 -0
  18. package/dist/index-CoA6y0MP.d.mts +498 -0
  19. package/dist/index-CoA6y0MP.d.ts +498 -0
  20. package/dist/index-DGXYp8J5.d.mts +392 -0
  21. package/dist/index-DGXYp8J5.d.ts +392 -0
  22. package/dist/index-DaW7IYA-.d.mts +185 -0
  23. package/dist/index-DaW7IYA-.d.ts +185 -0
  24. package/dist/index-Dp6FwWKh.d.ts +331 -0
  25. package/dist/index.d.mts +2261 -2207
  26. package/dist/index.d.ts +2261 -2207
  27. package/dist/index.js +8095 -10919
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +7873 -10690
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/rolldown-runtime-VH7oDXx4.js +28 -0
  32. package/dist/skill-linker.d.mts +82 -81
  33. package/dist/skill-linker.d.ts +82 -81
  34. package/dist/skill-linker.js +359 -402
  35. package/dist/skill-linker.js.map +1 -1
  36. package/dist/skill-linker.mjs +345 -357
  37. package/dist/skill-linker.mjs.map +1 -1
  38. package/dist/skill-store-Bym2yinU.mjs +388 -0
  39. package/dist/skill-store-Bym2yinU.mjs.map +1 -0
  40. package/dist/skill-store-CJdYyyxu.js +408 -0
  41. package/dist/skill-store-CJdYyyxu.js.map +1 -0
  42. package/dist/skill-store.d.mts +41 -42
  43. package/dist/skill-store.d.ts +41 -42
  44. package/dist/skill-store.js +5 -338
  45. package/dist/skill-store.mjs +2 -301
  46. package/dist/submit-Cg7wSa3x.mjs +84 -0
  47. package/dist/submit-Cg7wSa3x.mjs.map +1 -0
  48. package/dist/submit-CltE6YOZ.js +98 -0
  49. package/dist/submit-CltE6YOZ.js.map +1 -0
  50. package/dist/submit.d.mts +2 -3
  51. package/dist/submit.d.ts +2 -3
  52. package/dist/submit.js +4 -166
  53. package/dist/submit.mjs +2 -130
  54. package/dist/toolbox-CbfxpNg-.mjs +540 -0
  55. package/dist/toolbox-CbfxpNg-.mjs.map +1 -0
  56. package/dist/toolbox-CxnOr-FY.js +614 -0
  57. package/dist/toolbox-CxnOr-FY.js.map +1 -0
  58. package/dist/toolbox.d.mts +2 -244
  59. package/dist/toolbox.d.ts +2 -244
  60. package/dist/toolbox.js +14 -584
  61. package/dist/toolbox.mjs +2 -536
  62. package/dist/{types-B9gk3dXH.d.ts → types-BCV3IedE.d.mts} +25 -23
  63. package/dist/{types-B9gk3dXH.d.mts → types-BCV3IedE.d.ts} +25 -23
  64. package/dist/userHome-BquDeFVE.js +526 -0
  65. package/dist/userHome-BquDeFVE.js.map +1 -0
  66. package/dist/userHome-CcnAQPG1.mjs +212 -0
  67. package/dist/userHome-CcnAQPG1.mjs.map +1 -0
  68. package/package.json +10 -10
  69. package/dist/auth.js.map +0 -1
  70. package/dist/auth.mjs.map +0 -1
  71. package/dist/device.js.map +0 -1
  72. package/dist/device.mjs.map +0 -1
  73. package/dist/index-BMk4tqIT.d.mts +0 -328
  74. package/dist/index-CymN0x9Z.d.ts +0 -328
  75. package/dist/index-PD135hlB.d.mts +0 -626
  76. package/dist/index-PD135hlB.d.ts +0 -626
  77. package/dist/skill-store.js.map +0 -1
  78. package/dist/skill-store.mjs.map +0 -1
  79. package/dist/submit.js.map +0 -1
  80. package/dist/submit.mjs.map +0 -1
  81. package/dist/toolbox.js.map +0 -1
  82. package/dist/toolbox.mjs.map +0 -1
@@ -0,0 +1,995 @@
1
+ import { EventEmitter } from "node:events";
2
+ //#region src/auth/AccessControl.ts
3
+ const DEFAULT_CACHE_TTL_MS = 6e5;
4
+ const DEFAULT_PERSISTENT_CACHE_TTL_MS = 864e5;
5
+ const KEY_GRANTED_USERS = "msDevTools.accessControl.grantedUsers";
6
+ const KEY_MEMBERSHIP_CACHE = "msDevTools.accessControl.membershipCache";
7
+ const KEY_SERVER_IN_ORG = "msDevTools.accessControl.serverInOrg";
8
+ var AccessControl = class {
9
+ constructor(kv, options) {
10
+ this.kv = kv;
11
+ this.memCache = /* @__PURE__ */ new Map();
12
+ this.serverInOrgMem = /* @__PURE__ */ new Map();
13
+ this.listeners = /* @__PURE__ */ new Set();
14
+ this.opts = {
15
+ org: options.org,
16
+ tokenFetcher: options.tokenFetcher,
17
+ orgFetcher: options.orgFetcher,
18
+ microsoftEmailDomains: options.microsoftEmailDomains ?? [],
19
+ cacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
20
+ persistentCacheTtlMs: options.persistentCacheTtlMs ?? DEFAULT_PERSISTENT_CACHE_TTL_MS,
21
+ now: options.now ?? (() => Date.now())
22
+ };
23
+ const persistedServerInOrg = this.kv.get(KEY_SERVER_IN_ORG);
24
+ if (persistedServerInOrg) for (const [key, value] of Object.entries(persistedServerInOrg)) this.serverInOrgMem.set(key, value);
25
+ }
26
+ /** Subscribe to inOrg changes (used by extension to refresh access-gated UI). */
27
+ onDidChange(listener) {
28
+ this.listeners.add(listener);
29
+ return () => this.listeners.delete(listener);
30
+ }
31
+ /** Full check: requires authentication + org membership / admin grant. */
32
+ async checkAccess(user) {
33
+ if (!user) return {
34
+ allowed: false,
35
+ reason: "not_authenticated"
36
+ };
37
+ if (user.provider === "microsoft" && user.login) {
38
+ const inOrg = this.serverInOrgMem.get(user.login.toLowerCase());
39
+ if (inOrg === true) return { allowed: true };
40
+ if (inOrg === false) return {
41
+ allowed: false,
42
+ reason: "not_member",
43
+ username: user.login
44
+ };
45
+ }
46
+ const githubAccount = user.provider === "github" ? user : null;
47
+ if (!githubAccount && user.provider === "microsoft" && user.login) {
48
+ const [, domain = ""] = user.login.split("@");
49
+ if (domain.length > 0 && this.opts.microsoftEmailDomains.includes(domain.toLowerCase())) return { allowed: true };
50
+ return {
51
+ allowed: false,
52
+ reason: "not_member",
53
+ username: user.login
54
+ };
55
+ }
56
+ if (!githubAccount) return { allowed: true };
57
+ const accessUsername = githubAccount.login ?? user.login ?? "";
58
+ if (accessUsername.length === 0) return {
59
+ allowed: false,
60
+ reason: "not_member",
61
+ username: ""
62
+ };
63
+ if (this.getGrantedUsers().includes(accessUsername.toLowerCase())) return { allowed: true };
64
+ return await this.checkOrgMembership(accessUsername) ? { allowed: true } : {
65
+ allowed: false,
66
+ reason: "not_member",
67
+ username: accessUsername
68
+ };
69
+ }
70
+ /** Admin: grant access to a GitHub username (bypasses org check). */
71
+ async grantAccess(username) {
72
+ const list = this.getGrantedUsers();
73
+ const key = username.toLowerCase();
74
+ if (!list.includes(key)) {
75
+ list.push(key);
76
+ await this.kv.update(KEY_GRANTED_USERS, list);
77
+ }
78
+ }
79
+ /** Admin: revoke a previously granted access. */
80
+ async revokeAccess(username) {
81
+ const updated = this.getGrantedUsers().filter((u) => u !== username.toLowerCase());
82
+ await this.kv.update(KEY_GRANTED_USERS, updated);
83
+ }
84
+ /** Snapshot of all admin-granted usernames (lowercased). */
85
+ getGrantedUsers() {
86
+ return this.kv.get(KEY_GRANTED_USERS) ?? [];
87
+ }
88
+ /** Cached membership lookup with both in-memory + persistent layers. */
89
+ async checkOrgMembership(username) {
90
+ const now = this.opts.now();
91
+ const inMem = this.memCache.get(username);
92
+ if (inMem && now - inMem.at < this.opts.cacheTtlMs) return inMem.isMember;
93
+ const persistentEntry = (this.kv.get(KEY_MEMBERSHIP_CACHE) ?? {})[username];
94
+ if (persistentEntry && now - persistentEntry.at < this.opts.persistentCacheTtlMs) {
95
+ this.memCache.set(username, persistentEntry);
96
+ return persistentEntry.isMember;
97
+ }
98
+ const token = await this.opts.tokenFetcher("github");
99
+ if (!token) return false;
100
+ try {
101
+ const result = await this.opts.orgFetcher(token, this.opts.org);
102
+ const decision = this.resolveDecision(username, result);
103
+ if (typeof decision === "boolean") {
104
+ this.updateCache(username, decision);
105
+ return decision;
106
+ }
107
+ return true;
108
+ } catch {
109
+ return true;
110
+ }
111
+ }
112
+ /** Server-confirmed inOrg status (set by device claim flow). */
113
+ setServerInOrg(username, inOrg) {
114
+ const key = username.toLowerCase();
115
+ this.serverInOrgMem.set(key, inOrg);
116
+ const persisted = this.kv.get(KEY_SERVER_IN_ORG) ?? {};
117
+ persisted[key] = inOrg;
118
+ this.kv.update(KEY_SERVER_IN_ORG, persisted);
119
+ this.notifyListeners();
120
+ }
121
+ /** Inspect server-confirmed inOrg for a username; returns undefined when unknown. */
122
+ getServerInOrg(username) {
123
+ return this.serverInOrgMem.get(username.toLowerCase());
124
+ }
125
+ resolveDecision(_username, membership) {
126
+ if (membership.status === "active" || membership.status === "pending") return true;
127
+ if (membership.status === "not_member") return false;
128
+ }
129
+ updateCache(username, isMember) {
130
+ const at = this.opts.now();
131
+ this.memCache.set(username, {
132
+ isMember,
133
+ at
134
+ });
135
+ const persistent = this.kv.get(KEY_MEMBERSHIP_CACHE) ?? {};
136
+ persistent[username] = {
137
+ isMember,
138
+ at
139
+ };
140
+ this.kv.update(KEY_MEMBERSHIP_CACHE, persistent);
141
+ }
142
+ notifyListeners() {
143
+ for (const listener of this.listeners) try {
144
+ listener();
145
+ } catch {}
146
+ }
147
+ };
148
+ //#endregion
149
+ //#region src/logger.ts
150
+ const noopLogger = {
151
+ debug() {},
152
+ info() {},
153
+ warn() {},
154
+ error() {}
155
+ };
156
+ function formatArgs(args) {
157
+ return args.map((arg) => {
158
+ if (typeof arg === "string") return arg;
159
+ try {
160
+ return JSON.stringify(arg);
161
+ } catch {
162
+ return String(arg);
163
+ }
164
+ }).join(" ");
165
+ }
166
+ function createConsoleLogger(prefix = "serviceme") {
167
+ return {
168
+ debug(message, ...args) {
169
+ process.stderr.write(`[${prefix}] DEBUG ${message} ${formatArgs(args)}\n`);
170
+ },
171
+ info(message, ...args) {
172
+ process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}\n`);
173
+ },
174
+ warn(message, ...args) {
175
+ process.stderr.write(`[${prefix}] WARN ${message} ${formatArgs(args)}\n`);
176
+ },
177
+ error(message, ...args) {
178
+ process.stderr.write(`[${prefix}] ERROR ${message} ${formatArgs(args)}\n`);
179
+ }
180
+ };
181
+ }
182
+ //#endregion
183
+ //#region src/auth/AuthStateManager.ts
184
+ /**
185
+ * AuthStateManager — In-memory mirror of the persisted auth state.
186
+ *
187
+ * Holds the multi-account map (provider + accountId keyed) plus the
188
+ * "active" provider/account pair. Emits change events via Node's
189
+ * built-in `events.EventEmitter` so listeners stay decoupled.
190
+ *
191
+ * Important: this class does NOT touch `vscode.SecretStorage` or any
192
+ * file — it only keeps the metadata (`AuthAccountMeta`) and the
193
+ * `activeProvider` selection. Token bytes are looked up via the
194
+ * `KeychainAuthTokenStore` on demand. This separation is what lets
195
+ * `AuthCore` run identically in CLI + Extension.
196
+ *
197
+ * Refs:
198
+ * - 4.功能规划.md §2.1 — `AuthStateManager.ts events.EventEmitter, NO VSCode dep`
199
+ * - ADL-004 — token bytes never enter Core state
200
+ */
201
+ var AuthStateManager = class {
202
+ constructor(opts = {}) {
203
+ this.emitter = new EventEmitter();
204
+ this.accounts = [];
205
+ this.activeProvider = null;
206
+ this.activeAccountId = null;
207
+ this.lastError = null;
208
+ if (opts.maxListeners !== void 0) this.emitter.setMaxListeners(opts.maxListeners);
209
+ }
210
+ /** Subscribe to state-change events. Returns a disposer. */
211
+ onDidChange(listener) {
212
+ this.emitter.on("change", listener);
213
+ return () => this.emitter.off("change", listener);
214
+ }
215
+ /** Snapshot of all known accounts (immutable copy). */
216
+ listAccounts() {
217
+ return [...this.accounts];
218
+ }
219
+ /** Find an account by `(provider, accountId)` tuple; returns `null` when absent. */
220
+ findAccount(provider, accountId) {
221
+ return this.accounts.find((a) => a.provider === provider && a.id === accountId) ?? null;
222
+ }
223
+ /** First account for the requested provider — used for "switch to GitHub" UX. */
224
+ findFirstForProvider(provider) {
225
+ return this.accounts.find((a) => a.provider === provider) ?? null;
226
+ }
227
+ /** Insert or update an account entry. New accounts land at the head of the list. */
228
+ upsertAccount(meta) {
229
+ const idx = this.accounts.findIndex((a) => a.provider === meta.provider && a.id === meta.id);
230
+ if (idx >= 0) this.accounts[idx] = meta;
231
+ else this.accounts.unshift(meta);
232
+ this.fire();
233
+ }
234
+ /** Remove an account entry. Returns `true` when an entry was removed. */
235
+ removeAccount(provider, accountId) {
236
+ const before = this.accounts.length;
237
+ this.accounts = this.accounts.filter((a) => !(a.provider === provider && a.id === accountId));
238
+ const removed = this.accounts.length !== before;
239
+ if (removed && this.activeProvider === provider && this.activeAccountId === accountId) {
240
+ if (!this.accounts.some((a) => a.provider === provider)) {
241
+ this.activeProvider = null;
242
+ this.activeAccountId = null;
243
+ } else {
244
+ const next = this.accounts.find((a) => a.provider === provider);
245
+ if (next) this.activeAccountId = next.id;
246
+ }
247
+ }
248
+ if (removed) this.fire();
249
+ return removed;
250
+ }
251
+ /** Set the active provider. When `accountId` is omitted, picks the first account for that provider. */
252
+ setActive(provider, accountId) {
253
+ const match = accountId !== void 0 ? this.accounts.find((a) => a.provider === provider && a.id === accountId) : this.accounts.find((a) => a.provider === provider);
254
+ if (!match) return false;
255
+ this.activeProvider = provider;
256
+ this.activeAccountId = match.id;
257
+ this.fire();
258
+ return true;
259
+ }
260
+ /** Currently active provider — `null` when no session is active. */
261
+ getActiveProvider() {
262
+ return this.activeProvider;
263
+ }
264
+ /** Currently active account metadata — `null` when none. */
265
+ getActiveAccount() {
266
+ if (!this.activeProvider || !this.activeAccountId) return null;
267
+ return this.accounts.find((a) => a.provider === this.activeProvider && a.id === this.activeAccountId) ?? null;
268
+ }
269
+ /** True when at least one provider has an account entry. */
270
+ hasAnySession() {
271
+ return this.accounts.length > 0 && this.activeProvider !== null;
272
+ }
273
+ /** Snapshot of the state for `auth.status` and bridge serialization. */
274
+ getStatus() {
275
+ const grouped = {};
276
+ for (const account of this.accounts) grouped[account.provider] = account;
277
+ return {
278
+ activeProvider: this.activeProvider,
279
+ accounts: grouped,
280
+ hasAnySession: this.hasAnySession(),
281
+ lastError: this.lastError ?? void 0
282
+ };
283
+ }
284
+ /** Record a non-fatal error from the last login/refresh attempt. */
285
+ recordError(message) {
286
+ this.lastError = message;
287
+ this.fire();
288
+ }
289
+ /** Clear the recorded error (e.g. after a successful login). */
290
+ clearError() {
291
+ if (this.lastError === null) return;
292
+ this.lastError = null;
293
+ this.fire();
294
+ }
295
+ /** Drop every account — used by `auth.logout` with no provider. */
296
+ clearAll() {
297
+ this.accounts = [];
298
+ this.activeProvider = null;
299
+ this.activeAccountId = null;
300
+ this.lastError = null;
301
+ this.fire();
302
+ }
303
+ fire() {
304
+ this.emitter.emit("change");
305
+ }
306
+ };
307
+ //#endregion
308
+ //#region src/auth/ProviderRegistry.ts
309
+ var ProviderRegistry = class {
310
+ constructor() {
311
+ this.providers = /* @__PURE__ */ new Map();
312
+ }
313
+ /** Register or replace a provider implementation. */
314
+ register(provider) {
315
+ this.providers.set(provider.providerId, provider);
316
+ }
317
+ /** Look up a registered provider by id; throws when missing. */
318
+ get(providerId) {
319
+ const p = this.providers.get(providerId);
320
+ if (!p) throw new Error(`Auth provider not registered: ${providerId}`);
321
+ return p;
322
+ }
323
+ /** Non-throwing lookup; returns `undefined` when the provider is unknown. */
324
+ tryGet(providerId) {
325
+ return this.providers.get(providerId);
326
+ }
327
+ /** Return all registered provider ids — used by `auth.status` and bridge capability hints. */
328
+ list() {
329
+ return [...this.providers.keys()];
330
+ }
331
+ /** True when a provider has been registered for this id. */
332
+ has(providerId) {
333
+ return this.providers.has(providerId);
334
+ }
335
+ };
336
+ //#endregion
337
+ //#region src/auth/AuthCore.ts
338
+ var AuthCore = class {
339
+ constructor(opts) {
340
+ this.registry = new ProviderRegistry();
341
+ for (const provider of opts.providers) this.registry.register(provider);
342
+ this.state = opts.stateManager ?? new AuthStateManager();
343
+ this.tokenStore = opts.tokenStore;
344
+ this.accessControl = opts.accessControl;
345
+ this.logger = opts.logger ?? noopLogger;
346
+ }
347
+ /** Snapshot of every account, the active provider, and the last error. */
348
+ status() {
349
+ return this.state.getStatus();
350
+ }
351
+ /** List accounts (immutable copy). */
352
+ listAccounts() {
353
+ return this.state.listAccounts();
354
+ }
355
+ /**
356
+ * Drive the device-flow login for `provider`.
357
+ *
358
+ * Sequence:
359
+ * 1. Ask the provider for the user code + verification URL.
360
+ * 2. Surface the code to the user (via `ui`).
361
+ * 3. Poll until the user authorizes (or `shouldContinue` aborts).
362
+ * 4. Persist the token via `KeychainAuthTokenStore.set()`.
363
+ * 5. Insert the resulting `AuthAccountMeta` into state and mark active.
364
+ */
365
+ async login(provider, ui, shouldContinue) {
366
+ const providerImpl = this.registry.get(provider);
367
+ this.logger.info("[AuthCore] Starting device-flow login", { provider });
368
+ try {
369
+ const initial = await providerImpl.requestDeviceFlow();
370
+ await ui(initial);
371
+ if (!initial.deviceCode) {
372
+ this.logger.error("[AuthCore] Provider did not return deviceCode", provider);
373
+ throw new Error("Auth provider did not return deviceCode for device flow completion");
374
+ }
375
+ const session = await providerImpl.completeDeviceFlow(initial.deviceCode, shouldContinue, initial.pollIntervalMs);
376
+ const result = await this.persistSession(providerImpl, session);
377
+ this.logger.info("[AuthCore] Device-flow login completed", { provider });
378
+ return result;
379
+ } catch (err) {
380
+ const message = err instanceof Error ? err.message : String(err);
381
+ this.logger.error("[AuthCore] Device-flow login failed", message, { provider });
382
+ this.state.recordError(message);
383
+ throw err;
384
+ }
385
+ }
386
+ /**
387
+ * Complete the device-flow handshake given a pre-fetched user code.
388
+ * Useful when the caller (Phase 5.3 CLI) wants to fetch the code,
389
+ * print the URL, and then poll on a subsequent invocation.
390
+ */
391
+ async completeLogin(provider, deviceCode, shouldContinue) {
392
+ const providerImpl = this.registry.get(provider);
393
+ try {
394
+ const session = await providerImpl.completeDeviceFlow(deviceCode, shouldContinue);
395
+ return await this.persistSession(providerImpl, session);
396
+ } catch (err) {
397
+ this.state.recordError(err instanceof Error ? err.message : String(err));
398
+ throw err;
399
+ }
400
+ }
401
+ /** Resolve the provider + token for the active session; returns null when no session is active. */
402
+ async resolveActiveToken() {
403
+ const provider = this.state.getActiveProvider();
404
+ if (!provider) return null;
405
+ const account = this.state.getActiveAccount();
406
+ if (!account) return null;
407
+ const envelope = await this.tokenStore.get({
408
+ provider,
409
+ accountId: account.id
410
+ });
411
+ if (!envelope) return null;
412
+ return {
413
+ provider,
414
+ account,
415
+ token: envelope.token
416
+ };
417
+ }
418
+ /**
419
+ * Logout: removes the token bytes from the keychain + drops the
420
+ * account entry from state. When `provider` is omitted, clears
421
+ * every account.
422
+ */
423
+ async logout(provider) {
424
+ this.logger.info("[AuthCore] Logout requested", { provider: provider ?? "all" });
425
+ if (!provider) {
426
+ for (const account of this.state.listAccounts()) {
427
+ await this.tokenStore.delete({
428
+ provider: account.provider,
429
+ accountId: account.id
430
+ });
431
+ this.state.removeAccount(account.provider, account.id);
432
+ }
433
+ this.state.clearAll();
434
+ return {
435
+ provider: null,
436
+ success: true
437
+ };
438
+ }
439
+ const accountsForProvider = this.state.listAccounts().filter((a) => a.provider === provider);
440
+ let removed = false;
441
+ for (const account of accountsForProvider) {
442
+ await this.tokenStore.delete({
443
+ provider,
444
+ accountId: account.id
445
+ });
446
+ const didRemove = this.state.removeAccount(provider, account.id);
447
+ removed = removed || didRemove;
448
+ }
449
+ if (this.state.getActiveProvider() === provider) {
450
+ const firstRemaining = this.state.listAccounts()[0];
451
+ if (firstRemaining) this.state.setActive(firstRemaining.provider);
452
+ }
453
+ return {
454
+ provider,
455
+ success: removed
456
+ };
457
+ }
458
+ /** Minimal identity for `auth.whoami`. */
459
+ async whoami() {
460
+ const provider = this.state.getActiveProvider();
461
+ const account = this.state.getActiveAccount();
462
+ if (!provider || !account) return { provider: null };
463
+ return {
464
+ provider,
465
+ login: account.login,
466
+ name: account.displayName,
467
+ avatarUrl: account.avatarUrl,
468
+ email: account.email
469
+ };
470
+ }
471
+ /** Switch the active provider. Returns the resulting active account. */
472
+ switchProvider(provider, accountId) {
473
+ const ok = this.state.setActive(provider, accountId);
474
+ const account = this.state.getActiveAccount();
475
+ return {
476
+ activeProvider: ok ? provider : this.state.getActiveProvider(),
477
+ account
478
+ };
479
+ }
480
+ /** Run the access-control check against the active account (optional, requires AccessControl). */
481
+ async checkAccess() {
482
+ if (!this.accessControl) return null;
483
+ const account = this.state.getActiveAccount();
484
+ return this.accessControl.checkAccess(account);
485
+ }
486
+ /** Expose the state manager for test inspection (not for mutation). */
487
+ getStateManager() {
488
+ return this.state;
489
+ }
490
+ /** Expose the provider registry for test inspection. */
491
+ getProviderRegistry() {
492
+ return this.registry;
493
+ }
494
+ /** Expose the access control (or undefined when not configured). */
495
+ getAccessControl() {
496
+ return this.accessControl;
497
+ }
498
+ async persistSession(providerImpl, session) {
499
+ const expiresAt = session.expiresIn ? Date.now() + session.expiresIn * 1e3 : null;
500
+ const meta = await providerImpl.fetchAccountMeta(session.token);
501
+ const accountMeta = {
502
+ id: meta.id,
503
+ provider: meta.provider,
504
+ displayName: meta.displayName,
505
+ login: meta.login,
506
+ email: meta.email,
507
+ avatarUrl: meta.avatarUrl,
508
+ expiresAt: meta.expiresAt ?? expiresAt
509
+ };
510
+ this.logger.debug("[AuthCore] Persisting session", {
511
+ provider: meta.provider,
512
+ accountId: meta.id
513
+ });
514
+ await this.tokenStore.set({
515
+ provider: meta.provider,
516
+ accountId: meta.id
517
+ }, session.token, { expiresAt });
518
+ this.state.upsertAccount(accountMeta);
519
+ this.state.setActive(meta.provider, meta.id);
520
+ this.state.clearError();
521
+ return {
522
+ provider: meta.provider,
523
+ message: `Logged in as ${meta.login ?? meta.id}`
524
+ };
525
+ }
526
+ };
527
+ //#endregion
528
+ //#region src/auth/KeychainAuthTokenStore.ts
529
+ /**
530
+ * Sentinel error thrown by `KeychainAuthTokenStore.get()` / `.set()`
531
+ * when the underlying keychain is unavailable. Callers map this to
532
+ * `AUTH_KEYRING_UNAVAILABLE` (Phase 5.3 error code).
533
+ */
534
+ var KeychainUnavailableError = class extends Error {
535
+ constructor(message, cause) {
536
+ super(message, cause !== void 0 ? { cause } : void 0);
537
+ this.name = "KeychainUnavailableError";
538
+ }
539
+ };
540
+ /**
541
+ * In-memory `KeychainAuthTokenStore` — test/dev fallback. NEVER use
542
+ * in production: tokens live in process memory and disappear on exit.
543
+ * Production wiring is `KeyringTokenStore` (CLI) / `SecretStorageTokenStore`
544
+ * (Extension).
545
+ */
546
+ var InMemoryKeychainAuthTokenStore = class {
547
+ constructor() {
548
+ this.entries = /* @__PURE__ */ new Map();
549
+ }
550
+ compositeKey(key) {
551
+ return `${key.provider}:${key.accountId}`;
552
+ }
553
+ async set(key, token, opts) {
554
+ this.entries.set(this.compositeKey(key), {
555
+ token,
556
+ expiresAt: opts?.expiresAt ?? null,
557
+ storedAt: Date.now()
558
+ });
559
+ }
560
+ async get(key) {
561
+ const entry = this.entries.get(this.compositeKey(key));
562
+ if (!entry) return null;
563
+ return {
564
+ token: entry.token,
565
+ metadata: {
566
+ provider: key.provider,
567
+ accountId: key.accountId,
568
+ expiresAt: entry.expiresAt,
569
+ storedAt: entry.storedAt
570
+ }
571
+ };
572
+ }
573
+ async delete(key) {
574
+ this.entries.delete(this.compositeKey(key));
575
+ }
576
+ async list() {
577
+ const out = [];
578
+ for (const composite of this.entries.keys()) {
579
+ const sepIdx = composite.indexOf(":");
580
+ if (sepIdx <= 0) continue;
581
+ out.push({
582
+ provider: composite.slice(0, sepIdx),
583
+ accountId: composite.slice(sepIdx + 1)
584
+ });
585
+ }
586
+ return out;
587
+ }
588
+ async isAvailable() {
589
+ return true;
590
+ }
591
+ };
592
+ //#endregion
593
+ //#region src/auth/utils/githubUserEmail.ts
594
+ /**
595
+ * AuthCore — Pure local utilities for GitHub local email handling.
596
+ *
597
+ * Copied verbatim from `@serviceme/devtools-shared/src/github-user-email.ts` (15 LOC)
598
+ * because ADL-003 forbids Core from depending on `@serviceme/devtools-shared`. These
599
+ * helpers are pure functions with zero side effects, so the duplication is
600
+ * trivial to keep in sync.
601
+ *
602
+ * Refs:
603
+ * - 4.功能规划.md §2.1 — "utils/githubUserEmail.ts 纯函数,直接搬"
604
+ * - ADL-003 — `@serviceme/devtools-shared` boundary decision
605
+ */
606
+ /**
607
+ * Returns true when the supplied address is a synthetic GitHub "local" email
608
+ * (suffixed with `@github.local`). Real GitHub OAuth clients often return
609
+ * `null` for `email` (user kept it private) and the application substitutes
610
+ * `<login>@github.local` to keep the field non-null.
611
+ */
612
+ function isGitHubLocalEmail(email) {
613
+ return typeof email === "string" && email.trim().toLowerCase().endsWith("@github.local");
614
+ }
615
+ /** Build a synthetic `<login>@github.local` address. */
616
+ function buildGitHubLocalEmail(login) {
617
+ return `${login}@github.local`;
618
+ }
619
+ /**
620
+ * Resolve the user's primary email address — falling back to the synthetic
621
+ * `<login>@github.local` form when the upstream email is missing or blank.
622
+ */
623
+ function resolvePrimaryEmail(login, email) {
624
+ return email?.trim() || buildGitHubLocalEmail(login);
625
+ }
626
+ //#endregion
627
+ //#region src/auth/providers/GitHubAuthProvider.ts
628
+ const DEFAULT_DEVICE_CODE_URL = "https://github.com/login/device/code";
629
+ const DEFAULT_TOKEN_URL = "https://github.com/login/oauth/access_token";
630
+ const DEFAULT_USER_URL = "https://api.github.com/user";
631
+ const DEFAULT_SCOPE = "read:user user:email";
632
+ /** Attempts for the one-shot device-code request before surfacing a network error. */
633
+ const DEVICE_CODE_MAX_ATTEMPTS = 3;
634
+ const DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS = 500;
635
+ /**
636
+ * `fetch` (both browser and Node's undici) rejects with a `TypeError`
637
+ * (e.g. "fetch failed") when the underlying connection never completes —
638
+ * DNS failure, TLS handshake reset, ECONNRESET, etc. These are transient
639
+ * network hiccups, distinct from a well-formed non-2xx HTTP response, and
640
+ * should be retried rather than immediately failing the whole login.
641
+ */
642
+ function isTransientNetworkError(error) {
643
+ return error instanceof TypeError;
644
+ }
645
+ var GitHubAuthProvider = class {
646
+ constructor(config) {
647
+ this.providerId = "github";
648
+ if (!config.clientId || config.clientId.length === 0) throw new Error("GitHubAuthProvider requires a non-empty `clientId`");
649
+ this.cfg = {
650
+ clientId: config.clientId,
651
+ deviceCodeUrl: config.deviceCodeUrl ?? DEFAULT_DEVICE_CODE_URL,
652
+ tokenUrl: config.tokenUrl ?? DEFAULT_TOKEN_URL,
653
+ userUrl: config.userUrl ?? DEFAULT_USER_URL,
654
+ scope: config.scope ?? DEFAULT_SCOPE,
655
+ minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
656
+ maxPollIntervalMs: config.maxPollIntervalMs ?? 6e4,
657
+ maxWaitMs: config.maxWaitMs,
658
+ fetchImpl: config.fetchImpl ?? fetch,
659
+ deviceCodeRetryBaseDelayMs: config.deviceCodeRetryBaseDelayMs ?? DEFAULT_DEVICE_CODE_RETRY_BASE_DELAY_MS
660
+ };
661
+ this.logger = config.logger ?? noopLogger;
662
+ this.sleepImpl = config.sleepImpl ?? sleep;
663
+ }
664
+ async requestDeviceFlow(opts) {
665
+ const body = JSON.stringify({
666
+ client_id: this.cfg.clientId,
667
+ scope: opts?.scope ?? this.cfg.scope
668
+ });
669
+ let lastNetworkError;
670
+ for (let attempt = 1; attempt <= DEVICE_CODE_MAX_ATTEMPTS; attempt++) {
671
+ this.logger.debug("[GitHubAuthProvider] Requesting device code", {
672
+ attempt,
673
+ maxAttempts: DEVICE_CODE_MAX_ATTEMPTS
674
+ });
675
+ let resp;
676
+ try {
677
+ resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
678
+ method: "POST",
679
+ headers: {
680
+ Accept: "application/json",
681
+ "Content-Type": "application/json",
682
+ "User-Agent": "serviceme-core"
683
+ },
684
+ body
685
+ });
686
+ } catch (error) {
687
+ if (!isTransientNetworkError(error) || attempt === DEVICE_CODE_MAX_ATTEMPTS) {
688
+ this.logger.error("[GitHubAuthProvider] Device-code request failed (non-retryable)", error instanceof Error ? error.message : String(error), { attempt });
689
+ throw error;
690
+ }
691
+ const delayMs = this.cfg.deviceCodeRetryBaseDelayMs * 2 ** (attempt - 1);
692
+ this.logger.warn("[GitHubAuthProvider] Transient network error requesting device code, retrying", {
693
+ attempt,
694
+ delayMs,
695
+ error: error instanceof Error ? error.message : String(error)
696
+ });
697
+ lastNetworkError = error;
698
+ await this.sleepImpl(delayMs);
699
+ continue;
700
+ }
701
+ if (!resp.ok) {
702
+ this.logger.error("[GitHubAuthProvider] Device-code request rejected by GitHub", `HTTP ${resp.status}`);
703
+ throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
704
+ }
705
+ const data = await resp.json();
706
+ if (!data.device_code || !data.user_code || !data.verification_uri) {
707
+ this.logger.error("[GitHubAuthProvider] Device-code response missing required fields", JSON.stringify(Object.keys(data)));
708
+ throw new Error("GitHub device-code response missing required fields");
709
+ }
710
+ this.logger.info("[GitHubAuthProvider] Device code obtained", {
711
+ userCode: data.user_code,
712
+ verificationUri: data.verification_uri,
713
+ expiresInSec: data.expires_in,
714
+ pollIntervalSec: data.interval
715
+ });
716
+ return {
717
+ provider: this.providerId,
718
+ deviceCode: data.device_code,
719
+ userCode: data.user_code,
720
+ verificationUrl: data.verification_uri,
721
+ expiresAt: Date.now() + data.expires_in * 1e3,
722
+ pollIntervalMs: data.interval * 1e3,
723
+ message: `Open ${data.verification_uri} and enter ${data.user_code}`
724
+ };
725
+ }
726
+ throw lastNetworkError ?? /* @__PURE__ */ new Error("GitHub device-code request failed");
727
+ }
728
+ async completeDeviceFlow(deviceCode, shouldContinue, initialPollIntervalMs) {
729
+ const start = Date.now();
730
+ let pollIntervalMs = Math.max(this.cfg.minPollIntervalMs, initialPollIntervalMs ?? 0);
731
+ let consecutiveSlowDown = 0;
732
+ let pollCount = 0;
733
+ this.logger.info("[GitHubAuthProvider] Starting device-flow polling", {
734
+ initialPollIntervalMs: pollIntervalMs,
735
+ maxWaitMs: this.cfg.maxWaitMs
736
+ });
737
+ while (true) {
738
+ if (shouldContinue && !shouldContinue()) {
739
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
740
+ elapsedMs: Date.now() - start,
741
+ pollCount
742
+ });
743
+ throw new Error("GitHub device flow cancelled by caller");
744
+ }
745
+ const elapsed = Date.now() - start;
746
+ if (this.cfg.maxWaitMs !== void 0 && elapsed > this.cfg.maxWaitMs) {
747
+ this.logger.warn("[GitHubAuthProvider] Device flow exceeded max wait time", {
748
+ elapsedMs: elapsed,
749
+ maxWaitMs: this.cfg.maxWaitMs,
750
+ pollCount
751
+ });
752
+ throw new Error("GitHub device flow exceeded max wait time");
753
+ }
754
+ await this.sleepImpl(pollIntervalMs);
755
+ if (shouldContinue && !shouldContinue()) {
756
+ this.logger.info("[GitHubAuthProvider] Device flow cancelled by caller", {
757
+ elapsedMs: Date.now() - start,
758
+ pollCount
759
+ });
760
+ throw new Error("GitHub device flow cancelled by caller");
761
+ }
762
+ pollCount++;
763
+ this.logger.debug("[GitHubAuthProvider] Polling for authorization", {
764
+ pollCount,
765
+ elapsedMs: Date.now() - start,
766
+ pollIntervalMs
767
+ });
768
+ let resp;
769
+ try {
770
+ resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
771
+ method: "POST",
772
+ headers: {
773
+ Accept: "application/json",
774
+ "Content-Type": "application/json",
775
+ "User-Agent": "serviceme-core"
776
+ },
777
+ body: JSON.stringify({
778
+ client_id: this.cfg.clientId,
779
+ device_code: deviceCode,
780
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
781
+ })
782
+ });
783
+ } catch (error) {
784
+ if (!isTransientNetworkError(error)) {
785
+ this.logger.error("[GitHubAuthProvider] Non-retryable error while polling token endpoint", error instanceof Error ? error.message : String(error), { pollCount });
786
+ throw error;
787
+ }
788
+ const nextPollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
789
+ this.logger.warn("[GitHubAuthProvider] Transient network error while polling, backing off", {
790
+ pollCount,
791
+ error: error instanceof Error ? error.message : String(error),
792
+ previousPollIntervalMs: pollIntervalMs,
793
+ nextPollIntervalMs
794
+ });
795
+ pollIntervalMs = nextPollIntervalMs;
796
+ continue;
797
+ }
798
+ if (!resp.ok) {
799
+ const nextPollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
800
+ this.logger.warn("[GitHubAuthProvider] Token endpoint returned non-OK status, backing off", {
801
+ pollCount,
802
+ status: resp.status,
803
+ previousPollIntervalMs: pollIntervalMs,
804
+ nextPollIntervalMs
805
+ });
806
+ pollIntervalMs = nextPollIntervalMs;
807
+ continue;
808
+ }
809
+ const data = await resp.json();
810
+ if (data.access_token) {
811
+ this.logger.info("[GitHubAuthProvider] Authorization granted, fetching user profile", {
812
+ pollCount,
813
+ elapsedMs: Date.now() - start
814
+ });
815
+ const rawUser = await this.fetchGitHubUser(data.access_token);
816
+ const email = await this.resolveEmail(data.access_token, rawUser);
817
+ this.logger.info("[GitHubAuthProvider] Device-flow login completed", {
818
+ pollCount,
819
+ elapsedMs: Date.now() - start,
820
+ login: rawUser.login
821
+ });
822
+ return {
823
+ token: data.access_token,
824
+ refreshToken: data.refresh_token,
825
+ expiresIn: data.expires_in,
826
+ user: {
827
+ id: String(rawUser.id),
828
+ login: rawUser.login,
829
+ name: rawUser.name ?? void 0,
830
+ email: email ?? null,
831
+ avatarUrl: rawUser.avatar_url ?? void 0
832
+ }
833
+ };
834
+ }
835
+ if (data.error === "authorization_pending") {
836
+ this.logger.debug("[GitHubAuthProvider] Authorization still pending", {
837
+ pollCount,
838
+ elapsedMs: Date.now() - start
839
+ });
840
+ continue;
841
+ }
842
+ if (data.error === "slow_down") {
843
+ consecutiveSlowDown++;
844
+ const nextPollIntervalMs = Math.min(pollIntervalMs + 5e3, this.cfg.maxPollIntervalMs);
845
+ this.logger.warn("[GitHubAuthProvider] GitHub requested slower polling (slow_down)", {
846
+ pollCount,
847
+ consecutiveSlowDown,
848
+ previousPollIntervalMs: pollIntervalMs,
849
+ nextPollIntervalMs
850
+ });
851
+ pollIntervalMs = nextPollIntervalMs;
852
+ continue;
853
+ }
854
+ if (data.error === "access_denied") {
855
+ this.logger.warn("[GitHubAuthProvider] User denied authorization", { pollCount });
856
+ throw new Error("User denied authorization");
857
+ }
858
+ if (data.error === "expired_token") {
859
+ this.logger.warn("[GitHubAuthProvider] Device code expired before authorization completed", {
860
+ pollCount,
861
+ elapsedMs: Date.now() - start
862
+ });
863
+ throw new Error("GitHub device code expired — restart the flow");
864
+ }
865
+ this.logger.error("[GitHubAuthProvider] Unexpected device-flow error from token endpoint", data.error ?? "unknown", { pollCount });
866
+ throw new Error(`GitHub device flow error: ${data.error ?? "unknown"}`);
867
+ }
868
+ }
869
+ async refreshAccessToken(refreshToken) {
870
+ if (!refreshToken) throw new Error("refreshAccessToken requires a non-empty refresh token");
871
+ const resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
872
+ method: "POST",
873
+ headers: {
874
+ Accept: "application/json",
875
+ "Content-Type": "application/json",
876
+ "User-Agent": "serviceme-core"
877
+ },
878
+ body: JSON.stringify({
879
+ client_id: this.cfg.clientId,
880
+ grant_type: "refresh_token",
881
+ refresh_token: refreshToken
882
+ })
883
+ });
884
+ if (!resp.ok) throw new Error(`GitHub refresh failed: HTTP ${resp.status}`);
885
+ const data = await resp.json();
886
+ if (!data.access_token) throw new Error(`GitHub refresh failed: ${data.error ?? "no_access_token"}`);
887
+ const rawUser = await this.fetchGitHubUser(data.access_token);
888
+ const email = await this.resolveEmail(data.access_token, rawUser);
889
+ return {
890
+ token: data.access_token,
891
+ refreshToken: data.refresh_token ?? refreshToken,
892
+ expiresIn: data.expires_in,
893
+ user: {
894
+ id: String(rawUser.id),
895
+ login: rawUser.login,
896
+ name: rawUser.name ?? void 0,
897
+ email: email ?? null,
898
+ avatarUrl: rawUser.avatar_url ?? void 0
899
+ }
900
+ };
901
+ }
902
+ async validateToken(token) {
903
+ try {
904
+ const resp = await this.cfg.fetchImpl(this.cfg.userUrl, { headers: {
905
+ Accept: "application/vnd.github+json",
906
+ Authorization: `Bearer ${token}`,
907
+ "User-Agent": "serviceme-core"
908
+ } });
909
+ if (resp.status === 401) return false;
910
+ if (!resp.ok) throw new Error(`GitHub /user returned HTTP ${resp.status}`);
911
+ return true;
912
+ } catch (err) {
913
+ if (err instanceof Error && err.message.includes("401")) return false;
914
+ throw err;
915
+ }
916
+ }
917
+ async fetchAccountMeta(token) {
918
+ const user = await this.fetchGitHubUser(token);
919
+ const email = await this.resolveEmail(token, user);
920
+ return {
921
+ id: String(user.id),
922
+ provider: this.providerId,
923
+ displayName: user.name ?? user.login,
924
+ login: user.login,
925
+ email: email ?? buildGitHubLocalEmail(user.login),
926
+ avatarUrl: user.avatar_url ?? void 0,
927
+ expiresAt: null
928
+ };
929
+ }
930
+ async fetchGitHubUser(token) {
931
+ const resp = await this.cfg.fetchImpl(this.cfg.userUrl, { headers: {
932
+ Accept: "application/vnd.github+json",
933
+ Authorization: `Bearer ${token}`,
934
+ "User-Agent": "serviceme-core"
935
+ } });
936
+ if (!resp.ok) throw new Error(`GitHub /user failed: HTTP ${resp.status}`);
937
+ return await resp.json();
938
+ }
939
+ /**
940
+ * GitHub's `/user` endpoint returns `null` when the user kept their
941
+ * email private. The `/user/emails` endpoint reveals verified emails;
942
+ * we pick the primary one, falling back to the synthetic
943
+ * `<login>@github.local` form so the account is never email-less.
944
+ */
945
+ async resolveEmail(token, user) {
946
+ if (user.email) return user.email;
947
+ const emailsResp = await this.cfg.fetchImpl("https://api.github.com/user/emails", { headers: {
948
+ Accept: "application/vnd.github+json",
949
+ Authorization: `Bearer ${token}`,
950
+ "User-Agent": "serviceme-core"
951
+ } });
952
+ if (!emailsResp.ok) return null;
953
+ return (await emailsResp.json()).find((e) => e.primary && e.verified)?.email ?? null;
954
+ }
955
+ };
956
+ function sleep(ms) {
957
+ return new Promise((resolve) => setTimeout(resolve, ms));
958
+ }
959
+ //#endregion
960
+ //#region src/auth/providers/MicrosoftAuthProvider.ts
961
+ /**
962
+ * Error thrown when callers invoke Microsoft auth from a non-Extension
963
+ * runtime (CLI / Bridge). The CLI maps this to `AUTH_PROVIDER_REQUIRES_HOST`
964
+ * so the user is prompted to retry inside VSCode.
965
+ */
966
+ var MicrosoftProviderNotHostedError = class extends Error {
967
+ constructor(message = "Microsoft auth requires the VSCode host; CLI does not yet implement MSA device flow") {
968
+ super(message);
969
+ this.name = "MicrosoftProviderNotHostedError";
970
+ }
971
+ };
972
+ var MicrosoftAuthProvider = class {
973
+ constructor() {
974
+ this.providerId = "microsoft";
975
+ }
976
+ async requestDeviceFlow() {
977
+ throw new MicrosoftProviderNotHostedError();
978
+ }
979
+ async completeDeviceFlow(_deviceCode, _shouldContinue) {
980
+ throw new MicrosoftProviderNotHostedError();
981
+ }
982
+ async refreshAccessToken(_refreshToken) {
983
+ throw new MicrosoftProviderNotHostedError();
984
+ }
985
+ async validateToken(_token) {
986
+ throw new MicrosoftProviderNotHostedError();
987
+ }
988
+ async fetchAccountMeta(_token) {
989
+ throw new MicrosoftProviderNotHostedError();
990
+ }
991
+ };
992
+ //#endregion
993
+ export { isGitHubLocalEmail as a, KeychainUnavailableError as c, AuthStateManager as d, createConsoleLogger as f, buildGitHubLocalEmail as i, AuthCore as l, AccessControl as m, MicrosoftProviderNotHostedError as n, resolvePrimaryEmail as o, noopLogger as p, GitHubAuthProvider as r, InMemoryKeychainAuthTokenStore as s, MicrosoftAuthProvider as t, ProviderRegistry as u };
994
+
995
+ //# sourceMappingURL=auth-Bzss8zOv.mjs.map