@cloudflare/workers-auth 0.5.6 → 0.6.1

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-CEH7NWR7.mjs';
2
- import '../chunk-7ZPLVMOI.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,
@@ -143,6 +143,11 @@ function getCloudflareAccountIdFromEnv() {
143
143
  );
144
144
  }
145
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");
146
151
  var getAccessClientIdFromEnv = getEnvironmentVariableFactory({
147
152
  variableName: "CLOUDFLARE_ACCESS_CLIENT_ID"
148
153
  });
@@ -320,6 +325,36 @@ async function generatePKCECodes() {
320
325
  return { codeChallenge, codeVerifier };
321
326
  }
322
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");
323
358
 
324
359
  // src/generate-auth-url.ts
325
360
  var generateAuthUrl = /* @__PURE__ */ __name(({
@@ -1201,6 +1236,318 @@ If you have changed the callback host or port because you are running in a conta
1201
1236
  return Promise.race([timerPromise, loginPromise]);
1202
1237
  }
1203
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");
1204
1551
 
1205
1552
  // src/flow.ts
1206
1553
  function createOAuthFlow(ctx) {
@@ -1243,22 +1590,37 @@ function createOAuthFlow(ctx) {
1243
1590
  }
1244
1591
  );
1245
1592
  }
1246
- ctx.logger.log("Attempting to login via OAuth...");
1247
- const oauth = await getOauthToken(
1248
- {
1249
- browser: props.browser ?? true,
1250
- scopes: props.scopes,
1251
- clientId: getClientId(),
1252
- redirectUri: ctx.redirectUri,
1253
- denied: consent.denied,
1254
- granted: consent.granted,
1255
- callbackHost: props.callbackHost ?? defaultCallbackHost,
1256
- callbackPort: props.callbackPort ?? defaultCallbackPort
1257
- },
1258
- oauthFlowState,
1259
- ctx,
1260
- generators
1261
- );
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
+ }
1262
1624
  getStorage(props.profile).write({
1263
1625
  oauth_token: oauth.token?.value ?? "",
1264
1626
  expiration_time: oauth.token?.expiry,
@@ -3748,4 +4110,4 @@ smol-toml/dist/index.js:
3748
4110
  *)
3749
4111
  */
3750
4112
 
3751
- 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, validateAccountId, 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, validateAccountId, getCloudflareAccountIdFromEnv, getAuthConfigFilePath, getEncryptedAuthConfigFilePath, scrubEncryptedCredentials, validateProfileName, getCloudflareAuthUseKeyringFromEnv } from './chunk-7ZPLVMOI.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");
@@ -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
 
@@ -103,6 +103,25 @@ declare function getCloudflareAccountIdFromEnv(): string | undefined;
103
103
  */
104
104
  declare const getCloudflareAuthUseKeyringFromEnv: () => boolean | undefined;
105
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
+
106
125
  declare const TEMPORARY_TERMS_PROMPT: string;
107
126
  declare const TEMPORARY_TERMS_NOTICE: string;
108
127
 
@@ -790,4 +809,4 @@ declare function decryptString(envelope: EncryptedEnvelope, key: Uint8Array): st
790
809
  */
791
810
  declare function parseEncryptedEnvelope(raw: unknown): EncryptedEnvelope | undefined;
792
811
 
793
- 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-7ZPLVMOI.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';
@@ -1,4 +1,4 @@
1
- import { l as OAuthConsentPages, F as FileFormat, b as OAuthFlowContext, C as CredentialStore, d as LoginOrRefreshResult, U as UserAuthConfig } from './file-format-B1KgHuUR.mjs';
1
+ import { l as OAuthConsentPages, F as FileFormat, b as OAuthFlowContext, C as CredentialStore, d as LoginOrRefreshResult, U as UserAuthConfig } from './file-format-Dbmn5hhd.mjs';
2
2
  import { ApiCredentials, ComplianceConfig } from '@cloudflare/workers-utils';
3
3
 
4
4
  /** Minimal shape of the interactive `select` prompt's options for account selection. */
@@ -42,13 +42,29 @@ interface AuthContext {
42
42
  * rather than a fork of the factory.
43
43
  */
44
44
  interface CliDescriptor {
45
- /** CLI name used in user-facing messaging and keyring install-dir scoping (e.g. `"wrangler"`). */
45
+ /**
46
+ * The CLI's invocation name, used for keyring install-dir scoping and in
47
+ * messaging that refers to the executable (e.g. `"wrangler"`, `"cf"`).
48
+ */
46
49
  cliName: string;
50
+ /**
51
+ * The CLI's branded name, used in prose addressed to the user (e.g.
52
+ * `"Wrangler"` for wrangler, `"cf"` for cf). Distinct from
53
+ * {@link CliDescriptor.cliName}, which names the executable.
54
+ */
55
+ displayName: string;
47
56
  /** Commands surfaced in auth guidance. */
48
57
  commands: {
49
58
  login: string;
50
59
  whoami: string;
51
60
  createProfile: string;
61
+ /**
62
+ * The command that restarts the OAuth 2.0 Device Authorization Grant,
63
+ * surfaced when a device code is denied, expires, or times out. Spelled
64
+ * out per CLI rather than derived from {@link CliDescriptor.commands.login}
65
+ * so the flag name stays the CLI's business.
66
+ */
67
+ deviceLogin: string;
52
68
  };
53
69
  /**
54
70
  * OS-keyring service identifier. Becomes the `-s` arg to macOS `security`,
@@ -110,6 +126,13 @@ interface CloudflareLoginProps {
110
126
  callbackHost?: string;
111
127
  callbackPort?: number;
112
128
  profile?: string;
129
+ /**
130
+ * When `true`, authenticate using the OAuth 2.0 Device Authorization Grant
131
+ * (RFC 8628) instead of the authorization-code-with-PKCE callback flow. The
132
+ * device flow does not start a local callback server, so `callbackHost` and
133
+ * `callbackPort` are ignored when this is set.
134
+ */
135
+ device?: boolean;
113
136
  }
114
137
  /** A Cloudflare CLI's auth layer, returned by {@link createCloudflareAuth}. */
115
138
  interface CloudflareAuth {