@uapkg/cli 1.1.10 → 1.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 (165) hide show
  1. package/dist/app/CompositionRoot.d.ts +6 -0
  2. package/dist/app/CompositionRoot.d.ts.map +1 -1
  3. package/dist/app/CompositionRoot.js +16 -0
  4. package/dist/app/CompositionRoot.js.map +1 -1
  5. package/dist/app/UAPKGApplication.d.ts.map +1 -1
  6. package/dist/app/UAPKGApplication.js +62 -2
  7. package/dist/app/UAPKGApplication.js.map +1 -1
  8. package/dist/build/BuildMetadata.d.ts +12 -0
  9. package/dist/build/BuildMetadata.d.ts.map +1 -0
  10. package/dist/build/BuildMetadata.js +6 -0
  11. package/dist/cli/UAPKGCommandLine.d.ts +98 -3
  12. package/dist/cli/UAPKGCommandLine.d.ts.map +1 -1
  13. package/dist/cli/UAPKGCommandLine.js +69 -0
  14. package/dist/cli/UAPKGCommandLine.js.map +1 -1
  15. package/dist/cli/parsing/UAPKGCommandLineParser.d.ts.map +1 -1
  16. package/dist/cli/parsing/UAPKGCommandLineParser.js +8 -1
  17. package/dist/cli/parsing/UAPKGCommandLineParser.js.map +1 -1
  18. package/dist/cli/parsing/UAPKGParserRegistry.d.ts.map +1 -1
  19. package/dist/cli/parsing/UAPKGParserRegistry.js +12 -0
  20. package/dist/cli/parsing/UAPKGParserRegistry.js.map +1 -1
  21. package/dist/cli/parsing/modules/LifecycleCommandModule.d.ts +11 -0
  22. package/dist/cli/parsing/modules/LifecycleCommandModule.d.ts.map +1 -0
  23. package/dist/cli/parsing/modules/LifecycleCommandModule.js +56 -0
  24. package/dist/cli/parsing/modules/LifecycleCommandModule.js.map +1 -0
  25. package/dist/cli/parsing/modules/LoginCommandModule.d.ts +7 -0
  26. package/dist/cli/parsing/modules/LoginCommandModule.d.ts.map +1 -0
  27. package/dist/cli/parsing/modules/LoginCommandModule.js +34 -0
  28. package/dist/cli/parsing/modules/LoginCommandModule.js.map +1 -0
  29. package/dist/cli/parsing/modules/LogoutCommandModule.d.ts +7 -0
  30. package/dist/cli/parsing/modules/LogoutCommandModule.d.ts.map +1 -0
  31. package/dist/cli/parsing/modules/LogoutCommandModule.js +29 -0
  32. package/dist/cli/parsing/modules/LogoutCommandModule.js.map +1 -0
  33. package/dist/cli/parsing/modules/PublishCommandModule.d.ts +7 -0
  34. package/dist/cli/parsing/modules/PublishCommandModule.d.ts.map +1 -0
  35. package/dist/cli/parsing/modules/PublishCommandModule.js +50 -0
  36. package/dist/cli/parsing/modules/PublishCommandModule.js.map +1 -0
  37. package/dist/cli/parsing/modules/RegistryCommandModule.d.ts.map +1 -1
  38. package/dist/cli/parsing/modules/RegistryCommandModule.js +31 -2
  39. package/dist/cli/parsing/modules/RegistryCommandModule.js.map +1 -1
  40. package/dist/cli/parsing/modules/RequestsCommandModule.d.ts +7 -0
  41. package/dist/cli/parsing/modules/RequestsCommandModule.d.ts.map +1 -0
  42. package/dist/cli/parsing/modules/RequestsCommandModule.js +63 -0
  43. package/dist/cli/parsing/modules/RequestsCommandModule.js.map +1 -0
  44. package/dist/cli/parsing/modules/WhoamiCommandModule.d.ts +7 -0
  45. package/dist/cli/parsing/modules/WhoamiCommandModule.d.ts.map +1 -0
  46. package/dist/cli/parsing/modules/WhoamiCommandModule.js +29 -0
  47. package/dist/cli/parsing/modules/WhoamiCommandModule.js.map +1 -0
  48. package/dist/cli-bootstrap.d.ts +2 -0
  49. package/dist/cli-bootstrap.d.ts.map +1 -0
  50. package/dist/cli-bootstrap.js +17 -0
  51. package/dist/cli-bootstrap.js.map +1 -0
  52. package/dist/cli.js +24 -17
  53. package/dist/commands/InstallCommand.d.ts.map +1 -1
  54. package/dist/commands/InstallCommand.js +21 -6
  55. package/dist/commands/InstallCommand.js.map +1 -1
  56. package/dist/commands/LoginCommand.d.ts +17 -0
  57. package/dist/commands/LoginCommand.d.ts.map +1 -0
  58. package/dist/commands/LoginCommand.js +73 -0
  59. package/dist/commands/LoginCommand.js.map +1 -0
  60. package/dist/commands/LogoutCommand.d.ts +15 -0
  61. package/dist/commands/LogoutCommand.d.ts.map +1 -0
  62. package/dist/commands/LogoutCommand.js +38 -0
  63. package/dist/commands/LogoutCommand.js.map +1 -0
  64. package/dist/commands/PackageLifecycleCommand.d.ts +28 -0
  65. package/dist/commands/PackageLifecycleCommand.d.ts.map +1 -0
  66. package/dist/commands/PackageLifecycleCommand.js +134 -0
  67. package/dist/commands/PackageLifecycleCommand.js.map +1 -0
  68. package/dist/commands/PostinstallCandidateBuilder.d.ts +1 -1
  69. package/dist/commands/PostinstallCandidateBuilder.d.ts.map +1 -1
  70. package/dist/commands/PostinstallCandidateBuilder.js +4 -1
  71. package/dist/commands/PostinstallCandidateBuilder.js.map +1 -1
  72. package/dist/commands/PublishCommand.d.ts +32 -0
  73. package/dist/commands/PublishCommand.d.ts.map +1 -0
  74. package/dist/commands/PublishCommand.js +260 -0
  75. package/dist/commands/PublishCommand.js.map +1 -0
  76. package/dist/commands/RegistryCommand.d.ts +13 -3
  77. package/dist/commands/RegistryCommand.d.ts.map +1 -1
  78. package/dist/commands/RegistryCommand.js +115 -11
  79. package/dist/commands/RegistryCommand.js.map +1 -1
  80. package/dist/commands/RequestsCommand.d.ts +21 -0
  81. package/dist/commands/RequestsCommand.d.ts.map +1 -0
  82. package/dist/commands/RequestsCommand.js +98 -0
  83. package/dist/commands/RequestsCommand.js.map +1 -0
  84. package/dist/commands/WhoamiCommand.d.ts +32 -0
  85. package/dist/commands/WhoamiCommand.d.ts.map +1 -0
  86. package/dist/commands/WhoamiCommand.js +75 -0
  87. package/dist/commands/WhoamiCommand.js.map +1 -0
  88. package/dist/commands/add/DependencyAddResolver.d.ts.map +1 -1
  89. package/dist/commands/add/DependencyAddResolver.js +4 -2
  90. package/dist/commands/add/DependencyAddResolver.js.map +1 -1
  91. package/dist/control-plane/AccountManager.d.ts +110 -0
  92. package/dist/control-plane/AccountManager.d.ts.map +1 -0
  93. package/dist/control-plane/AccountManager.js +1014 -0
  94. package/dist/control-plane/AccountManager.js.map +1 -0
  95. package/dist/control-plane/ArtifactObserver.d.ts +24 -0
  96. package/dist/control-plane/ArtifactObserver.d.ts.map +1 -0
  97. package/dist/control-plane/ArtifactObserver.js +63 -0
  98. package/dist/control-plane/ArtifactObserver.js.map +1 -0
  99. package/dist/control-plane/AuthMetadataStore.d.ts +15 -0
  100. package/dist/control-plane/AuthMetadataStore.d.ts.map +1 -0
  101. package/dist/control-plane/AuthMetadataStore.js +116 -0
  102. package/dist/control-plane/AuthMetadataStore.js.map +1 -0
  103. package/dist/control-plane/AuthStoragePaths.d.ts +11 -0
  104. package/dist/control-plane/AuthStoragePaths.d.ts.map +1 -0
  105. package/dist/control-plane/AuthStoragePaths.js +17 -0
  106. package/dist/control-plane/AuthStoragePaths.js.map +1 -0
  107. package/dist/control-plane/AuthenticationSelector.d.ts +21 -0
  108. package/dist/control-plane/AuthenticationSelector.d.ts.map +1 -0
  109. package/dist/control-plane/AuthenticationSelector.js +64 -0
  110. package/dist/control-plane/AuthenticationSelector.js.map +1 -0
  111. package/dist/control-plane/ControlPlaneClient.d.ts +32 -0
  112. package/dist/control-plane/ControlPlaneClient.d.ts.map +1 -0
  113. package/dist/control-plane/ControlPlaneClient.js +280 -0
  114. package/dist/control-plane/ControlPlaneClient.js.map +1 -0
  115. package/dist/control-plane/ControlPlaneEndpoints.d.ts +13 -0
  116. package/dist/control-plane/ControlPlaneEndpoints.d.ts.map +1 -0
  117. package/dist/control-plane/ControlPlaneEndpoints.js +19 -0
  118. package/dist/control-plane/ControlPlaneEndpoints.js.map +1 -0
  119. package/dist/control-plane/ControlPlaneTypes.d.ts +168 -0
  120. package/dist/control-plane/ControlPlaneTypes.d.ts.map +1 -0
  121. package/dist/control-plane/ControlPlaneTypes.js +93 -0
  122. package/dist/control-plane/ControlPlaneTypes.js.map +1 -0
  123. package/dist/control-plane/CredentialStore.d.ts +28 -0
  124. package/dist/control-plane/CredentialStore.d.ts.map +1 -0
  125. package/dist/control-plane/CredentialStore.js +87 -0
  126. package/dist/control-plane/CredentialStore.js.map +1 -0
  127. package/dist/control-plane/DPoPKeyStore.d.ts +11 -0
  128. package/dist/control-plane/DPoPKeyStore.d.ts.map +1 -0
  129. package/dist/control-plane/DPoPKeyStore.js +54 -0
  130. package/dist/control-plane/DPoPKeyStore.js.map +1 -0
  131. package/dist/control-plane/GitHubActionsOidcCredentialProvider.d.ts +6 -0
  132. package/dist/control-plane/GitHubActionsOidcCredentialProvider.d.ts.map +1 -0
  133. package/dist/control-plane/GitHubActionsOidcCredentialProvider.js +67 -0
  134. package/dist/control-plane/GitHubActionsOidcCredentialProvider.js.map +1 -0
  135. package/dist/control-plane/PublishIdempotencyStore.d.ts +18 -0
  136. package/dist/control-plane/PublishIdempotencyStore.d.ts.map +1 -0
  137. package/dist/control-plane/PublishIdempotencyStore.js +71 -0
  138. package/dist/control-plane/PublishIdempotencyStore.js.map +1 -0
  139. package/dist/control-plane/RegistryGrantLock.d.ts +35 -0
  140. package/dist/control-plane/RegistryGrantLock.d.ts.map +1 -0
  141. package/dist/control-plane/RegistryGrantLock.js +199 -0
  142. package/dist/control-plane/RegistryGrantLock.js.map +1 -0
  143. package/dist/control-plane/RegistryTrustResolver.d.ts +17 -0
  144. package/dist/control-plane/RegistryTrustResolver.d.ts.map +1 -0
  145. package/dist/control-plane/RegistryTrustResolver.js +140 -0
  146. package/dist/control-plane/RegistryTrustResolver.js.map +1 -0
  147. package/dist/index.d.ts +16 -3
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +16 -2
  150. package/dist/index.js.map +1 -1
  151. package/dist/postinstall/markers/MarkerIntegrityValidator.d.ts +1 -1
  152. package/dist/prompts/InkPromptService.d.ts +1 -0
  153. package/dist/prompts/InkPromptService.d.ts.map +1 -1
  154. package/dist/prompts/InkPromptService.js +20 -0
  155. package/dist/prompts/InkPromptService.js.map +1 -1
  156. package/dist/prompts/PromptService.d.ts +2 -0
  157. package/dist/prompts/PromptService.d.ts.map +1 -1
  158. package/dist/reporting/JsonReporter.d.ts +2 -0
  159. package/dist/reporting/JsonReporter.d.ts.map +1 -1
  160. package/dist/reporting/JsonReporter.js +6 -0
  161. package/dist/reporting/JsonReporter.js.map +1 -1
  162. package/package.json +21 -15
  163. package/dist/cli.d.ts +0 -3
  164. package/dist/cli.d.ts.map +0 -1
  165. package/dist/cli.js.map +0 -1
@@ -0,0 +1,1014 @@
1
+ import { createServer } from 'node:http';
2
+ import { hostname } from 'node:os';
3
+ import { createControlPlaneCommandFailedDiagnostic, } from '@uapkg/diagnostics';
4
+ import isCI from 'is-ci';
5
+ import * as oauth from 'oauth4webapi';
6
+ import { AuthMetadataStore } from './AuthMetadataStore.js';
7
+ import { resolveAuthStoragePaths } from './AuthStoragePaths.js';
8
+ import { ControlPlaneClient } from './ControlPlaneClient.js';
9
+ import { ControlPlaneError, isUAPKGCliScope, OAuthScopeInsufficientError, OAuthScopeUnsupportedError, parseUAPKGCliScopeString, registryAudience, UAPKG_AUTHORIZATION_ISSUER, UAPKG_CLI_CLIENT_ID, UAPKG_CLI_SCOPES, UAPKG_CONTROL_PLANE_API, } from './ControlPlaneTypes.js';
10
+ import { CredentialStore } from './CredentialStore.js';
11
+ import { DPoPKeyStore } from './DPoPKeyStore.js';
12
+ import { FileRegistryGrantLock } from './RegistryGrantLock.js';
13
+ const INTERACTIVE_LOGIN_TIMEOUT_MS = 3 * 60 * 1000;
14
+ const INTERACTIVE_LOGIN_FINALIZATION_TIMEOUT_MS = 60 * 1000;
15
+ const CLI_LOGIN_RECONCILIATION_TIMEOUT_MS = 5 * 1000;
16
+ const OAUTH_BACKCHANNEL_TIMEOUT_MS = 30 * 1000;
17
+ const LOOPBACK_CALLBACK_PATH = '/callback';
18
+ const ACCOUNT_COMPLETION_PATH = '/cli-login/complete';
19
+ const CONTROL_PLANE_SERVER_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/;
20
+ export class LoginError extends Error {
21
+ code;
22
+ oauthError;
23
+ constructor(code, message, oauthError) {
24
+ super(message);
25
+ this.code = code;
26
+ this.oauthError = oauthError;
27
+ this.name = 'LoginError';
28
+ }
29
+ }
30
+ export class AccountManager {
31
+ metadata;
32
+ credentials;
33
+ openBrowser;
34
+ isInteractiveLogin;
35
+ interactiveLoginTimeoutMs;
36
+ interactiveLoginFinalizationTimeoutMs;
37
+ keyStore;
38
+ accessTokenCache = new Map();
39
+ grantOperationTails = new Map();
40
+ grantLock;
41
+ constructor(metadata = new AuthMetadataStore(), credentials = new CredentialStore(), openBrowser = async (url) => {
42
+ const { default: open } = await import('open');
43
+ await open(url, { wait: false });
44
+ }, isInteractiveLogin = () => Boolean(process.stdin.isTTY && process.stdout.isTTY && !isCI), grantLock, interactiveLoginTimeoutMs = INTERACTIVE_LOGIN_TIMEOUT_MS, interactiveLoginFinalizationTimeoutMs = INTERACTIVE_LOGIN_FINALIZATION_TIMEOUT_MS) {
45
+ this.metadata = metadata;
46
+ this.credentials = credentials;
47
+ this.openBrowser = openBrowser;
48
+ this.isInteractiveLogin = isInteractiveLogin;
49
+ this.interactiveLoginTimeoutMs = interactiveLoginTimeoutMs;
50
+ this.interactiveLoginFinalizationTimeoutMs = interactiveLoginFinalizationTimeoutMs;
51
+ this.keyStore = new DPoPKeyStore(credentials);
52
+ const metadataPath = typeof metadata.path === 'string' ? metadata.path : undefined;
53
+ this.grantLock = grantLock ?? new FileRegistryGrantLock(resolveAuthStoragePaths(metadataPath).locksDirectory);
54
+ }
55
+ async hasGrant(trust) {
56
+ return Boolean(await this.metadata.find(trust.issuer, trust.registryId));
57
+ }
58
+ invalidateAccessCredentials(trust) {
59
+ for (const key of this.accessTokenCache.keys()) {
60
+ if (key.startsWith(`${trust.issuer}|${trust.registryId}|`)) {
61
+ this.accessTokenCache.delete(key);
62
+ }
63
+ }
64
+ }
65
+ async login(trust, options = {}) {
66
+ this.assertPinnedTrust(trust);
67
+ this.assertInteractiveLogin();
68
+ await this.credentials.assertAvailable();
69
+ const previous = await this.metadata.find(trust.issuer, trust.registryId);
70
+ if (previous && !options.reauthorize) {
71
+ throw new Error(`This device already has a saved login for "${trust.alias}". Use \`uapkg login --registry ${trust.alias} --reauthorize\` to replace it.`);
72
+ }
73
+ emitLoginProgress(options.onProgress, { type: 'preparing', registryAlias: trust.alias });
74
+ const issuer = new URL(trust.issuer);
75
+ const as = await this.discover(issuer);
76
+ this.validateAuthorizationServer(as);
77
+ this.assertIssuerSupportsScopes(as, trust, UAPKG_CLI_SCOPES);
78
+ const client = this.client();
79
+ const keyPair = await this.keyStore.generate();
80
+ const dpop = oauth.DPoP(client, keyPair);
81
+ const publicKeyThumbprint = await dpop.calculateThumbprint();
82
+ const receiver = await LoopbackAuthorizationReceiver.listen(issuer);
83
+ let issuedRefreshToken;
84
+ let retainedIssuedGrant = false;
85
+ let preserveIssuedGrant = false;
86
+ let callbackReceipt;
87
+ let finalizationController;
88
+ let finalizationTimeout;
89
+ try {
90
+ const codeVerifier = oauth.generateRandomCodeVerifier();
91
+ const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
92
+ const state = oauth.generateRandomState();
93
+ const nonce = oauth.generateRandomNonce();
94
+ const requestedScopes = ['openid', 'offline_access', ...UAPKG_CLI_SCOPES];
95
+ const scope = requestedScopes.join(' ');
96
+ const deviceName = normalizeDeviceName(options.deviceName);
97
+ const parameters = new URLSearchParams({
98
+ client_id: client.client_id,
99
+ redirect_uri: receiver.redirectUri,
100
+ response_type: 'code',
101
+ code_challenge: codeChallenge,
102
+ code_challenge_method: 'S256',
103
+ state,
104
+ nonce,
105
+ scope,
106
+ resource: trust.resource,
107
+ dpop_jkt: publicKeyThumbprint,
108
+ registry_id: trust.registryId,
109
+ registry_source_fingerprint: trust.repositoryFingerprint,
110
+ device_name: deviceName,
111
+ max_age: '300',
112
+ });
113
+ if (requestedScopes.includes('offline_access')) {
114
+ parameters.set('prompt', 'consent');
115
+ }
116
+ const expectedPredecessorGrantId = options.reauthorize && previous ? previous.grantId : null;
117
+ if (expectedPredecessorGrantId) {
118
+ parameters.set('replace_grant_id', expectedPredecessorGrantId);
119
+ }
120
+ const pushed = await this.withDPoPNonceRetry(() => oauth.pushedAuthorizationRequest(as, client, oauth.None(), parameters, {
121
+ DPoP: dpop,
122
+ signal: () => AbortSignal.timeout(OAUTH_BACKCHANNEL_TIMEOUT_MS),
123
+ }), (response) => oauth.processPushedAuthorizationResponse(as, client, response));
124
+ if (!as.authorization_endpoint) {
125
+ throw new Error('The authorization server did not advertise an authorization endpoint.');
126
+ }
127
+ const authorizationUrl = new URL(as.authorization_endpoint);
128
+ authorizationUrl.searchParams.set('client_id', client.client_id);
129
+ authorizationUrl.searchParams.set('request_uri', pushed.request_uri);
130
+ emitLoginProgress(options.onProgress, { type: 'opening-browser' });
131
+ try {
132
+ await this.openBrowser(authorizationUrl.href);
133
+ }
134
+ catch {
135
+ emitLoginProgress(options.onProgress, {
136
+ type: 'browser-open-failed',
137
+ authorizationUrl: authorizationUrl.href,
138
+ });
139
+ }
140
+ emitLoginProgress(options.onProgress, { type: 'waiting-for-decision' });
141
+ callbackReceipt = await receiver.waitForCallback(this.interactiveLoginTimeoutMs, trust.alias);
142
+ finalizationController = new AbortController();
143
+ finalizationTimeout = setTimeout(() => {
144
+ finalizationController?.abort(loginFinalizationTimeoutError(trust.alias));
145
+ }, this.interactiveLoginFinalizationTimeoutMs);
146
+ const finalizationSignal = finalizationController.signal;
147
+ const callbackUrl = callbackReceipt.url;
148
+ if (callbackUrl.searchParams.get('iss') !== trust.issuer) {
149
+ throw new LoginError('LOGIN_AUTHORIZATION_RESPONSE_INVALID', 'The browser authorization response came from an unexpected issuer. Run `uapkg login` again.');
150
+ }
151
+ let callbackParameters;
152
+ try {
153
+ callbackParameters = oauth.validateAuthResponse(as, client, callbackUrl, state);
154
+ }
155
+ catch (error) {
156
+ throw authorizationResponseLoginError(error);
157
+ }
158
+ emitLoginProgress(options.onProgress, { type: 'approval-received' });
159
+ const tokens = await this.withDPoPNonceRetry(() => oauth.authorizationCodeGrantRequest(as, client, oauth.None(), callbackParameters, receiver.redirectUri, codeVerifier, { DPoP: dpop, signal: finalizationSignal }), (response) => oauth.processAuthorizationCodeResponse(as, client, response, {
160
+ expectedNonce: nonce,
161
+ maxAge: 300,
162
+ requireIdToken: true,
163
+ }));
164
+ finalizationSignal.throwIfAborted();
165
+ if (tokens.token_type !== 'dpop') {
166
+ throw new Error('The authorization server did not issue a DPoP-bound access token.');
167
+ }
168
+ if (!tokens.refresh_token) {
169
+ throw new Error('The authorization server did not issue a persistent registry grant.');
170
+ }
171
+ const refreshToken = tokens.refresh_token;
172
+ issuedRefreshToken = refreshToken;
173
+ const controlPlane = new ControlPlaneClient(trust.apiBaseUrl);
174
+ const issuedCredential = {
175
+ kind: 'dpop',
176
+ accessToken: tokens.access_token,
177
+ dpop,
178
+ registryAlias: trust.alias,
179
+ requestedScopes: UAPKG_CLI_SCOPES,
180
+ };
181
+ const self = await controlPlane.getCliLoginConfirmation(issuedCredential, finalizationSignal);
182
+ finalizationSignal.throwIfAborted();
183
+ if (self.grant.replacesGrantId !== expectedPredecessorGrantId) {
184
+ throw loginReplacementBindingError(trust.alias);
185
+ }
186
+ if (self.registry.id !== trust.registryId) {
187
+ throw new Error('The authorization server returned a registry grant for an unexpected registry.');
188
+ }
189
+ const missingScopes = UAPKG_CLI_SCOPES.filter((scope) => !self.grant.scopes.includes(scope));
190
+ if (missingScopes.length > 0) {
191
+ throw new Error(`The authorization server omitted required CLI capabilities: ${missingScopes.join(', ')}.`);
192
+ }
193
+ if (self.grant.status === 'pending' &&
194
+ timestampFromIso(self.grant.activationExpiresAt) <= Math.floor(Date.now() / 1000)) {
195
+ throw new LoginError('LOGIN_AUTHORIZATION_TIMEOUT', `Login approval expired before this device could confirm it. Run \`uapkg login --registry ${trust.alias}\` to try again.`);
196
+ }
197
+ const grantId = self.grant.id;
198
+ const keyReference = this.credentials.createReference('dpop-key', trust.issuer, trust.registryId, grantId);
199
+ const refreshTokenReference = this.credentials.createReference('grant', trust.issuer, trust.registryId, grantId);
200
+ const metadata = {
201
+ issuer: trust.issuer,
202
+ registryId: trust.registryId,
203
+ registryName: trust.registryName,
204
+ grantId,
205
+ clientId: client.client_id,
206
+ keyReference,
207
+ refreshTokenReference,
208
+ publicKeyThumbprint,
209
+ deviceName: self.grant.deviceName,
210
+ repositoryFingerprint: trust.repositoryFingerprint,
211
+ account: self.account,
212
+ createdAt: Math.floor(Date.now() / 1000),
213
+ idleExpiresAt: timestampFromIso(self.grant.idleExpiresAt),
214
+ expiresAt: timestampFromIso(self.grant.absoluteExpiresAt),
215
+ };
216
+ let previousForCleanup;
217
+ await this.serializeGrantOperation(trust, async () => {
218
+ const currentPrevious = await this.metadata.find(trust.issuer, trust.registryId);
219
+ if (!isSameRegistryGrant(previous, currentPrevious)) {
220
+ if (options.reauthorize) {
221
+ throw loginReauthorizationConflictError(trust.alias);
222
+ }
223
+ throw new Error(`This device already has a saved login for "${trust.alias}". Use \`uapkg login --registry ${trust.alias} --reauthorize\` to replace it.`);
224
+ }
225
+ if (currentPrevious && !options.reauthorize) {
226
+ throw new Error(`This device already has a saved login for "${trust.alias}". Use \`uapkg login --registry ${trust.alias} --reauthorize\` to replace it.`);
227
+ }
228
+ emitLoginProgress(options.onProgress, { type: 'saving-local-credentials' });
229
+ await this.persistPreparedGrant(metadata, keyPair, refreshToken, currentPrevious, finalizationSignal);
230
+ if (self.grant.status === 'pending') {
231
+ emitLoginProgress(options.onProgress, { type: 'confirming-with-service' });
232
+ const activation = await this.activatePreparedGrant(controlPlane, issuedCredential, metadata.grantId, expectedPredecessorGrantId, finalizationSignal, trust.alias);
233
+ if (activation.kind === 'rejected') {
234
+ await this.rollbackPreparedGrant(metadata, currentPrevious);
235
+ throw activation.error;
236
+ }
237
+ if (activation.kind === 'ambiguous') {
238
+ preserveIssuedGrant = true;
239
+ throw activation.error;
240
+ }
241
+ }
242
+ retainedIssuedGrant = true;
243
+ this.invalidateAccessCredentials(trust);
244
+ if (finalizationTimeout) {
245
+ clearTimeout(finalizationTimeout);
246
+ finalizationTimeout = undefined;
247
+ }
248
+ previousForCleanup = currentPrevious;
249
+ });
250
+ await callbackReceipt.complete('success');
251
+ let warnings = [];
252
+ if (previousForCleanup) {
253
+ try {
254
+ warnings = await this.serializeGrantOperation(trust, () => this.cleanupPreviousLocalCredentials(metadata, previousForCleanup));
255
+ }
256
+ catch {
257
+ warnings = [
258
+ 'The new login is active, but UAPKG could not remove one or more old protected credential entries. Remove the stale UAPKG entries from the operating-system credential store.',
259
+ ];
260
+ }
261
+ }
262
+ return { grant: metadata, warnings };
263
+ }
264
+ catch (error) {
265
+ if (finalizationTimeout) {
266
+ clearTimeout(finalizationTimeout);
267
+ finalizationTimeout = undefined;
268
+ }
269
+ const failure = error instanceof LoginError
270
+ ? error
271
+ : finalizationController?.signal.aborted && finalizationController.signal.reason instanceof LoginError
272
+ ? finalizationController.signal.reason
273
+ : error;
274
+ if (issuedRefreshToken && !retainedIssuedGrant && !preserveIssuedGrant) {
275
+ await this.revokeToken(as, client, issuedRefreshToken, dpop).catch(() => undefined);
276
+ }
277
+ const loginError = normalizeLoginError(failure);
278
+ if (callbackReceipt) {
279
+ const outcome = loginError.code === 'LOGIN_ACCESS_DENIED' ? 'denied' : 'failed';
280
+ await callbackReceipt.complete(outcome).catch(() => undefined);
281
+ }
282
+ throw loginError;
283
+ }
284
+ finally {
285
+ if (finalizationTimeout)
286
+ clearTimeout(finalizationTimeout);
287
+ receiver.close();
288
+ }
289
+ }
290
+ async getAccessCredential(trust, scopes) {
291
+ this.assertPinnedTrust(trust);
292
+ const requestedScopes = [...new Set(scopes)].sort();
293
+ if (requestedScopes.length === 0 || requestedScopes.some((scope) => !isUAPKGCliScope(scope))) {
294
+ throw new Error('A control-plane operation requested an unsupported OAuth capability scope.');
295
+ }
296
+ const grant = await this.requireUsableGrant(trust);
297
+ const as = await this.discover(new URL(trust.issuer));
298
+ this.assertIssuerSupportsScopes(as, trust, requestedScopes);
299
+ const cacheKey = this.accessTokenCacheKey(grant, requestedScopes);
300
+ const cached = this.accessTokenCache.get(cacheKey);
301
+ if (cached && cached.expiresAt > Date.now() + 15_000) {
302
+ return {
303
+ grant,
304
+ credential: {
305
+ kind: 'dpop',
306
+ accessToken: cached.token,
307
+ dpop: cached.dpop,
308
+ registryAlias: trust.alias,
309
+ requestedScopes,
310
+ },
311
+ };
312
+ }
313
+ return this.serializeGrantOperation(trust, async () => {
314
+ // Another process may have replaced the grant or rotated its refresh
315
+ // token while this process waited. Reload every persistent component
316
+ // only after the cross-process lock has been acquired.
317
+ const currentGrant = await this.requireUsableGrant(trust);
318
+ const currentCacheKey = this.accessTokenCacheKey(currentGrant, requestedScopes);
319
+ const refreshedCached = this.accessTokenCache.get(currentCacheKey);
320
+ if (refreshedCached && refreshedCached.expiresAt > Date.now() + 15_000) {
321
+ return {
322
+ grant: currentGrant,
323
+ credential: {
324
+ kind: 'dpop',
325
+ accessToken: refreshedCached.token,
326
+ dpop: refreshedCached.dpop,
327
+ registryAlias: trust.alias,
328
+ requestedScopes,
329
+ },
330
+ };
331
+ }
332
+ const pair = await this.keyStore.load(currentGrant.keyReference);
333
+ if (!pair)
334
+ throw missingKeyError(trust);
335
+ const client = this.client();
336
+ const dpop = oauth.DPoP(client, pair);
337
+ if ((await dpop.calculateThumbprint()) !== currentGrant.publicKeyThumbprint) {
338
+ throw missingKeyError(trust);
339
+ }
340
+ // Refresh tokens rotate on every successful use. Read the current value
341
+ // only after acquiring both the process-local and filesystem lock.
342
+ const refreshToken = await this.credentials.get(currentGrant.refreshTokenReference);
343
+ if (!refreshToken) {
344
+ throw new Error(`The saved login for "${trust.alias}" is missing its protected registry grant. Run \`uapkg login --registry ${trust.alias}\` again.`);
345
+ }
346
+ let tokenResponse;
347
+ try {
348
+ tokenResponse = await this.withDPoPNonceRetry(() => oauth.refreshTokenGrantRequest(as, client, oauth.None(), refreshToken, {
349
+ DPoP: dpop,
350
+ additionalParameters: {
351
+ resource: trust.resource,
352
+ scope: requestedScopes.join(' '),
353
+ },
354
+ }), (response) => oauth.processRefreshTokenResponse(as, client, response));
355
+ }
356
+ catch (error) {
357
+ if (error instanceof oauth.ResponseBodyError && error.error === 'invalid_scope') {
358
+ const hintedScopes = parseUAPKGCliScopeString(error.cause.scope).filter((scope) => requestedScopes.includes(scope));
359
+ const missingScopes = hintedScopes.length > 0 ? hintedScopes : requestedScopes;
360
+ throw new OAuthScopeInsufficientError('The saved registry grant does not include every OAuth scope required by this operation.', trust.alias, requestedScopes, requestedScopes, missingScopes, 403, { cause: error });
361
+ }
362
+ if (error instanceof oauth.ResponseBodyError &&
363
+ (error.error === 'invalid_grant' || error.error === 'invalid_token')) {
364
+ throw new Error(`Your saved login for "${trust.alias}" is no longer valid. Run \`uapkg login --registry ${trust.alias} --reauthorize\` to authenticate again.`, { cause: error });
365
+ }
366
+ throw error;
367
+ }
368
+ if (tokenResponse.token_type !== 'dpop') {
369
+ throw new Error('The authorization server returned an access token that is not DPoP-bound.');
370
+ }
371
+ if (tokenResponse.refresh_token && tokenResponse.refresh_token !== refreshToken) {
372
+ await this.credentials.set(currentGrant.refreshTokenReference, tokenResponse.refresh_token);
373
+ }
374
+ const expiresAt = Date.now() + Math.max(1, tokenResponse.expires_in ?? 300) * 1000;
375
+ this.accessTokenCache.set(currentCacheKey, { token: tokenResponse.access_token, expiresAt, dpop });
376
+ return {
377
+ grant: currentGrant,
378
+ credential: {
379
+ kind: 'dpop',
380
+ accessToken: tokenResponse.access_token,
381
+ dpop,
382
+ registryAlias: trust.alias,
383
+ requestedScopes,
384
+ },
385
+ };
386
+ });
387
+ }
388
+ async logout(trust, localOnly = false) {
389
+ this.assertPinnedTrust(trust);
390
+ return this.serializeGrantOperation(trust, async () => {
391
+ const grant = await this.metadata.find(trust.issuer, trust.registryId);
392
+ if (!grant)
393
+ return 'not-logged-in';
394
+ if (!localOnly) {
395
+ try {
396
+ await this.revokeSavedGrant(grant);
397
+ }
398
+ catch (error) {
399
+ throw new Error(`Unable to revoke the registry grant for "${trust.alias}". The local login was kept so you can retry. Use --local-only only if you accept that account-site revocation may still be required.`, { cause: error });
400
+ }
401
+ }
402
+ await Promise.all([
403
+ this.keyStore.delete(grant.keyReference),
404
+ this.credentials.delete(grant.refreshTokenReference),
405
+ ]);
406
+ await this.metadata.remove(grant.issuer, grant.registryId);
407
+ this.invalidateAccessCredentials(trust);
408
+ return 'removed';
409
+ });
410
+ }
411
+ async requireUsableGrant(trust) {
412
+ const grant = await this.metadata.find(trust.issuer, trust.registryId);
413
+ if (!grant) {
414
+ throw new Error(`You are not logged in to "${trust.alias}". Run \`uapkg login --registry ${trust.alias}\`.`);
415
+ }
416
+ if (grant.repositoryFingerprint !== trust.repositoryFingerprint) {
417
+ throw new Error(`The configured Git source for "${trust.alias}" does not match the source authorized by the saved login. Run \`uapkg login --registry ${trust.alias} --reauthorize\`.`);
418
+ }
419
+ const now = Math.floor(Date.now() / 1000);
420
+ // The idle deadline advances whenever the grant is used. The saved value
421
+ // is display metadata from login time, so the server remains authoritative
422
+ // for idle expiry; the absolute deadline is stable and safe to reject
423
+ // locally.
424
+ if (grant.expiresAt && grant.expiresAt <= now) {
425
+ throw new Error(`Your saved login for "${trust.alias}" has expired. Run \`uapkg login --registry ${trust.alias}\` again.`);
426
+ }
427
+ return grant;
428
+ }
429
+ async revokeSavedGrant(grant) {
430
+ const refreshToken = await this.credentials.get(grant.refreshTokenReference);
431
+ if (!refreshToken) {
432
+ throw new Error('The registry grant cannot be revoked because its protected credential is missing.');
433
+ }
434
+ const pair = await this.keyStore.load(grant.keyReference);
435
+ if (!pair)
436
+ throw new Error('The registry grant cannot be revoked because its DPoP key is missing.');
437
+ const dpop = oauth.DPoP(this.client(), pair);
438
+ if ((await dpop.calculateThumbprint()) !== grant.publicKeyThumbprint) {
439
+ throw new Error('The registry grant cannot be revoked because its DPoP key has changed.');
440
+ }
441
+ const as = await this.discover(new URL(grant.issuer));
442
+ await this.revokeToken(as, this.client(), refreshToken, dpop);
443
+ }
444
+ async activatePreparedGrant(controlPlane, credential, expectedGrantId, expectedPredecessorGrantId, signal, registryAlias) {
445
+ try {
446
+ const confirmed = await controlPlane.confirmCliLogin(credential, signal);
447
+ if (confirmed.id === expectedGrantId && confirmed.replacesGrantId === expectedPredecessorGrantId) {
448
+ return { kind: 'active' };
449
+ }
450
+ return this.reconcilePreparedGrant(controlPlane, credential, expectedGrantId, expectedPredecessorGrantId, registryAlias);
451
+ }
452
+ catch (error) {
453
+ if (isCliLoginReauthorizationConflict(error)) {
454
+ return { kind: 'rejected', error: reauthorizationConflictLoginError(error) };
455
+ }
456
+ if (isDefinitiveCliLoginConfirmationRejection(error))
457
+ return { kind: 'rejected', error };
458
+ return this.reconcilePreparedGrant(controlPlane, credential, expectedGrantId, expectedPredecessorGrantId, registryAlias);
459
+ }
460
+ }
461
+ async reconcilePreparedGrant(controlPlane, credential, expectedGrantId, expectedPredecessorGrantId, registryAlias) {
462
+ const signal = AbortSignal.timeout(CLI_LOGIN_RECONCILIATION_TIMEOUT_MS);
463
+ let retryActivation = true;
464
+ try {
465
+ const observed = await controlPlane.getCliLoginConfirmation(credential, signal);
466
+ if (observed.grant.id !== expectedGrantId || observed.grant.replacesGrantId !== expectedPredecessorGrantId) {
467
+ retryActivation = false;
468
+ }
469
+ else if (observed.grant.status === 'active') {
470
+ return { kind: 'active' };
471
+ }
472
+ }
473
+ catch {
474
+ // An idempotent activation retry can still resolve a failed status read.
475
+ }
476
+ if (retryActivation) {
477
+ try {
478
+ const confirmed = await controlPlane.confirmCliLogin(credential, signal);
479
+ if (confirmed.id === expectedGrantId && confirmed.replacesGrantId === expectedPredecessorGrantId) {
480
+ return { kind: 'active' };
481
+ }
482
+ }
483
+ catch {
484
+ // Once the first activation outcome is ambiguous, even a later 4xx
485
+ // cannot prove that the original request did not commit.
486
+ }
487
+ }
488
+ try {
489
+ const observed = await controlPlane.getCliLoginConfirmation(credential, signal);
490
+ if (observed.grant.id === expectedGrantId &&
491
+ observed.grant.replacesGrantId === expectedPredecessorGrantId &&
492
+ observed.grant.status === 'active') {
493
+ return { kind: 'active' };
494
+ }
495
+ }
496
+ catch {
497
+ // Fall through to the explicit ambiguous outcome below.
498
+ }
499
+ return { kind: 'ambiguous', error: loginConfirmationAmbiguousError(registryAlias) };
500
+ }
501
+ async persistPreparedGrant(metadata, keyPair, refreshToken, previous, signal) {
502
+ let metadataWriteAttempted = false;
503
+ try {
504
+ signal?.throwIfAborted();
505
+ await this.keyStore.save(metadata.keyReference, keyPair);
506
+ signal?.throwIfAborted();
507
+ await this.credentials.set(metadata.refreshTokenReference, refreshToken);
508
+ signal?.throwIfAborted();
509
+ metadataWriteAttempted = true;
510
+ await this.metadata.upsert(metadata);
511
+ signal?.throwIfAborted();
512
+ }
513
+ catch (error) {
514
+ const cleanup = [
515
+ this.keyStore.delete(metadata.keyReference),
516
+ this.credentials.delete(metadata.refreshTokenReference),
517
+ ];
518
+ let metadataRollbackIndex;
519
+ if (metadataWriteAttempted) {
520
+ metadataRollbackIndex = cleanup.length;
521
+ cleanup.push(previous ? this.metadata.upsert(previous) : this.metadata.remove(metadata.issuer, metadata.registryId));
522
+ }
523
+ const cleanupResults = await Promise.allSettled(cleanup);
524
+ if (metadataRollbackIndex !== undefined && cleanupResults[metadataRollbackIndex]?.status === 'rejected') {
525
+ throw new LoginError('LOGIN_FAILED', 'Login failed, and UAPKG could not roll back the local login metadata safely. Check `uapkg whoami --json` before retrying.');
526
+ }
527
+ throw error;
528
+ }
529
+ }
530
+ async rollbackPreparedGrant(metadata, previous) {
531
+ const cleanupResults = await Promise.allSettled([
532
+ this.keyStore.delete(metadata.keyReference),
533
+ this.credentials.delete(metadata.refreshTokenReference),
534
+ previous ? this.metadata.upsert(previous) : this.metadata.remove(metadata.issuer, metadata.registryId),
535
+ ]);
536
+ if (cleanupResults.some(({ status }) => status === 'rejected')) {
537
+ throw new LoginError('LOGIN_FAILED', 'Login confirmation failed, and UAPKG could not roll back the local login safely. Check `uapkg whoami --json` before retrying.');
538
+ }
539
+ }
540
+ async cleanupPreviousLocalCredentials(metadata, previous) {
541
+ if (!previous)
542
+ return [];
543
+ if (previous.grantId === metadata.grantId ||
544
+ (previous.keyReference === metadata.keyReference &&
545
+ previous.refreshTokenReference === metadata.refreshTokenReference)) {
546
+ return [];
547
+ }
548
+ const cleanupResults = await Promise.allSettled([
549
+ this.keyStore.delete(previous.keyReference),
550
+ this.credentials.delete(previous.refreshTokenReference),
551
+ ]);
552
+ const warnings = [];
553
+ if (cleanupResults.some(({ status }) => status === 'rejected')) {
554
+ warnings.push('The previous registry grant was replaced, but one or more old protected credential entries could not be removed. Remove the stale UAPKG entries from the operating-system credential store.');
555
+ }
556
+ return warnings;
557
+ }
558
+ async revokeToken(as, client, refreshToken, dpop) {
559
+ const signer = signingDPoPHandle(dpop);
560
+ await this.withDPoPNonceRetry(() => oauth.revocationRequest(as, client, oauth.None(), refreshToken, {
561
+ additionalParameters: { token_type_hint: 'refresh_token' },
562
+ signal: AbortSignal.timeout(OAUTH_BACKCHANNEL_TIMEOUT_MS),
563
+ [oauth.customFetch]: async (url, options) => {
564
+ const endpoint = new URL(url);
565
+ const headers = new Headers(options.headers);
566
+ await signer.addProof(endpoint, headers, 'POST');
567
+ const response = await fetch(url, { ...options, headers });
568
+ signer.cacheNonce(response, endpoint);
569
+ return response;
570
+ },
571
+ }), (response) => oauth.processRevocationResponse(response));
572
+ }
573
+ async discover(issuer) {
574
+ const discoveryUrl = oidcDiscoveryUrl(issuer);
575
+ let response;
576
+ try {
577
+ response = await oauth.discoveryRequest(issuer, {
578
+ algorithm: 'oidc',
579
+ signal: AbortSignal.timeout(OAUTH_BACKCHANNEL_TIMEOUT_MS),
580
+ });
581
+ }
582
+ catch {
583
+ throw new Error(`Unable to retrieve OAuth metadata for build-pinned issuer "${issuer.href}" from discovery URL "${discoveryUrl.href}".`);
584
+ }
585
+ try {
586
+ return await oauth.processDiscoveryResponse(issuer, response);
587
+ }
588
+ catch {
589
+ throw new Error(`Invalid OAuth metadata for build-pinned issuer "${issuer.href}" from discovery URL "${discoveryUrl.href}" (HTTP ${response.status}).`);
590
+ }
591
+ }
592
+ validateAuthorizationServer(as) {
593
+ if (!as.authorization_endpoint || !as.token_endpoint || !as.pushed_authorization_request_endpoint) {
594
+ throw new Error('The UAPKG authorization server is missing required authorization, token, or PAR endpoints.');
595
+ }
596
+ if (!as.revocation_endpoint) {
597
+ throw new Error('The UAPKG authorization server is missing its grant revocation endpoint.');
598
+ }
599
+ if (as.code_challenge_methods_supported && !as.code_challenge_methods_supported.includes('S256')) {
600
+ throw new Error('The UAPKG authorization server does not support PKCE S256.');
601
+ }
602
+ if (as.dpop_signing_alg_values_supported && !as.dpop_signing_alg_values_supported.includes('ES256')) {
603
+ throw new Error('The UAPKG authorization server does not support ES256 DPoP proofs.');
604
+ }
605
+ }
606
+ assertIssuerSupportsScopes(as, trust, requestedScopes) {
607
+ const advertisedScopes = new Set(Array.isArray(as.scopes_supported) ? as.scopes_supported : []);
608
+ const unsupportedScopes = requestedScopes.filter((scope) => !advertisedScopes.has(scope));
609
+ if (unsupportedScopes.length > 0) {
610
+ throw new OAuthScopeUnsupportedError(trust.alias, requestedScopes, unsupportedScopes);
611
+ }
612
+ }
613
+ client() {
614
+ return {
615
+ client_id: UAPKG_CLI_CLIENT_ID,
616
+ token_endpoint_auth_method: 'none',
617
+ };
618
+ }
619
+ assertPinnedTrust(trust) {
620
+ if (trust.issuer !== UAPKG_AUTHORIZATION_ISSUER ||
621
+ trust.apiBaseUrl !== UAPKG_CONTROL_PLANE_API ||
622
+ trust.resource !== registryAudience(trust.registryId)) {
623
+ throw new Error('UAPKG v1 does not accept project-configured authorization issuers or API URLs.');
624
+ }
625
+ }
626
+ assertInteractiveLogin() {
627
+ if (this.isInteractiveLogin())
628
+ return;
629
+ throw new Error([
630
+ 'Unable to complete interactive login on this machine.',
631
+ '',
632
+ 'Persistent login is not supported in headless environments.',
633
+ '',
634
+ 'For automated publishing, configure a GitHub Actions OIDC trusted publisher.',
635
+ 'For interactive publishing, run `uapkg login` from a trusted, browser-capable workstation.',
636
+ ].join('\n'));
637
+ }
638
+ async withDPoPNonceRetry(request, processResponse) {
639
+ try {
640
+ return await processResponse(await request());
641
+ }
642
+ catch (error) {
643
+ if (!oauth.isDPoPNonceError(error))
644
+ throw error;
645
+ return processResponse(await request());
646
+ }
647
+ }
648
+ accessTokenCacheKey(grant, scopes) {
649
+ return `${grant.issuer}|${grant.registryId}|${grant.grantId}|${scopes.join(' ')}`;
650
+ }
651
+ async serializeGrantOperation(trust, operation) {
652
+ const key = `${trust.issuer}|${trust.registryId}`;
653
+ const previous = this.grantOperationTails.get(key) ?? Promise.resolve();
654
+ const result = previous.then(() => this.grantLock.withLock(trust.issuer, trust.registryId, operation));
655
+ const tail = result.then(() => undefined, () => undefined);
656
+ this.grantOperationTails.set(key, tail);
657
+ try {
658
+ return await result;
659
+ }
660
+ finally {
661
+ if (this.grantOperationTails.get(key) === tail) {
662
+ this.grantOperationTails.delete(key);
663
+ }
664
+ }
665
+ }
666
+ }
667
+ function signingDPoPHandle(handle) {
668
+ const signer = handle;
669
+ if (typeof signer.addProof !== 'function' || typeof signer.cacheNonce !== 'function') {
670
+ throw new Error('The installed OAuth implementation cannot sender-constrain grant revocation.');
671
+ }
672
+ return signer;
673
+ }
674
+ export class LoopbackAuthorizationReceiver {
675
+ server;
676
+ callbackPath;
677
+ expectedHost;
678
+ completionUrl;
679
+ redirectUri;
680
+ callbackPromise;
681
+ callbackResolve;
682
+ callbackReject;
683
+ receivedReceipt;
684
+ timeout;
685
+ state = 'listening';
686
+ terminalError;
687
+ pendingResponse;
688
+ completion;
689
+ constructor(server, callbackPath, expectedHost, completionUrl, redirectUri) {
690
+ this.server = server;
691
+ this.callbackPath = callbackPath;
692
+ this.expectedHost = expectedHost;
693
+ this.completionUrl = completionUrl;
694
+ this.redirectUri = redirectUri;
695
+ }
696
+ static async listen(issuer) {
697
+ const callbackPath = LOOPBACK_CALLBACK_PATH;
698
+ let receiver;
699
+ const server = createServer((request, response) => {
700
+ if (!receiver) {
701
+ response.writeHead(503).end();
702
+ return;
703
+ }
704
+ receiver.receive(request, response);
705
+ });
706
+ await new Promise((resolve, reject) => {
707
+ server.once('error', reject);
708
+ server.listen(0, '127.0.0.1', () => resolve());
709
+ });
710
+ const address = server.address();
711
+ if (!address || typeof address === 'string') {
712
+ server.close();
713
+ throw new Error('Unable to determine the interactive login callback address.');
714
+ }
715
+ const expectedHost = `127.0.0.1:${address.port}`;
716
+ receiver = new LoopbackAuthorizationReceiver(server, callbackPath, expectedHost, new URL(ACCOUNT_COMPLETION_PATH, issuer.origin), `http://${expectedHost}${callbackPath}`);
717
+ return receiver;
718
+ }
719
+ async waitForCallback(timeoutMs, registryAlias) {
720
+ if (this.receivedReceipt)
721
+ return this.receivedReceipt;
722
+ if (this.state === 'closed') {
723
+ throw this.terminalError ?? new LoginError('LOGIN_FAILED', 'The browser authorization listener was closed.');
724
+ }
725
+ if (this.callbackPromise)
726
+ return this.callbackPromise;
727
+ this.callbackPromise = new Promise((resolve, reject) => {
728
+ this.callbackResolve = resolve;
729
+ this.callbackReject = reject;
730
+ this.timeout = setTimeout(() => {
731
+ if (this.state !== 'listening')
732
+ return;
733
+ this.terminate(new LoginError('LOGIN_AUTHORIZATION_TIMEOUT', registryAlias
734
+ ? `Login timed out while waiting for browser authorization. Run \`uapkg login --registry ${registryAlias}\` to try again.`
735
+ : 'Login timed out while waiting for browser authorization. Run `uapkg login` to try again.'));
736
+ }, timeoutMs);
737
+ });
738
+ return this.callbackPromise;
739
+ }
740
+ close() {
741
+ this.terminate(new LoginError('LOGIN_FAILED', 'The browser authorization listener was closed.'));
742
+ }
743
+ receive(request, response) {
744
+ if (this.state !== 'listening' ||
745
+ request.method !== 'GET' ||
746
+ request.headers.host !== this.expectedHost ||
747
+ (request.url !== this.callbackPath && !request.url?.startsWith(`${this.callbackPath}?`))) {
748
+ rejectLoopbackRequest(response);
749
+ return;
750
+ }
751
+ let url;
752
+ try {
753
+ url = new URL(request.url, this.redirectUri);
754
+ }
755
+ catch {
756
+ rejectLoopbackRequest(response);
757
+ return;
758
+ }
759
+ if (url.origin !== new URL(this.redirectUri).origin || url.pathname !== this.callbackPath) {
760
+ rejectLoopbackRequest(response);
761
+ return;
762
+ }
763
+ this.state = 'callback-accepted';
764
+ if (this.timeout)
765
+ clearTimeout(this.timeout);
766
+ this.timeout = undefined;
767
+ this.pendingResponse = response;
768
+ const receipt = {
769
+ url,
770
+ complete: (outcome) => this.complete(response, outcome),
771
+ };
772
+ this.receivedReceipt = receipt;
773
+ this.callbackResolve?.(receipt);
774
+ this.callbackResolve = undefined;
775
+ this.callbackReject = undefined;
776
+ }
777
+ complete(response, outcome) {
778
+ if (this.completion)
779
+ return this.completion;
780
+ this.completion = new Promise((resolve) => {
781
+ let settled = false;
782
+ const finish = () => {
783
+ if (settled)
784
+ return;
785
+ settled = true;
786
+ this.pendingResponse = undefined;
787
+ this.close();
788
+ resolve();
789
+ };
790
+ response.once('finish', finish);
791
+ response.once('close', finish);
792
+ response.once('error', finish);
793
+ if (response.destroyed || response.writableEnded) {
794
+ finish();
795
+ return;
796
+ }
797
+ const location = new URL(this.completionUrl);
798
+ location.hash = new URLSearchParams({ result: outcome }).toString();
799
+ try {
800
+ response
801
+ .writeHead(303, {
802
+ location: location.href,
803
+ 'cache-control': 'no-store',
804
+ 'referrer-policy': 'no-referrer',
805
+ 'content-length': '0',
806
+ 'x-content-type-options': 'nosniff',
807
+ connection: 'close',
808
+ })
809
+ .end();
810
+ }
811
+ catch {
812
+ finish();
813
+ }
814
+ });
815
+ return this.completion;
816
+ }
817
+ terminate(error) {
818
+ if (this.state === 'closed')
819
+ return;
820
+ this.state = 'closed';
821
+ this.terminalError = error;
822
+ if (this.timeout)
823
+ clearTimeout(this.timeout);
824
+ this.timeout = undefined;
825
+ this.callbackReject?.(error);
826
+ this.callbackResolve = undefined;
827
+ this.callbackReject = undefined;
828
+ if (this.pendingResponse && !this.pendingResponse.writableEnded) {
829
+ this.pendingResponse.destroy();
830
+ }
831
+ this.pendingResponse = undefined;
832
+ this.server.close();
833
+ }
834
+ }
835
+ function rejectLoopbackRequest(response) {
836
+ response
837
+ .writeHead(404, {
838
+ 'cache-control': 'no-store',
839
+ 'content-type': 'text/plain; charset=utf-8',
840
+ 'x-content-type-options': 'nosniff',
841
+ })
842
+ .end('Not found.');
843
+ }
844
+ function normalizeDeviceName(value) {
845
+ const candidate = value?.trim() || hostname().trim() || 'UAPKG CLI';
846
+ return candidate.slice(0, 80);
847
+ }
848
+ function oidcDiscoveryUrl(issuer) {
849
+ const discoveryUrl = new URL(issuer);
850
+ discoveryUrl.pathname = `${discoveryUrl.pathname.replace(/\/+$/, '')}/.well-known/openid-configuration`;
851
+ return discoveryUrl;
852
+ }
853
+ function timestampFromIso(value) {
854
+ const parsed = Date.parse(value);
855
+ if (!Number.isFinite(parsed)) {
856
+ throw new Error('The authorization server returned an invalid registry-grant expiration.');
857
+ }
858
+ return Math.floor(parsed / 1000);
859
+ }
860
+ function missingKeyError(trust) {
861
+ return new Error([
862
+ `The saved login for "${trust.alias}" cannot be used because its device-bound signing key is missing or has changed.`,
863
+ '',
864
+ 'This can happen after copying configuration to another computer, resetting secure storage,',
865
+ 'restoring an incomplete backup, or reinstalling the operating system.',
866
+ '',
867
+ `Run \`uapkg login --registry ${trust.alias} --reauthorize\` to authorize this device again.`,
868
+ ].join('\n'));
869
+ }
870
+ function loginFinalizationTimeoutError(registryAlias) {
871
+ return new LoginError('LOGIN_AUTHORIZATION_TIMEOUT', `Login timed out while verifying and saving browser authorization. No new login was saved. Run \`uapkg login --registry ${registryAlias}\` to try again.`);
872
+ }
873
+ function loginConfirmationAmbiguousError(registryAlias) {
874
+ return new LoginError('LOGIN_FAILED', [
875
+ 'UAPKG could not determine whether the service activated this login.',
876
+ 'The locally saved credentials were kept to avoid orphaning a possibly active grant.',
877
+ `Run \`uapkg whoami --registry ${registryAlias}\` to check the login before retrying.`,
878
+ ].join(' '));
879
+ }
880
+ function loginReauthorizationConflictError(registryAlias) {
881
+ return new LoginError('LOGIN_REAUTHORIZATION_CONFLICT', `The saved login for "${registryAlias}" changed while reauthorization was in progress. The newer local login was preserved. Run \`uapkg login --registry ${registryAlias} --reauthorize\` again if you still want to replace it.`);
882
+ }
883
+ function loginReplacementBindingError(registryAlias) {
884
+ return new LoginError('LOGIN_REAUTHORIZATION_CONFLICT', `The authorization service did not bind this login to the expected saved grant for "${registryAlias}". No new local login was saved. Run \`uapkg login --registry ${registryAlias} --reauthorize\` to try again.`);
885
+ }
886
+ function reauthorizationConflictLoginError(error) {
887
+ return new LoginError('LOGIN_REAUTHORIZATION_CONFLICT', error.message);
888
+ }
889
+ function isCliLoginReauthorizationConflict(error) {
890
+ return error instanceof ControlPlaneError && error.code === 'CLI_LOGIN_REAUTHORIZATION_CONFLICT';
891
+ }
892
+ function isSameRegistryGrant(expected, current) {
893
+ if (!expected || !current)
894
+ return expected === current;
895
+ return (expected.grantId === current.grantId &&
896
+ expected.keyReference === current.keyReference &&
897
+ expected.refreshTokenReference === current.refreshTokenReference);
898
+ }
899
+ function isDefinitiveCliLoginConfirmationRejection(error) {
900
+ if (!(error instanceof ControlPlaneError) || error.status === undefined)
901
+ return false;
902
+ if (error.status < 400 || error.status >= 500)
903
+ return false;
904
+ return ![408, 409, 425, 429].includes(error.status);
905
+ }
906
+ function emitLoginProgress(onProgress, event) {
907
+ try {
908
+ onProgress?.(event);
909
+ }
910
+ catch {
911
+ // Reporting must never change the authorization outcome.
912
+ }
913
+ }
914
+ function authorizationResponseLoginError(error) {
915
+ if (error instanceof oauth.AuthorizationResponseError) {
916
+ const oauthError = sanitizeOAuthErrorIdentifier(error.error);
917
+ const description = sanitizeOAuthErrorDescription(error.error_description);
918
+ if (oauthError === 'access_denied') {
919
+ return new LoginError('LOGIN_ACCESS_DENIED', `Login denied: ${description ?? 'The user denied the registry grant.'} (access_denied)`, oauthError);
920
+ }
921
+ return new LoginError('LOGIN_OAUTH_ERROR', `Login failed: ${description ?? 'The authorization server rejected the request.'}${oauthError ? ` (${oauthError})` : ''}`, oauthError);
922
+ }
923
+ return new LoginError('LOGIN_AUTHORIZATION_RESPONSE_INVALID', 'The browser authorization response was invalid. Run `uapkg login` again.');
924
+ }
925
+ function normalizeLoginError(error) {
926
+ if (error instanceof LoginError)
927
+ return error;
928
+ if (isCliLoginReauthorizationConflict(error))
929
+ return reauthorizationConflictLoginError(error);
930
+ if (error instanceof oauth.AuthorizationResponseError)
931
+ return authorizationResponseLoginError(error);
932
+ if (error instanceof oauth.ResponseBodyError) {
933
+ const oauthError = sanitizeOAuthErrorIdentifier(error.error);
934
+ const description = sanitizeOAuthErrorDescription(error.error_description);
935
+ return new LoginError('LOGIN_OAUTH_ERROR', `Login failed: ${description ?? 'The authorization server rejected the request.'}${oauthError ? ` (${oauthError})` : ''}`, oauthError);
936
+ }
937
+ return new LoginError('LOGIN_FAILED', describeControlPlaneError(error));
938
+ }
939
+ function sanitizeOAuthErrorIdentifier(value) {
940
+ return value && /^[a-z][a-z0-9._~-]{0,63}$/i.test(value) ? value : undefined;
941
+ }
942
+ function sanitizeOAuthErrorDescription(value) {
943
+ if (!value)
944
+ return undefined;
945
+ const sanitized = Array.from(value, (character) => {
946
+ const codePoint = character.codePointAt(0) ?? 0;
947
+ return isUnsafeOAuthDescriptionCodePoint(codePoint) ? ' ' : character;
948
+ })
949
+ .join('')
950
+ .replace(/\s+/g, ' ')
951
+ .trim();
952
+ return sanitized ? Array.from(sanitized).slice(0, 300).join('') : undefined;
953
+ }
954
+ function isUnsafeOAuthDescriptionCodePoint(codePoint) {
955
+ return (codePoint <= 0x1f ||
956
+ (codePoint >= 0x7f && codePoint <= 0x9f) ||
957
+ codePoint === 0x061c ||
958
+ codePoint === 0x200e ||
959
+ codePoint === 0x200f ||
960
+ (codePoint >= 0x202a && codePoint <= 0x202e) ||
961
+ codePoint === 0x2028 ||
962
+ codePoint === 0x2029 ||
963
+ (codePoint >= 0x2066 && codePoint <= 0x2069));
964
+ }
965
+ export function loginDiagnosticForError(error) {
966
+ const loginError = normalizeLoginError(error);
967
+ return {
968
+ level: 'error',
969
+ code: loginError.code,
970
+ message: loginError.message,
971
+ data: loginError.oauthError ? { oauthError: loginError.oauthError } : {},
972
+ };
973
+ }
974
+ export function controlPlaneDiagnosticForError(error, operation) {
975
+ const serverCode = error instanceof ControlPlaneError && CONTROL_PLANE_SERVER_CODE_PATTERN.test(error.code) ? error.code : undefined;
976
+ const status = error instanceof ControlPlaneError &&
977
+ error.status !== undefined &&
978
+ Number.isInteger(error.status) &&
979
+ error.status >= 100 &&
980
+ error.status <= 599
981
+ ? error.status
982
+ : undefined;
983
+ const data = {
984
+ operation,
985
+ ...(serverCode === undefined ? {} : { serverCode }),
986
+ ...(status === undefined ? {} : { status }),
987
+ };
988
+ return createControlPlaneCommandFailedDiagnostic(describeControlPlaneError(error), data);
989
+ }
990
+ export function describeControlPlaneError(error) {
991
+ if (error instanceof LoginError)
992
+ return error.message;
993
+ if (error instanceof OAuthScopeInsufficientError && error.missingScopes.length > 0) {
994
+ const scopes = error.missingScopes.map((scope) => `\`${scope}\``).join(', ');
995
+ const noun = error.missingScopes.length === 1 ? 'scope' : 'scopes';
996
+ return `Missing authorization ${noun} ${scopes} for this action.\nRun \`uapkg login --registry ${error.registryAlias} --reauthorize\` to authorize the capabilities required by this CLI version.`;
997
+ }
998
+ if (error instanceof ControlPlaneError) {
999
+ if (error.status === 401 && requiresSavedLoginRenewal(error.code)) {
1000
+ return `${error.message}\n\nRun \`uapkg login\` to authenticate again.`;
1001
+ }
1002
+ return `${error.message} (${error.code})`;
1003
+ }
1004
+ return error instanceof Error ? error.message : String(error);
1005
+ }
1006
+ function requiresSavedLoginRenewal(code) {
1007
+ const normalized = code.toUpperCase();
1008
+ return (normalized.startsWith('DPOP_') ||
1009
+ normalized.startsWith('REGISTRY_GRANT_') ||
1010
+ normalized === 'CONTROL_PLANE_AUTHENTICATION_REQUIRED' ||
1011
+ normalized === 'CLI_ACCESS_TOKEN_INVALID' ||
1012
+ normalized === 'CLI_ACCESS_TOKEN_EXPIRED');
1013
+ }
1014
+ //# sourceMappingURL=AccountManager.js.map