@cloudflare/workers-auth 0.5.5 → 0.6.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.
@@ -1,6 +1,6 @@
1
- import { a as ConfigStorage, A as AuthConfigStorage, U as UserAuthConfig, b as OAuthFlowContext, k as ProfileStore } from '../file-format-B1KgHuUR.mjs';
2
- import { C as CliDescriptor, U as UserPreferences, A as AuthContext, a as CloudflareAuth, K as KeyringPreferenceContext, S as SetKeyringPreferenceResult } from '../keyring-preference-BgjU5DM9.mjs';
3
- export { b as CloudflareLoginProps } from '../keyring-preference-BgjU5DM9.mjs';
1
+ import { a as ConfigStorage, A as AuthConfigStorage, U as UserAuthConfig, b as OAuthFlowContext, k as ProfileStore } from '../file-format-Dbmn5hhd.mjs';
2
+ import { C as CliDescriptor, U as UserPreferences, A as AuthContext, a as CloudflareAuth, K as KeyringPreferenceContext, S as SetKeyringPreferenceResult } from '../keyring-preference-CeP6MLyb.mjs';
3
+ export { b as CloudflareLoginProps } from '../keyring-preference-CeP6MLyb.mjs';
4
4
  import '@cloudflare/workers-utils';
5
5
 
6
6
  /**
package/dist/cf/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { createAuthConfigFileHelpers, createPreferences, createKeyringPreference, createFileStorage, createCloudflareAuth, createCloudflareProfileStore } from '../chunk-RTFTHYPD.mjs';
2
- import '../chunk-PBW6773T.mjs';
1
+ import { createAuthConfigFileHelpers, createPreferences, createKeyringPreference, createFileStorage, createCloudflareAuth, createCloudflareProfileStore } from '../chunk-AEOSGWMT.mjs';
2
+ import '../chunk-7AYQ2BAF.mjs';
3
3
  import { __name } from '../chunk-O6YSETKJ.mjs';
4
4
  import { getEnvironmentVariableFactory, getGlobalConfigPath, getCloudflareApiEnvironmentFromEnv } from '@cloudflare/workers-utils';
5
5
  import path from 'node:path';
@@ -7,6 +7,7 @@ import path from 'node:path';
7
7
  // src/cf/constants.ts
8
8
  var CF_KEYRING_SERVICE_NAME = "cloudflare";
9
9
  var CF_CLI_NAME = "cf";
10
+ var CF_DISPLAY_NAME = "cf";
10
11
  var CF_OAUTH_CALLBACK_URL = "http://localhost:8877/oauth/callback";
11
12
  var CF_CONSENT_PAGES = {
12
13
  granted: {
@@ -158,10 +159,12 @@ var {
158
159
  // src/cf/index.ts
159
160
  var CF_CLI = {
160
161
  cliName: CF_CLI_NAME,
162
+ displayName: CF_DISPLAY_NAME,
161
163
  commands: {
162
164
  login: "cf auth login",
163
165
  whoami: "cf auth whoami",
164
- createProfile: "cf auth create"
166
+ createProfile: "cf auth create",
167
+ deviceLogin: "cf auth login --device"
165
168
  },
166
169
  keyringServiceName: CF_KEYRING_SERVICE_NAME,
167
170
  clientId: getClientIdFromEnv,
@@ -99,6 +99,19 @@ function requireApiToken(options) {
99
99
  return credentials;
100
100
  }
101
101
  __name(requireApiToken, "requireApiToken");
102
+ var ACCOUNT_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
103
+ function validateAccountId(accountId, source) {
104
+ if (!ACCOUNT_ID_PATTERN.test(accountId)) {
105
+ throw new UserError(
106
+ `Invalid account ID "${accountId}" ${source}. Account IDs may only contain alphanumeric characters, hyphens, and underscores.`,
107
+ { telemetryMessage: "user account id invalid characters" }
108
+ );
109
+ }
110
+ return accountId;
111
+ }
112
+ __name(validateAccountId, "validateAccountId");
113
+
114
+ // src/env-vars.ts
102
115
  var getAuthDomainFromEnv = getEnvironmentVariableFactory({
103
116
  variableName: "WRANGLER_AUTH_DOMAIN",
104
117
  defaultValue: /* @__PURE__ */ __name(() => getCloudflareApiEnvironmentFromEnv() === "staging" ? "dash.staging.cloudflare.com" : "dash.cloudflare.com", "defaultValue")
@@ -115,10 +128,26 @@ var getRevokeUrlFromEnv = getEnvironmentVariableFactory({
115
128
  variableName: "WRANGLER_REVOKE_URL",
116
129
  defaultValue: /* @__PURE__ */ __name(() => `https://${getAuthDomainFromEnv()}/oauth2/revoke`, "defaultValue")
117
130
  });
118
- var getCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({
131
+ var readCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({
119
132
  variableName: "CLOUDFLARE_ACCOUNT_ID",
120
133
  deprecatedName: "CF_ACCOUNT_ID"
121
134
  });
135
+ function getCloudflareAccountIdFromEnv() {
136
+ const accountId = readCloudflareAccountIdFromEnv();
137
+ if (!accountId) {
138
+ return void 0;
139
+ }
140
+ return validateAccountId(
141
+ accountId,
142
+ "set in the `CLOUDFLARE_ACCOUNT_ID` environment variable"
143
+ );
144
+ }
145
+ __name(getCloudflareAccountIdFromEnv, "getCloudflareAccountIdFromEnv");
146
+ var DEVICE_AUTH_PATH = "/oauth2/device/auth";
147
+ function getDeviceAuthUrl() {
148
+ return `https://${getAuthDomainFromEnv()}${DEVICE_AUTH_PATH}`;
149
+ }
150
+ __name(getDeviceAuthUrl, "getDeviceAuthUrl");
122
151
  var getAccessClientIdFromEnv = getEnvironmentVariableFactory({
123
152
  variableName: "CLOUDFLARE_ACCESS_CLIENT_ID"
124
153
  });
@@ -296,6 +325,36 @@ async function generatePKCECodes() {
296
325
  return { codeChallenge, codeVerifier };
297
326
  }
298
327
  __name(generatePKCECodes, "generatePKCECodes");
328
+ var generateVerificationUrl = /* @__PURE__ */ __name(({
329
+ verificationUri,
330
+ userCode
331
+ }) => {
332
+ const url2 = new URL(verificationUri);
333
+ url2.searchParams.set("user_code", userCode);
334
+ return url2.toString();
335
+ }, "generateVerificationUrl");
336
+ function parseUrl(value) {
337
+ try {
338
+ return new URL(value);
339
+ } catch {
340
+ return void 0;
341
+ }
342
+ }
343
+ __name(parseUrl, "parseUrl");
344
+ function assertTrustedVerificationUrl(verificationUrl, options) {
345
+ const url2 = parseUrl(verificationUrl);
346
+ const expected = parseUrl(`https://${options.authDomain}`);
347
+ const trusted = url2 !== void 0 && expected !== void 0 && url2.protocol === "https:" && url2.username === "" && url2.password === "" && // `host` rather than `hostname` so a port in either value is significant.
348
+ url2.host === expected.host;
349
+ if (!trusted) {
350
+ throw new UserError(
351
+ `The authorization server returned an untrusted \`${options.field}\`: ${verificationUrl}
352
+ Expected an \`https\` URL on \`${options.authDomain}\`. Refusing to display or open it.`,
353
+ { telemetryMessage: "user device-flow untrusted verification url" }
354
+ );
355
+ }
356
+ }
357
+ __name(assertTrustedVerificationUrl, "assertTrustedVerificationUrl");
299
358
 
300
359
  // src/generate-auth-url.ts
301
360
  var generateAuthUrl = /* @__PURE__ */ __name(({
@@ -1177,6 +1236,318 @@ If you have changed the callback host or port because you are running in a conta
1177
1236
  return Promise.race([timerPromise, loginPromise]);
1178
1237
  }
1179
1238
  __name(getOauthToken, "getOauthToken");
1239
+ var DEVICE_FLOW_MAX_DURATION_SECONDS = 300;
1240
+ var DEVICE_FLOW_MIN_POLL_INTERVAL_SECONDS = 1;
1241
+ function getStringMember(body, key) {
1242
+ if (typeof body !== "object" || body === null) {
1243
+ return void 0;
1244
+ }
1245
+ const value = body[key];
1246
+ return typeof value === "string" && value !== "" ? value : void 0;
1247
+ }
1248
+ __name(getStringMember, "getStringMember");
1249
+ function getPositiveNumberMember(body, key) {
1250
+ if (typeof body !== "object" || body === null) {
1251
+ return void 0;
1252
+ }
1253
+ const value = body[key];
1254
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
1255
+ }
1256
+ __name(getPositiveNumberMember, "getPositiveNumberMember");
1257
+ function describeUnusableResponse(response, body) {
1258
+ const status = `HTTP ${response.status} ${response.statusText}`.trim();
1259
+ const description = getStringMember(body, "error_description");
1260
+ return description ? `${status}: ${description}` : status;
1261
+ }
1262
+ __name(describeUnusableResponse, "describeUnusableResponse");
1263
+ function isRetryableStatus(status) {
1264
+ return status >= 500 || status === 429;
1265
+ }
1266
+ __name(isRetryableStatus, "isRetryableStatus");
1267
+ function asDeviceAuthorizationResponse(body) {
1268
+ const deviceCode = getStringMember(body, "device_code");
1269
+ const userCode = getStringMember(body, "user_code");
1270
+ const verificationUri = getStringMember(body, "verification_uri");
1271
+ const expiresIn = getPositiveNumberMember(body, "expires_in");
1272
+ if (!deviceCode || !userCode || !verificationUri || expiresIn === void 0) {
1273
+ return void 0;
1274
+ }
1275
+ return {
1276
+ device_code: deviceCode,
1277
+ user_code: userCode,
1278
+ verification_uri: verificationUri,
1279
+ verification_uri_complete: getStringMember(
1280
+ body,
1281
+ "verification_uri_complete"
1282
+ ),
1283
+ expires_in: expiresIn,
1284
+ interval: getPositiveNumberMember(body, "interval")
1285
+ };
1286
+ }
1287
+ __name(asDeviceAuthorizationResponse, "asDeviceAuthorizationResponse");
1288
+ async function readJson(response, logger) {
1289
+ const text = await response.text();
1290
+ try {
1291
+ return JSON.parse(text);
1292
+ } catch (e) {
1293
+ if (text.match(/<!DOCTYPE html>/)) {
1294
+ logger.debug(
1295
+ "The body of the response was HTML rather than JSON. Check the debug logs to see the full body of the response."
1296
+ );
1297
+ if (text.match(/challenge-platform/)) {
1298
+ logger.debug(
1299
+ `It looks like you might have hit a bot challenge page. This may be transient but if not, please contact Cloudflare to find out what can be done. When you contact Cloudflare, please provide your Ray ID: ${response.headers.get(
1300
+ "cf-ray"
1301
+ )}`
1302
+ );
1303
+ }
1304
+ }
1305
+ logger.debug("Full body of response\n\n", text);
1306
+ throw new Error(
1307
+ `Invalid JSON in response: status: ${response.status} ${response.statusText}`,
1308
+ { cause: e }
1309
+ );
1310
+ }
1311
+ }
1312
+ __name(readJson, "readJson");
1313
+ async function buildDeviceAuthHeaders(logger, isNonInteractiveOrCI2) {
1314
+ const headers = {
1315
+ "Content-Type": "application/x-www-form-urlencoded"
1316
+ };
1317
+ if (await domainUsesAccess(getAuthDomainFromEnv(), logger)) {
1318
+ logger.debug(
1319
+ "Using Cloudflare Access to get an access token for the device authorization request"
1320
+ );
1321
+ Object.assign(
1322
+ headers,
1323
+ await getCloudflareAccessHeaders({ logger, isNonInteractiveOrCI: isNonInteractiveOrCI2 })
1324
+ );
1325
+ }
1326
+ return headers;
1327
+ }
1328
+ __name(buildDeviceAuthHeaders, "buildDeviceAuthHeaders");
1329
+ async function requestDeviceAuthorization(scopes, clientId, logger, isNonInteractiveOrCI2) {
1330
+ const params = new URLSearchParams({
1331
+ client_id: clientId,
1332
+ scope: [...scopes, "offline_access"].join(" ")
1333
+ });
1334
+ const headers = await buildDeviceAuthHeaders(logger, isNonInteractiveOrCI2);
1335
+ const deviceAuthUrl = getDeviceAuthUrl();
1336
+ logger.debug("Fetching device authorization from", deviceAuthUrl);
1337
+ const response = await fetch(deviceAuthUrl, {
1338
+ method: "POST",
1339
+ body: params.toString(),
1340
+ headers
1341
+ });
1342
+ if (!response.ok) {
1343
+ const body2 = await readJson(response, logger).catch(() => void 0);
1344
+ const rawError = body2 && typeof body2 === "object" && "error" in body2 ? String(body2.error) : `HTTP ${response.status} ${response.statusText}`;
1345
+ throw toErrorClass(rawError);
1346
+ }
1347
+ const body = await readJson(response, logger);
1348
+ const deviceAuth = asDeviceAuthorizationResponse(body);
1349
+ if (deviceAuth === void 0) {
1350
+ logger.debug("Unusable device authorization response body:", body);
1351
+ throw new UserError(
1352
+ `The device authorization endpoint returned a response that could not be interpreted (${describeUnusableResponse(response, body)}).
1353
+ Enable debug logging to see the full response.`,
1354
+ {
1355
+ telemetryMessage: "user device-flow unusable device authorization response"
1356
+ }
1357
+ );
1358
+ }
1359
+ return deviceAuth;
1360
+ }
1361
+ __name(requestDeviceAuthorization, "requestDeviceAuthorization");
1362
+ async function pollDeviceToken(deviceCode, clientId, logger, isNonInteractiveOrCI2) {
1363
+ const params = new URLSearchParams({
1364
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1365
+ device_code: deviceCode,
1366
+ client_id: clientId
1367
+ });
1368
+ const response = await fetchAuthToken(params, logger, isNonInteractiveOrCI2);
1369
+ const body = await readJson(response, logger);
1370
+ const error = getStringMember(body, "error");
1371
+ if (error !== void 0) {
1372
+ return { kind: "oauth-error", error };
1373
+ }
1374
+ const accessToken = getStringMember(body, "access_token");
1375
+ const expiresIn = getPositiveNumberMember(body, "expires_in");
1376
+ if (!response.ok || accessToken === void 0 || expiresIn === void 0) {
1377
+ logger.debug("Unusable token response body:", body);
1378
+ return {
1379
+ kind: "unexpected",
1380
+ detail: describeUnusableResponse(response, body),
1381
+ // A 2xx that carries no usable grant is not going to become one on the
1382
+ // next attempt, so only the retryable statuses keep the loop alive.
1383
+ retryable: !response.ok && isRetryableStatus(response.status)
1384
+ };
1385
+ }
1386
+ return {
1387
+ kind: "token",
1388
+ grant: {
1389
+ access_token: accessToken,
1390
+ expires_in: expiresIn,
1391
+ refresh_token: getStringMember(body, "refresh_token"),
1392
+ scope: getStringMember(body, "scope")
1393
+ }
1394
+ };
1395
+ }
1396
+ __name(pollDeviceToken, "pollDeviceToken");
1397
+ function sleep(seconds) {
1398
+ return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
1399
+ }
1400
+ __name(sleep, "sleep");
1401
+ async function getOauthTokenViaDeviceFlow(options, ctx) {
1402
+ const { logger, openInBrowser, isNonInteractiveOrCI: isNonInteractiveOrCI2 } = ctx;
1403
+ const deviceAuth = await requestDeviceAuthorization(
1404
+ options.scopes,
1405
+ options.clientId,
1406
+ logger,
1407
+ isNonInteractiveOrCI2
1408
+ );
1409
+ const authDomain = getAuthDomainFromEnv();
1410
+ assertTrustedVerificationUrl(deviceAuth.verification_uri, {
1411
+ field: "verification_uri",
1412
+ authDomain
1413
+ });
1414
+ if (deviceAuth.verification_uri_complete !== void 0) {
1415
+ assertTrustedVerificationUrl(deviceAuth.verification_uri_complete, {
1416
+ field: "verification_uri_complete",
1417
+ authDomain
1418
+ });
1419
+ }
1420
+ const verificationUrl = deviceAuth.verification_uri_complete ?? generateVerificationUrl({
1421
+ verificationUri: deviceAuth.verification_uri,
1422
+ userCode: deviceAuth.user_code
1423
+ });
1424
+ const maxDurationSeconds = Math.min(
1425
+ deviceAuth.expires_in,
1426
+ DEVICE_FLOW_MAX_DURATION_SECONDS
1427
+ );
1428
+ logger.log(
1429
+ esm_default`
1430
+ To authorize ${ctx.displayName}, please visit:
1431
+
1432
+ ${deviceAuth.verification_uri}
1433
+
1434
+ and enter the code:
1435
+
1436
+ ${deviceAuth.user_code}
1437
+
1438
+ You have ${toHumanPeriod(maxDurationSeconds)} to approve this request.
1439
+ `
1440
+ );
1441
+ if (options.browser) {
1442
+ logger.log(`
1443
+ Opening a link in your default browser: ${verificationUrl}`);
1444
+ await openInBrowser(verificationUrl);
1445
+ }
1446
+ const deadline = Date.now() + maxDurationSeconds * 1e3;
1447
+ let intervalSeconds = Math.max(
1448
+ deviceAuth.interval ?? DEVICE_FLOW_MIN_POLL_INTERVAL_SECONDS,
1449
+ DEVICE_FLOW_MIN_POLL_INTERVAL_SECONDS
1450
+ );
1451
+ let firstPoll = true;
1452
+ let lastPollFailure;
1453
+ while (Date.now() < deadline) {
1454
+ if (!firstPoll) {
1455
+ await sleep(intervalSeconds);
1456
+ }
1457
+ firstPoll = false;
1458
+ let result;
1459
+ try {
1460
+ result = await pollDeviceToken(
1461
+ deviceAuth.device_code,
1462
+ options.clientId,
1463
+ logger,
1464
+ isNonInteractiveOrCI2
1465
+ );
1466
+ } catch (e) {
1467
+ logger.debug("Ignoring transient error while polling for token:", e);
1468
+ lastPollFailure = e instanceof Error ? e.message : String(e);
1469
+ continue;
1470
+ }
1471
+ if (result.kind === "unexpected") {
1472
+ if (!result.retryable) {
1473
+ throw new UserError(
1474
+ `The token endpoint returned a response that could not be interpreted (${result.detail}).
1475
+ Enable debug logging to see the full response.`,
1476
+ { telemetryMessage: "user device-flow unusable token response" }
1477
+ );
1478
+ }
1479
+ logger.debug(
1480
+ "Ignoring transient response while polling for token:",
1481
+ result.detail
1482
+ );
1483
+ lastPollFailure = result.detail;
1484
+ continue;
1485
+ }
1486
+ lastPollFailure = void 0;
1487
+ if (result.kind === "oauth-error") {
1488
+ switch (result.error) {
1489
+ case "authorization_pending":
1490
+ continue;
1491
+ case "slow_down":
1492
+ intervalSeconds += 5;
1493
+ continue;
1494
+ case "access_denied":
1495
+ throw new UserError(
1496
+ `Consent denied. You must grant consent to ${ctx.displayName} in order to login.
1497
+ If you don't want to do this consider passing an API token via the \`CLOUDFLARE_API_TOKEN\` environment variable.`,
1498
+ { telemetryMessage: "user device-flow consent denied" }
1499
+ );
1500
+ case "expired_token":
1501
+ throw new UserError(
1502
+ `Device code expired before the request was approved. Please run \`${ctx.deviceLoginCommand}\` again to obtain a new code.`,
1503
+ { telemetryMessage: "user device-flow code expired" }
1504
+ );
1505
+ default:
1506
+ throw toErrorClass(result.error);
1507
+ }
1508
+ }
1509
+ const { grant } = result;
1510
+ const accessToken = {
1511
+ value: grant.access_token,
1512
+ expiry: new Date(Date.now() + grant.expires_in * 1e3).toISOString()
1513
+ };
1514
+ const scopes = grant.scope ? grant.scope.split(" ") : [];
1515
+ return {
1516
+ token: accessToken,
1517
+ scopes,
1518
+ refreshToken: grant.refresh_token ? { value: grant.refresh_token } : void 0
1519
+ };
1520
+ }
1521
+ if (lastPollFailure !== void 0) {
1522
+ throw new UserError(
1523
+ `Gave up waiting for device authorization: the token endpoint kept returning responses that could not be interpreted (last: ${lastPollFailure}).
1524
+ Enable debug logging to see the full response, then run \`${ctx.deviceLoginCommand}\` to try again.`,
1525
+ { telemetryMessage: "user device-flow poll failed" }
1526
+ );
1527
+ }
1528
+ throw new UserError(
1529
+ `Device authorization timed out after ${toHumanPeriod(maxDurationSeconds)}. Please run \`${ctx.deviceLoginCommand}\` again to obtain a new code.`,
1530
+ { telemetryMessage: "user device-flow authorization timeout" }
1531
+ );
1532
+ }
1533
+ __name(getOauthTokenViaDeviceFlow, "getOauthTokenViaDeviceFlow");
1534
+ function toHumanPeriod(seconds) {
1535
+ if (seconds < 60) {
1536
+ return `${seconds} seconds`;
1537
+ }
1538
+ const minutes = Math.floor(seconds / 60);
1539
+ const minutesPostfix = minutes === 1 ? "minute" : "minutes";
1540
+ const remainingSeconds = seconds % 60;
1541
+ const secondsPostfix = remainingSeconds === 1 ? "second" : "seconds";
1542
+ if (remainingSeconds === 0) {
1543
+ return `${minutes} ${minutesPostfix}`;
1544
+ }
1545
+ if (remainingSeconds == 59) {
1546
+ return `${minutes + 1} ${minutesPostfix}`;
1547
+ }
1548
+ return `${minutes} ${minutesPostfix} and ${remainingSeconds} ${secondsPostfix}`;
1549
+ }
1550
+ __name(toHumanPeriod, "toHumanPeriod");
1180
1551
 
1181
1552
  // src/flow.ts
1182
1553
  function createOAuthFlow(ctx) {
@@ -1219,22 +1590,37 @@ function createOAuthFlow(ctx) {
1219
1590
  }
1220
1591
  );
1221
1592
  }
1222
- ctx.logger.log("Attempting to login via OAuth...");
1223
- const oauth = await getOauthToken(
1224
- {
1225
- browser: props.browser ?? true,
1226
- scopes: props.scopes,
1227
- clientId: getClientId(),
1228
- redirectUri: ctx.redirectUri,
1229
- denied: consent.denied,
1230
- granted: consent.granted,
1231
- callbackHost: props.callbackHost ?? defaultCallbackHost,
1232
- callbackPort: props.callbackPort ?? defaultCallbackPort
1233
- },
1234
- oauthFlowState,
1235
- ctx,
1236
- generators
1237
- );
1593
+ let oauth;
1594
+ if (props.device) {
1595
+ ctx.logger.log(
1596
+ "Attempting to login via OAuth Device Authorization Grant..."
1597
+ );
1598
+ oauth = await getOauthTokenViaDeviceFlow(
1599
+ {
1600
+ browser: props.browser ?? true,
1601
+ scopes: props.scopes,
1602
+ clientId: getClientId()
1603
+ },
1604
+ ctx
1605
+ );
1606
+ } else {
1607
+ ctx.logger.log("Attempting to login via OAuth...");
1608
+ oauth = await getOauthToken(
1609
+ {
1610
+ browser: props.browser ?? true,
1611
+ scopes: props.scopes,
1612
+ clientId: getClientId(),
1613
+ redirectUri: ctx.redirectUri,
1614
+ denied: consent.denied,
1615
+ granted: consent.granted,
1616
+ callbackHost: props.callbackHost ?? defaultCallbackHost,
1617
+ callbackPort: props.callbackPort ?? defaultCallbackPort
1618
+ },
1619
+ oauthFlowState,
1620
+ ctx,
1621
+ generators
1622
+ );
1623
+ }
1238
1624
  getStorage(props.profile).write({
1239
1625
  oauth_token: oauth.token?.value ?? "",
1240
1626
  expiration_time: oauth.token?.expiry,
@@ -3724,4 +4110,4 @@ smol-toml/dist/index.js:
3724
4110
  *)
3725
4111
  */
3726
4112
 
3727
- export { EncryptedFileCredentialStore, FileCredentialStore, LinuxSecretToolKeyProvider, MacSecurityKeyProvider, NapiKeyringKeyProvider, PINNED_KEYRING_VERSION, PKCE_CHARSET, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, clearAccessCaches, clearCredentialStorageState, createCredentialStorageContext, createOAuthFlow, createProfileStore, decryptString, dedent, domainUsesAccess, encryptString, findKeyringBinding, generateAuthUrl, generateKey, generateRandomState, getAccessHeaders, getAuthConfigFilePath, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareAccountIdFromEnv, getCloudflareAuthUseKeyringFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, getEncryptedAuthConfigFilePath, getKeyringAccountName, getKeyringInstallDir, installKeyringBindingSync, parseEncryptedEnvelope, parseFile, probeSecretTool, readStoredAuthState, resetCredentialStorageState, resolveKeyProvider, scrubEncryptedCredentials, setKeyProviderFactoryForTesting, setKeyringEntryFactory, setLinuxSecretToolRunner, setMacSecurityCommandRunner, setNpmRunner, stringifyFile, validateProfileName };
4113
+ export { EncryptedFileCredentialStore, FileCredentialStore, LinuxSecretToolKeyProvider, MacSecurityKeyProvider, NapiKeyringKeyProvider, PINNED_KEYRING_VERSION, PKCE_CHARSET, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, clearAccessCaches, clearCredentialStorageState, createCredentialStorageContext, createOAuthFlow, createProfileStore, decryptString, dedent, domainUsesAccess, encryptString, findKeyringBinding, generateAuthUrl, generateKey, generateRandomState, generateVerificationUrl, getAccessHeaders, getAuthConfigFilePath, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareAccountIdFromEnv, getCloudflareAuthUseKeyringFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, getEncryptedAuthConfigFilePath, getKeyringAccountName, getKeyringInstallDir, installKeyringBindingSync, parseEncryptedEnvelope, parseFile, probeSecretTool, readStoredAuthState, resetCredentialStorageState, resolveKeyProvider, scrubEncryptedCredentials, setKeyProviderFactoryForTesting, setKeyringEntryFactory, setLinuxSecretToolRunner, setMacSecurityCommandRunner, setNpmRunner, stringifyFile, validateAccountId, validateProfileName };
@@ -1,4 +1,4 @@
1
- import { stringifyFile, parseFile, dedent, createCredentialStorageContext, createOAuthFlow, getAuthFromEnv, createProfileStore, getCloudflareAccountIdFromEnv, getAuthConfigFilePath, getEncryptedAuthConfigFilePath, scrubEncryptedCredentials, validateProfileName, getCloudflareAuthUseKeyringFromEnv } from './chunk-PBW6773T.mjs';
1
+ import { stringifyFile, parseFile, dedent, createCredentialStorageContext, createOAuthFlow, getAuthFromEnv, createProfileStore, validateAccountId, getCloudflareAccountIdFromEnv, getAuthConfigFilePath, getEncryptedAuthConfigFilePath, scrubEncryptedCredentials, validateProfileName, getCloudflareAuthUseKeyringFromEnv } from './chunk-7AYQ2BAF.mjs';
2
2
  import { __name } from './chunk-O6YSETKJ.mjs';
3
3
  import { existsSync, rmSync, mkdirSync, writeFileSync, chmodSync, readFileSync as readFileSync$1, readdirSync } from 'node:fs';
4
4
  import path3 from 'node:path';
@@ -810,6 +810,8 @@ Run \`${descriptor.commands.whoami}\` to check your current authentication statu
810
810
  purgeOnLoginOrLogout: configCache.purgeConfigCaches,
811
811
  clientId: descriptor.clientId,
812
812
  consent: descriptor.consent,
813
+ displayName: descriptor.displayName,
814
+ deviceLoginCommand: descriptor.commands.deviceLogin,
813
815
  redirectUri: descriptor.redirectUri,
814
816
  storageFactory: credentialStorage.storageFactory,
815
817
  allowGlobalAuthKey,
@@ -855,7 +857,8 @@ Run \`${descriptor.commands.whoami}\` to check your current authentication statu
855
857
  browser: props?.browser ?? true,
856
858
  callbackHost: props?.callbackHost,
857
859
  callbackPort: props?.callbackPort,
858
- profile: props?.profile
860
+ profile: props?.profile,
861
+ device: props?.device
859
862
  };
860
863
  }
861
864
  __name(withDefaultScopes, "withDefaultScopes");
@@ -1012,7 +1015,10 @@ Alternatively, try running \`${descriptor.commands.login}\` to re-authenticate.`
1012
1015
  return temporaryAccount.account.id;
1013
1016
  }
1014
1017
  if (config.account_id) {
1015
- return config.account_id;
1018
+ return validateAccountId(
1019
+ config.account_id,
1020
+ `set as \`account_id\` in your ${descriptor.getConfigFileLabel()} file`
1021
+ );
1016
1022
  }
1017
1023
  const envAccountId = getCloudflareAccountIdFromEnv();
1018
1024
  if (envAccountId) {
@@ -174,6 +174,18 @@ interface OAuthFlowContext {
174
174
  * or denies consent.
175
175
  */
176
176
  consent: OAuthConsentPages;
177
+ /**
178
+ * The consuming CLI's branded name (e.g. `"Wrangler"`, `"cf"`), interpolated
179
+ * into the copy the flow prints to the user — "To authorize <name>, please
180
+ * visit ...". Consumer-specific, so it is required.
181
+ */
182
+ displayName: string;
183
+ /**
184
+ * The command that restarts the device authorization flow (e.g.
185
+ * `"wrangler login --device"`), quoted when a device code is denied,
186
+ * expires, or the flow times out. Consumer-specific, so it is required.
187
+ */
188
+ deviceLoginCommand: string;
177
189
  /**
178
190
  * The `redirect_uri` registered on the consumer's OAuth app
179
191
  */
@@ -271,6 +283,16 @@ interface LoginProps {
271
283
  * Only for use by 'auth create', not exposed to the user
272
284
  */
273
285
  profile?: string;
286
+ /**
287
+ * When `true`, authenticate using the OAuth 2.0 Device Authorization Grant
288
+ * (RFC 8628) instead of the authorization-code-with-PKCE callback flow. The
289
+ * device flow does not start a local callback server and works in
290
+ * environments where the consumer's loopback callback URL
291
+ * ({@link OAuthFlowContext.redirectUri}) is unreachable from the user's
292
+ * browser (containers, remote SSH sessions, Codespaces). `callbackHost` and
293
+ * `callbackPort` are ignored when this is set.
294
+ */
295
+ device?: boolean;
274
296
  }
275
297
  /**
276
298
  * Public surface returned by {@link createOAuthFlow}.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OAuthFlowLogger, U as UserAuthConfig, A as AuthConfigStorage, C as CredentialStore, F as FileFormat } from './file-format-B1KgHuUR.mjs';
2
- export { a as ConfigStorage, D as DeactivateDirectoryResult, i as DirectoryBindingOperations, j as DirectoryBindingsStorage, L as LoginOrRefreshFailureReason, d as LoginOrRefreshResult, e as LoginProps, b as OAuthFlowContext, P as ProfileConfigOperations, k as ProfileStore, T as TemporaryPreviewAccount, c as createOAuthFlow, h as createProfileStore, g as generateAuthUrl, f as generateRandomState, v as validateProfileName } from './file-format-B1KgHuUR.mjs';
1
+ import { O as OAuthFlowLogger, U as UserAuthConfig, A as AuthConfigStorage, C as CredentialStore, F as FileFormat } from './file-format-Dbmn5hhd.mjs';
2
+ export { a as ConfigStorage, D as DeactivateDirectoryResult, i as DirectoryBindingOperations, j as DirectoryBindingsStorage, L as LoginOrRefreshFailureReason, d as LoginOrRefreshResult, e as LoginProps, b as OAuthFlowContext, P as ProfileConfigOperations, k as ProfileStore, T as TemporaryPreviewAccount, c as createOAuthFlow, h as createProfileStore, g as generateAuthUrl, f as generateRandomState, v as validateProfileName } from './file-format-Dbmn5hhd.mjs';
3
3
  import { ApiCredentials } from '@cloudflare/workers-utils';
4
4
  import { SpawnSyncReturns } from 'node:child_process';
5
5
 
@@ -81,8 +81,13 @@ declare const getAuthUrlFromEnv: () => string;
81
81
  * This is a Cloudflare-wide variable (not wrangler-specific), so it lives in the
82
82
  * shared core rather than a consumer layer. `CF_ACCOUNT_ID` is the deprecated
83
83
  * spelling.
84
+ *
85
+ * Every caller feeds the result into a Cloudflare API URL path, so the value is
86
+ * validated here rather than at each call site. An empty string is treated as
87
+ * unset so callers keep falling back to the cached / interactively selected
88
+ * account.
84
89
  */
85
- declare const getCloudflareAccountIdFromEnv: () => string | undefined;
90
+ declare function getCloudflareAccountIdFromEnv(): string | undefined;
86
91
  /**
87
92
  * `CLOUDFLARE_AUTH_USE_KEYRING` overrides where OAuth credentials are stored.
88
93
  *
@@ -98,6 +103,25 @@ declare const getCloudflareAccountIdFromEnv: () => string | undefined;
98
103
  */
99
104
  declare const getCloudflareAuthUseKeyringFromEnv: () => boolean | undefined;
100
105
 
106
+ /**
107
+ * Build the URL the user should visit to approve a device authorization
108
+ * request, with the `user_code` embedded as a query parameter so the user
109
+ * does not have to type it manually.
110
+ *
111
+ * Per RFC 8628 §3.3.1, this is the "verification_uri_complete" optimization
112
+ * for non-textual transmission (such as QR codes). Authorization servers may
113
+ * return their own `verification_uri_complete` in the device authorization
114
+ * response — prefer that when it is provided. Use this helper as a fallback
115
+ * when only `verification_uri` is available.
116
+ *
117
+ * Extracted into its own module (mirroring `generate-auth-url.ts`) so that
118
+ * tests can mock the generated URL deterministically.
119
+ */
120
+ declare const generateVerificationUrl: ({ verificationUri, userCode, }: {
121
+ verificationUri: string;
122
+ userCode: string;
123
+ }) => string;
124
+
101
125
  declare const TEMPORARY_TERMS_PROMPT: string;
102
126
  declare const TEMPORARY_TERMS_NOTICE: string;
103
127
 
@@ -785,4 +809,4 @@ declare function decryptString(envelope: EncryptedEnvelope, key: Uint8Array): st
785
809
  */
786
810
  declare function parseEncryptedEnvelope(raw: unknown): EncryptedEnvelope | undefined;
787
811
 
788
- export { AuthConfigStorage, type CredentialStorageBundle, type CredentialStorageContext, CredentialStore, type EncryptedEnvelope, EncryptedFileCredentialStore, FileCredentialStore, type KeyProvider, type KeyProviderResolution, type KeyringEntry, type KeyringEntryFactory, LinuxSecretToolKeyProvider, type LinuxSecretToolRunner, type MacSecurityCommandRunner, MacSecurityKeyProvider, NapiKeyringKeyProvider, type NpmRunner, OAuthFlowLogger, PINNED_KEYRING_VERSION, PKCE_CHARSET, type ScrubEncryptedCredentialsResult, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, UserAuthConfig, clearAccessCaches, clearCredentialStorageState, createCredentialStorageContext, decryptString, domainUsesAccess, encryptString, findKeyringBinding, generateKey, getAccessHeaders, getAuthConfigFilePath, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareAccountIdFromEnv, getCloudflareAuthUseKeyringFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, getEncryptedAuthConfigFilePath, getKeyringAccountName, getKeyringInstallDir, installKeyringBindingSync, parseEncryptedEnvelope, probeSecretTool, readStoredAuthState, resetCredentialStorageState, resolveKeyProvider, scrubEncryptedCredentials, setKeyProviderFactoryForTesting, setKeyringEntryFactory, setLinuxSecretToolRunner, setMacSecurityCommandRunner, setNpmRunner };
812
+ export { AuthConfigStorage, type CredentialStorageBundle, type CredentialStorageContext, CredentialStore, type EncryptedEnvelope, EncryptedFileCredentialStore, FileCredentialStore, type KeyProvider, type KeyProviderResolution, type KeyringEntry, type KeyringEntryFactory, LinuxSecretToolKeyProvider, type LinuxSecretToolRunner, type MacSecurityCommandRunner, MacSecurityKeyProvider, NapiKeyringKeyProvider, type NpmRunner, OAuthFlowLogger, PINNED_KEYRING_VERSION, PKCE_CHARSET, type ScrubEncryptedCredentialsResult, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, UserAuthConfig, clearAccessCaches, clearCredentialStorageState, createCredentialStorageContext, decryptString, domainUsesAccess, encryptString, findKeyringBinding, generateKey, generateVerificationUrl, getAccessHeaders, getAuthConfigFilePath, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareAccountIdFromEnv, getCloudflareAuthUseKeyringFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, getEncryptedAuthConfigFilePath, getKeyringAccountName, getKeyringInstallDir, installKeyringBindingSync, parseEncryptedEnvelope, probeSecretTool, readStoredAuthState, resetCredentialStorageState, resolveKeyProvider, scrubEncryptedCredentials, setKeyProviderFactoryForTesting, setKeyringEntryFactory, setLinuxSecretToolRunner, setMacSecurityCommandRunner, setNpmRunner };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export { EncryptedFileCredentialStore, FileCredentialStore, LinuxSecretToolKeyProvider, MacSecurityKeyProvider, NapiKeyringKeyProvider, PINNED_KEYRING_VERSION, PKCE_CHARSET, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, clearAccessCaches, clearCredentialStorageState, createCredentialStorageContext, createOAuthFlow, createProfileStore, decryptString, domainUsesAccess, encryptString, findKeyringBinding, generateAuthUrl, generateKey, generateRandomState, getAccessHeaders, getAuthConfigFilePath, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareAccountIdFromEnv, getCloudflareAuthUseKeyringFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, getEncryptedAuthConfigFilePath, getKeyringAccountName, getKeyringInstallDir, installKeyringBindingSync, parseEncryptedEnvelope, probeSecretTool, readStoredAuthState, resetCredentialStorageState, resolveKeyProvider, scrubEncryptedCredentials, setKeyProviderFactoryForTesting, setKeyringEntryFactory, setLinuxSecretToolRunner, setMacSecurityCommandRunner, setNpmRunner, validateProfileName } from './chunk-PBW6773T.mjs';
1
+ export { EncryptedFileCredentialStore, FileCredentialStore, LinuxSecretToolKeyProvider, MacSecurityKeyProvider, NapiKeyringKeyProvider, PINNED_KEYRING_VERSION, PKCE_CHARSET, TEMPORARY_TERMS_NOTICE, TEMPORARY_TERMS_PROMPT, clearAccessCaches, clearCredentialStorageState, createCredentialStorageContext, createOAuthFlow, createProfileStore, decryptString, domainUsesAccess, encryptString, findKeyringBinding, generateAuthUrl, generateKey, generateRandomState, generateVerificationUrl, getAccessHeaders, getAuthConfigFilePath, getAuthFromEnv, getAuthUrlFromEnv, getCloudflareAPITokenFromEnv, getCloudflareAccountIdFromEnv, getCloudflareAuthUseKeyringFromEnv, getCloudflareGlobalAuthEmailFromEnv, getCloudflareGlobalAuthKeyFromEnv, getEncryptedAuthConfigFilePath, getKeyringAccountName, getKeyringInstallDir, installKeyringBindingSync, parseEncryptedEnvelope, probeSecretTool, readStoredAuthState, resetCredentialStorageState, resolveKeyProvider, scrubEncryptedCredentials, setKeyProviderFactoryForTesting, setKeyringEntryFactory, setLinuxSecretToolRunner, setMacSecurityCommandRunner, setNpmRunner, validateProfileName } from './chunk-7AYQ2BAF.mjs';
2
2
  import './chunk-O6YSETKJ.mjs';