@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
package/dist/auth.js ADDED
@@ -0,0 +1,842 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/auth/index.ts
21
+ var auth_exports = {};
22
+ __export(auth_exports, {
23
+ AccessControl: () => AccessControl,
24
+ AuthCore: () => AuthCore,
25
+ AuthStateManager: () => AuthStateManager,
26
+ GitHubAuthProvider: () => GitHubAuthProvider,
27
+ InMemoryKeychainAuthTokenStore: () => InMemoryKeychainAuthTokenStore,
28
+ KeychainUnavailableError: () => KeychainUnavailableError,
29
+ MicrosoftAuthProvider: () => MicrosoftAuthProvider,
30
+ MicrosoftProviderNotHostedError: () => MicrosoftProviderNotHostedError,
31
+ ProviderRegistry: () => ProviderRegistry,
32
+ buildGitHubLocalEmail: () => buildGitHubLocalEmail,
33
+ isGitHubLocalEmail: () => isGitHubLocalEmail,
34
+ resolvePrimaryEmail: () => resolvePrimaryEmail
35
+ });
36
+ module.exports = __toCommonJS(auth_exports);
37
+
38
+ // src/auth/AccessControl.ts
39
+ var DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
40
+ var DEFAULT_PERSISTENT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
41
+ var KEY_GRANTED_USERS = "msDevTools.accessControl.grantedUsers";
42
+ var KEY_MEMBERSHIP_CACHE = "msDevTools.accessControl.membershipCache";
43
+ var KEY_SERVER_IN_ORG = "msDevTools.accessControl.serverInOrg";
44
+ var AccessControl = class {
45
+ constructor(kv, options) {
46
+ this.kv = kv;
47
+ this.memCache = /* @__PURE__ */ new Map();
48
+ this.serverInOrgMem = /* @__PURE__ */ new Map();
49
+ this.listeners = /* @__PURE__ */ new Set();
50
+ this.opts = {
51
+ org: options.org,
52
+ tokenFetcher: options.tokenFetcher,
53
+ orgFetcher: options.orgFetcher,
54
+ microsoftEmailDomains: options.microsoftEmailDomains ?? [],
55
+ cacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
56
+ persistentCacheTtlMs: options.persistentCacheTtlMs ?? DEFAULT_PERSISTENT_CACHE_TTL_MS,
57
+ now: options.now ?? (() => Date.now())
58
+ };
59
+ const persistedServerInOrg = this.kv.get(KEY_SERVER_IN_ORG);
60
+ if (persistedServerInOrg) {
61
+ for (const [key, value] of Object.entries(persistedServerInOrg)) {
62
+ this.serverInOrgMem.set(key, value);
63
+ }
64
+ }
65
+ }
66
+ /** Subscribe to inOrg changes (used by extension to refresh access-gated UI). */
67
+ onDidChange(listener) {
68
+ this.listeners.add(listener);
69
+ return () => this.listeners.delete(listener);
70
+ }
71
+ /** Full check: requires authentication + org membership / admin grant. */
72
+ async checkAccess(user) {
73
+ if (!user) {
74
+ return { allowed: false, reason: "not_authenticated" };
75
+ }
76
+ if (user.provider === "microsoft" && user.login) {
77
+ const inOrg = this.serverInOrgMem.get(user.login.toLowerCase());
78
+ if (inOrg === true) return { allowed: true };
79
+ if (inOrg === false) {
80
+ return { allowed: false, reason: "not_member", username: user.login };
81
+ }
82
+ }
83
+ const githubAccount = user.provider === "github" ? user : null;
84
+ if (!githubAccount && user.provider === "microsoft" && user.login) {
85
+ const [, domain = ""] = user.login.split("@");
86
+ const isDomainMember = domain.length > 0 && this.opts.microsoftEmailDomains.includes(domain.toLowerCase());
87
+ if (isDomainMember) return { allowed: true };
88
+ return { allowed: false, reason: "not_member", username: user.login };
89
+ }
90
+ if (!githubAccount) {
91
+ return { allowed: true };
92
+ }
93
+ const accessUsername = githubAccount.login ?? user.login ?? "";
94
+ if (accessUsername.length === 0) {
95
+ return { allowed: false, reason: "not_member", username: "" };
96
+ }
97
+ const granted = this.getGrantedUsers();
98
+ if (granted.includes(accessUsername.toLowerCase())) {
99
+ return { allowed: true };
100
+ }
101
+ const isMember = await this.checkOrgMembership(accessUsername);
102
+ return isMember ? { allowed: true } : { allowed: false, reason: "not_member", username: accessUsername };
103
+ }
104
+ /** Admin: grant access to a GitHub username (bypasses org check). */
105
+ async grantAccess(username) {
106
+ const list = this.getGrantedUsers();
107
+ const key = username.toLowerCase();
108
+ if (!list.includes(key)) {
109
+ list.push(key);
110
+ await this.kv.update(KEY_GRANTED_USERS, list);
111
+ }
112
+ }
113
+ /** Admin: revoke a previously granted access. */
114
+ async revokeAccess(username) {
115
+ const updated = this.getGrantedUsers().filter((u) => u !== username.toLowerCase());
116
+ await this.kv.update(KEY_GRANTED_USERS, updated);
117
+ }
118
+ /** Snapshot of all admin-granted usernames (lowercased). */
119
+ getGrantedUsers() {
120
+ return this.kv.get(KEY_GRANTED_USERS) ?? [];
121
+ }
122
+ /** Cached membership lookup with both in-memory + persistent layers. */
123
+ async checkOrgMembership(username) {
124
+ const now = this.opts.now();
125
+ const inMem = this.memCache.get(username);
126
+ if (inMem && now - inMem.at < this.opts.cacheTtlMs) {
127
+ return inMem.isMember;
128
+ }
129
+ const persistent = this.kv.get(KEY_MEMBERSHIP_CACHE) ?? {};
130
+ const persistentEntry = persistent[username];
131
+ if (persistentEntry && now - persistentEntry.at < this.opts.persistentCacheTtlMs) {
132
+ this.memCache.set(username, persistentEntry);
133
+ return persistentEntry.isMember;
134
+ }
135
+ const token = await this.opts.tokenFetcher("github");
136
+ if (!token) return false;
137
+ try {
138
+ const result = await this.opts.orgFetcher(token, this.opts.org);
139
+ const decision = this.resolveDecision(username, result);
140
+ if (typeof decision === "boolean") {
141
+ this.updateCache(username, decision);
142
+ return decision;
143
+ }
144
+ return true;
145
+ } catch {
146
+ return true;
147
+ }
148
+ }
149
+ /** Server-confirmed inOrg status (set by device claim flow). */
150
+ setServerInOrg(username, inOrg) {
151
+ const key = username.toLowerCase();
152
+ this.serverInOrgMem.set(key, inOrg);
153
+ const persisted = this.kv.get(KEY_SERVER_IN_ORG) ?? {};
154
+ persisted[key] = inOrg;
155
+ void this.kv.update(KEY_SERVER_IN_ORG, persisted);
156
+ this.notifyListeners();
157
+ }
158
+ /** Inspect server-confirmed inOrg for a username; returns undefined when unknown. */
159
+ getServerInOrg(username) {
160
+ return this.serverInOrgMem.get(username.toLowerCase());
161
+ }
162
+ resolveDecision(_username, membership) {
163
+ if (membership.status === "active" || membership.status === "pending") return true;
164
+ if (membership.status === "not_member") return false;
165
+ return void 0;
166
+ }
167
+ updateCache(username, isMember) {
168
+ const at = this.opts.now();
169
+ this.memCache.set(username, { isMember, at });
170
+ const persistent = this.kv.get(KEY_MEMBERSHIP_CACHE) ?? {};
171
+ persistent[username] = { isMember, at };
172
+ void this.kv.update(KEY_MEMBERSHIP_CACHE, persistent);
173
+ }
174
+ notifyListeners() {
175
+ for (const listener of this.listeners) {
176
+ try {
177
+ listener();
178
+ } catch {
179
+ }
180
+ }
181
+ }
182
+ };
183
+
184
+ // src/auth/AuthStateManager.ts
185
+ var import_node_events = require("events");
186
+ var AuthStateManager = class {
187
+ constructor(opts = {}) {
188
+ this.emitter = new import_node_events.EventEmitter();
189
+ this.accounts = [];
190
+ this.activeProvider = null;
191
+ this.activeAccountId = null;
192
+ this.lastError = null;
193
+ if (opts.maxListeners !== void 0) {
194
+ this.emitter.setMaxListeners(opts.maxListeners);
195
+ }
196
+ }
197
+ /** Subscribe to state-change events. Returns a disposer. */
198
+ onDidChange(listener) {
199
+ this.emitter.on("change", listener);
200
+ return () => this.emitter.off("change", listener);
201
+ }
202
+ /** Snapshot of all known accounts (immutable copy). */
203
+ listAccounts() {
204
+ return [...this.accounts];
205
+ }
206
+ /** Find an account by `(provider, accountId)` tuple; returns `null` when absent. */
207
+ findAccount(provider, accountId) {
208
+ return this.accounts.find((a) => a.provider === provider && a.id === accountId) ?? null;
209
+ }
210
+ /** First account for the requested provider — used for "switch to GitHub" UX. */
211
+ findFirstForProvider(provider) {
212
+ return this.accounts.find((a) => a.provider === provider) ?? null;
213
+ }
214
+ /** Insert or update an account entry. New accounts land at the head of the list. */
215
+ upsertAccount(meta) {
216
+ const idx = this.accounts.findIndex((a) => a.provider === meta.provider && a.id === meta.id);
217
+ if (idx >= 0) {
218
+ this.accounts[idx] = meta;
219
+ } else {
220
+ this.accounts.unshift(meta);
221
+ }
222
+ this.fire();
223
+ }
224
+ /** Remove an account entry. Returns `true` when an entry was removed. */
225
+ removeAccount(provider, accountId) {
226
+ const before = this.accounts.length;
227
+ this.accounts = this.accounts.filter((a) => !(a.provider === provider && a.id === accountId));
228
+ const removed = this.accounts.length !== before;
229
+ if (removed && this.activeProvider === provider && this.activeAccountId === accountId) {
230
+ const stillHasSameProvider = this.accounts.some((a) => a.provider === provider);
231
+ if (!stillHasSameProvider) {
232
+ this.activeProvider = null;
233
+ this.activeAccountId = null;
234
+ } else {
235
+ const next = this.accounts.find((a) => a.provider === provider);
236
+ if (next) {
237
+ this.activeAccountId = next.id;
238
+ }
239
+ }
240
+ }
241
+ if (removed) this.fire();
242
+ return removed;
243
+ }
244
+ /** Set the active provider. When `accountId` is omitted, picks the first account for that provider. */
245
+ setActive(provider, accountId) {
246
+ const match = accountId !== void 0 ? this.accounts.find((a) => a.provider === provider && a.id === accountId) : this.accounts.find((a) => a.provider === provider);
247
+ if (!match) return false;
248
+ this.activeProvider = provider;
249
+ this.activeAccountId = match.id;
250
+ this.fire();
251
+ return true;
252
+ }
253
+ /** Currently active provider — `null` when no session is active. */
254
+ getActiveProvider() {
255
+ return this.activeProvider;
256
+ }
257
+ /** Currently active account metadata — `null` when none. */
258
+ getActiveAccount() {
259
+ if (!this.activeProvider || !this.activeAccountId) return null;
260
+ return this.accounts.find(
261
+ (a) => a.provider === this.activeProvider && a.id === this.activeAccountId
262
+ ) ?? null;
263
+ }
264
+ /** True when at least one provider has an account entry. */
265
+ hasAnySession() {
266
+ return this.accounts.length > 0 && this.activeProvider !== null;
267
+ }
268
+ /** Snapshot of the state for `auth.status` and bridge serialization. */
269
+ getStatus() {
270
+ const grouped = {};
271
+ for (const account of this.accounts) {
272
+ grouped[account.provider] = account;
273
+ }
274
+ return {
275
+ activeProvider: this.activeProvider,
276
+ accounts: grouped,
277
+ hasAnySession: this.hasAnySession(),
278
+ lastError: this.lastError ?? void 0
279
+ };
280
+ }
281
+ /** Record a non-fatal error from the last login/refresh attempt. */
282
+ recordError(message) {
283
+ this.lastError = message;
284
+ this.fire();
285
+ }
286
+ /** Clear the recorded error (e.g. after a successful login). */
287
+ clearError() {
288
+ if (this.lastError === null) return;
289
+ this.lastError = null;
290
+ this.fire();
291
+ }
292
+ /** Drop every account — used by `auth.logout` with no provider. */
293
+ clearAll() {
294
+ this.accounts = [];
295
+ this.activeProvider = null;
296
+ this.activeAccountId = null;
297
+ this.lastError = null;
298
+ this.fire();
299
+ }
300
+ fire() {
301
+ this.emitter.emit("change");
302
+ }
303
+ };
304
+
305
+ // src/auth/ProviderRegistry.ts
306
+ var ProviderRegistry = class {
307
+ constructor() {
308
+ this.providers = /* @__PURE__ */ new Map();
309
+ }
310
+ /** Register or replace a provider implementation. */
311
+ register(provider) {
312
+ this.providers.set(provider.providerId, provider);
313
+ }
314
+ /** Look up a registered provider by id; throws when missing. */
315
+ get(providerId) {
316
+ const p = this.providers.get(providerId);
317
+ if (!p) {
318
+ throw new Error(`Auth provider not registered: ${providerId}`);
319
+ }
320
+ return p;
321
+ }
322
+ /** Non-throwing lookup; returns `undefined` when the provider is unknown. */
323
+ tryGet(providerId) {
324
+ return this.providers.get(providerId);
325
+ }
326
+ /** Return all registered provider ids — used by `auth.status` and bridge capability hints. */
327
+ list() {
328
+ return [...this.providers.keys()];
329
+ }
330
+ /** True when a provider has been registered for this id. */
331
+ has(providerId) {
332
+ return this.providers.has(providerId);
333
+ }
334
+ };
335
+
336
+ // src/auth/AuthCore.ts
337
+ var AuthCore = class {
338
+ constructor(opts) {
339
+ this.registry = new ProviderRegistry();
340
+ for (const provider of opts.providers) {
341
+ this.registry.register(provider);
342
+ }
343
+ this.state = opts.stateManager ?? new AuthStateManager();
344
+ this.tokenStore = opts.tokenStore;
345
+ this.accessControl = opts.accessControl;
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
+ try {
368
+ const initial = await providerImpl.requestDeviceFlow();
369
+ await ui(initial);
370
+ const session = await providerImpl.completeDeviceFlow(
371
+ initial.userCode ? initial.deviceCode ?? "" : "",
372
+ shouldContinue
373
+ );
374
+ return await this.persistSession(providerImpl, session);
375
+ } catch (err) {
376
+ this.state.recordError(err instanceof Error ? err.message : String(err));
377
+ throw err;
378
+ }
379
+ }
380
+ /**
381
+ * Complete the device-flow handshake given a pre-fetched user code.
382
+ * Useful when the caller (Phase 5.3 CLI) wants to fetch the code,
383
+ * print the URL, and then poll on a subsequent invocation.
384
+ */
385
+ async completeLogin(provider, deviceCode, shouldContinue) {
386
+ const providerImpl = this.registry.get(provider);
387
+ try {
388
+ const session = await providerImpl.completeDeviceFlow(deviceCode, shouldContinue);
389
+ return await this.persistSession(providerImpl, session);
390
+ } catch (err) {
391
+ this.state.recordError(err instanceof Error ? err.message : String(err));
392
+ throw err;
393
+ }
394
+ }
395
+ /** Resolve the provider + token for the active session; returns null when no session is active. */
396
+ async resolveActiveToken() {
397
+ const provider = this.state.getActiveProvider();
398
+ if (!provider) return null;
399
+ const account = this.state.getActiveAccount();
400
+ if (!account) return null;
401
+ const envelope = await this.tokenStore.get({ provider, accountId: account.id });
402
+ if (!envelope) return null;
403
+ return { provider, account, token: envelope.token };
404
+ }
405
+ /**
406
+ * Logout: removes the token bytes from the keychain + drops the
407
+ * account entry from state. When `provider` is omitted, clears
408
+ * every account.
409
+ */
410
+ async logout(provider) {
411
+ if (!provider) {
412
+ for (const account of this.state.listAccounts()) {
413
+ await this.tokenStore.delete({ provider: account.provider, accountId: account.id });
414
+ this.state.removeAccount(account.provider, account.id);
415
+ }
416
+ this.state.clearAll();
417
+ return { provider: null, success: true };
418
+ }
419
+ const accountsForProvider = this.state.listAccounts().filter((a) => a.provider === provider);
420
+ let removed = false;
421
+ for (const account of accountsForProvider) {
422
+ await this.tokenStore.delete({ provider, accountId: account.id });
423
+ const didRemove = this.state.removeAccount(provider, account.id);
424
+ removed = removed || didRemove;
425
+ }
426
+ if (this.state.getActiveProvider() === provider) {
427
+ const firstRemaining = this.state.listAccounts()[0];
428
+ if (firstRemaining) {
429
+ this.state.setActive(firstRemaining.provider);
430
+ }
431
+ }
432
+ return { provider, success: removed };
433
+ }
434
+ /** Minimal identity for `auth.whoami`. */
435
+ async whoami() {
436
+ const provider = this.state.getActiveProvider();
437
+ const account = this.state.getActiveAccount();
438
+ if (!provider || !account) {
439
+ return { provider: null };
440
+ }
441
+ return {
442
+ provider,
443
+ login: account.login,
444
+ name: account.displayName,
445
+ avatarUrl: account.avatarUrl,
446
+ email: account.email
447
+ };
448
+ }
449
+ /** Switch the active provider. Returns the resulting active account. */
450
+ switchProvider(provider, accountId) {
451
+ const ok = this.state.setActive(provider, accountId);
452
+ const account = this.state.getActiveAccount();
453
+ return {
454
+ activeProvider: ok ? provider : this.state.getActiveProvider(),
455
+ account
456
+ };
457
+ }
458
+ /** Run the access-control check against the active account (optional, requires AccessControl). */
459
+ async checkAccess() {
460
+ if (!this.accessControl) return null;
461
+ const account = this.state.getActiveAccount();
462
+ return this.accessControl.checkAccess(account);
463
+ }
464
+ /** Expose the state manager for test inspection (not for mutation). */
465
+ getStateManager() {
466
+ return this.state;
467
+ }
468
+ /** Expose the provider registry for test inspection. */
469
+ getProviderRegistry() {
470
+ return this.registry;
471
+ }
472
+ /** Expose the access control (or undefined when not configured). */
473
+ getAccessControl() {
474
+ return this.accessControl;
475
+ }
476
+ async persistSession(providerImpl, session) {
477
+ const expiresAt = session.expiresIn ? Date.now() + session.expiresIn * 1e3 : null;
478
+ const meta = await providerImpl.fetchAccountMeta(session.token);
479
+ const accountMeta = {
480
+ id: meta.id,
481
+ provider: meta.provider,
482
+ displayName: meta.displayName,
483
+ login: meta.login,
484
+ email: meta.email,
485
+ avatarUrl: meta.avatarUrl,
486
+ expiresAt: meta.expiresAt ?? expiresAt
487
+ };
488
+ await this.tokenStore.set({ provider: meta.provider, accountId: meta.id }, session.token, {
489
+ expiresAt
490
+ });
491
+ this.state.upsertAccount(accountMeta);
492
+ this.state.setActive(meta.provider, meta.id);
493
+ this.state.clearError();
494
+ return {
495
+ provider: meta.provider,
496
+ message: `Logged in as ${meta.login ?? meta.id}`
497
+ };
498
+ }
499
+ };
500
+
501
+ // src/auth/KeychainAuthTokenStore.ts
502
+ var KeychainUnavailableError = class extends Error {
503
+ constructor(message, cause) {
504
+ super(message, cause !== void 0 ? { cause } : void 0);
505
+ this.name = "KeychainUnavailableError";
506
+ }
507
+ };
508
+ var InMemoryKeychainAuthTokenStore = class {
509
+ constructor() {
510
+ this.entries = /* @__PURE__ */ new Map();
511
+ }
512
+ compositeKey(key) {
513
+ return `${key.provider}:${key.accountId}`;
514
+ }
515
+ async set(key, token, opts) {
516
+ this.entries.set(this.compositeKey(key), {
517
+ token,
518
+ expiresAt: opts?.expiresAt ?? null,
519
+ storedAt: Date.now()
520
+ });
521
+ }
522
+ async get(key) {
523
+ const entry = this.entries.get(this.compositeKey(key));
524
+ if (!entry) return null;
525
+ return {
526
+ token: entry.token,
527
+ metadata: {
528
+ provider: key.provider,
529
+ accountId: key.accountId,
530
+ expiresAt: entry.expiresAt,
531
+ storedAt: entry.storedAt
532
+ }
533
+ };
534
+ }
535
+ async delete(key) {
536
+ this.entries.delete(this.compositeKey(key));
537
+ }
538
+ async list() {
539
+ const out = [];
540
+ for (const composite of this.entries.keys()) {
541
+ const sepIdx = composite.indexOf(":");
542
+ if (sepIdx <= 0) continue;
543
+ out.push({
544
+ provider: composite.slice(0, sepIdx),
545
+ accountId: composite.slice(sepIdx + 1)
546
+ });
547
+ }
548
+ return out;
549
+ }
550
+ async isAvailable() {
551
+ return true;
552
+ }
553
+ };
554
+
555
+ // src/auth/utils/githubUserEmail.ts
556
+ function isGitHubLocalEmail(email) {
557
+ return typeof email === "string" && email.trim().toLowerCase().endsWith("@github.local");
558
+ }
559
+ function buildGitHubLocalEmail(login) {
560
+ return `${login}@github.local`;
561
+ }
562
+ function resolvePrimaryEmail(login, email) {
563
+ const normalizedEmail = email?.trim();
564
+ return normalizedEmail || buildGitHubLocalEmail(login);
565
+ }
566
+
567
+ // src/auth/providers/GitHubAuthProvider.ts
568
+ var DEFAULT_DEVICE_CODE_URL = "https://github.com/login/device/code";
569
+ var DEFAULT_TOKEN_URL = "https://github.com/login/oauth/access_token";
570
+ var DEFAULT_USER_URL = "https://api.github.com/user";
571
+ var DEFAULT_SCOPE = "read:user user:email";
572
+ var GitHubAuthProvider = class {
573
+ constructor(config) {
574
+ this.providerId = "github";
575
+ if (!config.clientId || config.clientId.length === 0) {
576
+ throw new Error("GitHubAuthProvider requires a non-empty `clientId`");
577
+ }
578
+ this.cfg = {
579
+ clientId: config.clientId,
580
+ deviceCodeUrl: config.deviceCodeUrl ?? DEFAULT_DEVICE_CODE_URL,
581
+ tokenUrl: config.tokenUrl ?? DEFAULT_TOKEN_URL,
582
+ userUrl: config.userUrl ?? DEFAULT_USER_URL,
583
+ scope: config.scope ?? DEFAULT_SCOPE,
584
+ minPollIntervalMs: config.minPollIntervalMs ?? 1e3,
585
+ maxPollIntervalMs: config.maxPollIntervalMs ?? 15e3,
586
+ maxWaitMs: config.maxWaitMs,
587
+ fetchImpl: config.fetchImpl ?? fetch
588
+ };
589
+ }
590
+ async requestDeviceFlow(opts) {
591
+ const body = JSON.stringify({
592
+ client_id: this.cfg.clientId,
593
+ scope: opts?.scope ?? this.cfg.scope
594
+ });
595
+ const resp = await this.cfg.fetchImpl(this.cfg.deviceCodeUrl, {
596
+ method: "POST",
597
+ headers: {
598
+ Accept: "application/json",
599
+ "Content-Type": "application/json",
600
+ "User-Agent": "serviceme-core"
601
+ },
602
+ body
603
+ });
604
+ if (!resp.ok) {
605
+ throw new Error(`GitHub device-code request failed: HTTP ${resp.status}`);
606
+ }
607
+ const data = await resp.json();
608
+ if (!data.device_code || !data.user_code || !data.verification_uri) {
609
+ throw new Error("GitHub device-code response missing required fields");
610
+ }
611
+ return {
612
+ provider: this.providerId,
613
+ userCode: data.user_code,
614
+ verificationUrl: data.verification_uri,
615
+ expiresAt: Date.now() + data.expires_in * 1e3,
616
+ message: `Open ${data.verification_uri} and enter ${data.user_code}`
617
+ };
618
+ }
619
+ async completeDeviceFlow(deviceCode, shouldContinue) {
620
+ const start = Date.now();
621
+ let pollIntervalMs = this.cfg.minPollIntervalMs;
622
+ let consecutiveSlowDown = 0;
623
+ while (true) {
624
+ if (shouldContinue && !shouldContinue()) {
625
+ throw new Error("GitHub device flow cancelled by caller");
626
+ }
627
+ const elapsed = Date.now() - start;
628
+ if (this.cfg.maxWaitMs !== void 0 && elapsed > this.cfg.maxWaitMs) {
629
+ throw new Error("GitHub device flow exceeded max wait time");
630
+ }
631
+ await sleep(pollIntervalMs);
632
+ if (shouldContinue && !shouldContinue()) {
633
+ throw new Error("GitHub device flow cancelled by caller");
634
+ }
635
+ const resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
636
+ method: "POST",
637
+ headers: {
638
+ Accept: "application/json",
639
+ "Content-Type": "application/json",
640
+ "User-Agent": "serviceme-core"
641
+ },
642
+ body: JSON.stringify({
643
+ client_id: this.cfg.clientId,
644
+ device_code: deviceCode,
645
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
646
+ })
647
+ });
648
+ if (!resp.ok) {
649
+ pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), this.cfg.maxPollIntervalMs);
650
+ continue;
651
+ }
652
+ const data = await resp.json();
653
+ if (data.access_token) {
654
+ const rawUser = await this.fetchGitHubUser(data.access_token);
655
+ const email = await this.resolveEmail(data.access_token, rawUser);
656
+ return {
657
+ token: data.access_token,
658
+ refreshToken: data.refresh_token,
659
+ expiresIn: data.expires_in,
660
+ user: {
661
+ id: String(rawUser.id),
662
+ login: rawUser.login,
663
+ name: rawUser.name ?? void 0,
664
+ email: email ?? null,
665
+ avatarUrl: rawUser.avatar_url ?? void 0
666
+ }
667
+ };
668
+ }
669
+ if (data.error === "authorization_pending") {
670
+ continue;
671
+ }
672
+ if (data.error === "slow_down") {
673
+ consecutiveSlowDown++;
674
+ pollIntervalMs = Math.min(
675
+ Math.round(pollIntervalMs * 1.5 + Math.min(consecutiveSlowDown * 500, 2e3)),
676
+ this.cfg.maxPollIntervalMs
677
+ );
678
+ continue;
679
+ }
680
+ if (data.error === "access_denied") {
681
+ throw new Error("User denied authorization");
682
+ }
683
+ if (data.error === "expired_token") {
684
+ throw new Error("GitHub device code expired \u2014 restart the flow");
685
+ }
686
+ throw new Error(`GitHub device flow error: ${data.error ?? "unknown"}`);
687
+ }
688
+ }
689
+ async refreshAccessToken(refreshToken) {
690
+ if (!refreshToken) {
691
+ throw new Error("refreshAccessToken requires a non-empty refresh token");
692
+ }
693
+ const resp = await this.cfg.fetchImpl(this.cfg.tokenUrl, {
694
+ method: "POST",
695
+ headers: {
696
+ Accept: "application/json",
697
+ "Content-Type": "application/json",
698
+ "User-Agent": "serviceme-core"
699
+ },
700
+ body: JSON.stringify({
701
+ client_id: this.cfg.clientId,
702
+ grant_type: "refresh_token",
703
+ refresh_token: refreshToken
704
+ })
705
+ });
706
+ if (!resp.ok) {
707
+ throw new Error(`GitHub refresh failed: HTTP ${resp.status}`);
708
+ }
709
+ const data = await resp.json();
710
+ if (!data.access_token) {
711
+ throw new Error(`GitHub refresh failed: ${data.error ?? "no_access_token"}`);
712
+ }
713
+ const rawUser = await this.fetchGitHubUser(data.access_token);
714
+ const email = await this.resolveEmail(data.access_token, rawUser);
715
+ return {
716
+ token: data.access_token,
717
+ refreshToken: data.refresh_token ?? refreshToken,
718
+ expiresIn: data.expires_in,
719
+ user: {
720
+ id: String(rawUser.id),
721
+ login: rawUser.login,
722
+ name: rawUser.name ?? void 0,
723
+ email: email ?? null,
724
+ avatarUrl: rawUser.avatar_url ?? void 0
725
+ }
726
+ };
727
+ }
728
+ async validateToken(token) {
729
+ try {
730
+ const resp = await this.cfg.fetchImpl(this.cfg.userUrl, {
731
+ headers: {
732
+ Accept: "application/vnd.github+json",
733
+ Authorization: `Bearer ${token}`,
734
+ "User-Agent": "serviceme-core"
735
+ }
736
+ });
737
+ if (resp.status === 401) return false;
738
+ if (!resp.ok) {
739
+ throw new Error(`GitHub /user returned HTTP ${resp.status}`);
740
+ }
741
+ return true;
742
+ } catch (err) {
743
+ if (err instanceof Error && err.message.includes("401")) return false;
744
+ throw err;
745
+ }
746
+ }
747
+ async fetchAccountMeta(token) {
748
+ const user = await this.fetchGitHubUser(token);
749
+ const email = await this.resolveEmail(token, user);
750
+ return {
751
+ id: String(user.id),
752
+ provider: this.providerId,
753
+ displayName: user.name ?? user.login,
754
+ login: user.login,
755
+ email: email ?? buildGitHubLocalEmail(user.login),
756
+ avatarUrl: user.avatar_url ?? void 0,
757
+ expiresAt: null
758
+ };
759
+ }
760
+ async fetchGitHubUser(token) {
761
+ const resp = await this.cfg.fetchImpl(this.cfg.userUrl, {
762
+ headers: {
763
+ Accept: "application/vnd.github+json",
764
+ Authorization: `Bearer ${token}`,
765
+ "User-Agent": "serviceme-core"
766
+ }
767
+ });
768
+ if (!resp.ok) {
769
+ throw new Error(`GitHub /user failed: HTTP ${resp.status}`);
770
+ }
771
+ return await resp.json();
772
+ }
773
+ /**
774
+ * GitHub's `/user` endpoint returns `null` when the user kept their
775
+ * email private. The `/user/emails` endpoint reveals verified emails;
776
+ * we pick the primary one, falling back to the synthetic
777
+ * `<login>@github.local` form so the account is never email-less.
778
+ */
779
+ async resolveEmail(token, user) {
780
+ if (user.email) return user.email;
781
+ const emailsResp = await this.cfg.fetchImpl("https://api.github.com/user/emails", {
782
+ headers: {
783
+ Accept: "application/vnd.github+json",
784
+ Authorization: `Bearer ${token}`,
785
+ "User-Agent": "serviceme-core"
786
+ }
787
+ });
788
+ if (!emailsResp.ok) {
789
+ return null;
790
+ }
791
+ const emails = await emailsResp.json();
792
+ const primary = emails.find((e) => e.primary && e.verified);
793
+ return primary?.email ?? null;
794
+ }
795
+ };
796
+ function sleep(ms) {
797
+ return new Promise((resolve) => setTimeout(resolve, ms));
798
+ }
799
+
800
+ // src/auth/providers/MicrosoftAuthProvider.ts
801
+ var MicrosoftProviderNotHostedError = class extends Error {
802
+ constructor(message = "Microsoft auth requires the VSCode host; CLI does not yet implement MSA device flow") {
803
+ super(message);
804
+ this.name = "MicrosoftProviderNotHostedError";
805
+ }
806
+ };
807
+ var MicrosoftAuthProvider = class {
808
+ constructor() {
809
+ this.providerId = "microsoft";
810
+ }
811
+ async requestDeviceFlow() {
812
+ throw new MicrosoftProviderNotHostedError();
813
+ }
814
+ async completeDeviceFlow(_deviceCode, _shouldContinue) {
815
+ throw new MicrosoftProviderNotHostedError();
816
+ }
817
+ async refreshAccessToken(_refreshToken) {
818
+ throw new MicrosoftProviderNotHostedError();
819
+ }
820
+ async validateToken(_token) {
821
+ throw new MicrosoftProviderNotHostedError();
822
+ }
823
+ async fetchAccountMeta(_token) {
824
+ throw new MicrosoftProviderNotHostedError();
825
+ }
826
+ };
827
+ // Annotate the CommonJS export names for ESM import in node:
828
+ 0 && (module.exports = {
829
+ AccessControl,
830
+ AuthCore,
831
+ AuthStateManager,
832
+ GitHubAuthProvider,
833
+ InMemoryKeychainAuthTokenStore,
834
+ KeychainUnavailableError,
835
+ MicrosoftAuthProvider,
836
+ MicrosoftProviderNotHostedError,
837
+ ProviderRegistry,
838
+ buildGitHubLocalEmail,
839
+ isGitHubLocalEmail,
840
+ resolvePrimaryEmail
841
+ });
842
+ //# sourceMappingURL=auth.js.map