@authrim/setup 0.1.141 → 0.1.142

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 (103) hide show
  1. package/dist/__tests__/keys.test.js.map +1 -1
  2. package/dist/__tests__/migrate.test.js +4 -4
  3. package/dist/__tests__/migrate.test.js.map +1 -1
  4. package/dist/__tests__/paths.test.js.map +1 -1
  5. package/dist/cli/commands/deploy.d.ts.map +1 -1
  6. package/dist/cli/commands/deploy.js +57 -63
  7. package/dist/cli/commands/deploy.js.map +1 -1
  8. package/dist/cli/commands/init.d.ts.map +1 -1
  9. package/dist/cli/commands/init.js +231 -171
  10. package/dist/cli/commands/init.js.map +1 -1
  11. package/dist/core/admin.d.ts.map +1 -1
  12. package/dist/core/admin.js +13 -3
  13. package/dist/core/admin.js.map +1 -1
  14. package/dist/core/cloudflare.d.ts +38 -1
  15. package/dist/core/cloudflare.d.ts.map +1 -1
  16. package/dist/core/cloudflare.js +729 -115
  17. package/dist/core/cloudflare.js.map +1 -1
  18. package/dist/core/config.d.ts +136 -28
  19. package/dist/core/config.d.ts.map +1 -1
  20. package/dist/core/config.js +58 -11
  21. package/dist/core/config.js.map +1 -1
  22. package/dist/core/deploy.d.ts +18 -0
  23. package/dist/core/deploy.d.ts.map +1 -1
  24. package/dist/core/deploy.js +126 -25
  25. package/dist/core/deploy.js.map +1 -1
  26. package/dist/core/keys.d.ts.map +1 -1
  27. package/dist/core/keys.js +2 -0
  28. package/dist/core/keys.js.map +1 -1
  29. package/dist/core/login-ui-client.d.ts.map +1 -1
  30. package/dist/core/login-ui-client.js +43 -7
  31. package/dist/core/login-ui-client.js.map +1 -1
  32. package/dist/core/paths.d.ts.map +1 -1
  33. package/dist/core/paths.js +5 -5
  34. package/dist/core/paths.js.map +1 -1
  35. package/dist/core/tenant-mode.d.ts +4 -0
  36. package/dist/core/tenant-mode.d.ts.map +1 -0
  37. package/dist/core/tenant-mode.js +17 -0
  38. package/dist/core/tenant-mode.js.map +1 -0
  39. package/dist/core/ui-deployment.d.ts +21 -0
  40. package/dist/core/ui-deployment.d.ts.map +1 -0
  41. package/dist/core/ui-deployment.js +90 -0
  42. package/dist/core/ui-deployment.js.map +1 -0
  43. package/dist/core/ui-env.d.ts +17 -0
  44. package/dist/core/ui-env.d.ts.map +1 -1
  45. package/dist/core/ui-env.js +16 -0
  46. package/dist/core/ui-env.js.map +1 -1
  47. package/dist/core/url-config.d.ts +16 -0
  48. package/dist/core/url-config.d.ts.map +1 -0
  49. package/dist/core/url-config.js +46 -0
  50. package/dist/core/url-config.js.map +1 -0
  51. package/dist/core/wrangler.d.ts +50 -1
  52. package/dist/core/wrangler.d.ts.map +1 -1
  53. package/dist/core/wrangler.js +169 -55
  54. package/dist/core/wrangler.js.map +1 -1
  55. package/dist/i18n/locales/de.d.ts.map +1 -1
  56. package/dist/i18n/locales/de.js +37 -0
  57. package/dist/i18n/locales/de.js.map +1 -1
  58. package/dist/i18n/locales/en.d.ts.map +1 -1
  59. package/dist/i18n/locales/en.js +37 -0
  60. package/dist/i18n/locales/en.js.map +1 -1
  61. package/dist/i18n/locales/es.d.ts.map +1 -1
  62. package/dist/i18n/locales/es.js +37 -0
  63. package/dist/i18n/locales/es.js.map +1 -1
  64. package/dist/i18n/locales/fr.d.ts.map +1 -1
  65. package/dist/i18n/locales/fr.js +37 -0
  66. package/dist/i18n/locales/fr.js.map +1 -1
  67. package/dist/i18n/locales/id.d.ts.map +1 -1
  68. package/dist/i18n/locales/id.js +37 -0
  69. package/dist/i18n/locales/id.js.map +1 -1
  70. package/dist/i18n/locales/ja.d.ts.map +1 -1
  71. package/dist/i18n/locales/ja.js +37 -0
  72. package/dist/i18n/locales/ja.js.map +1 -1
  73. package/dist/i18n/locales/ko.d.ts.map +1 -1
  74. package/dist/i18n/locales/ko.js +37 -0
  75. package/dist/i18n/locales/ko.js.map +1 -1
  76. package/dist/i18n/locales/pt.d.ts.map +1 -1
  77. package/dist/i18n/locales/pt.js +37 -0
  78. package/dist/i18n/locales/pt.js.map +1 -1
  79. package/dist/i18n/locales/ru.d.ts.map +1 -1
  80. package/dist/i18n/locales/ru.js +37 -0
  81. package/dist/i18n/locales/ru.js.map +1 -1
  82. package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
  83. package/dist/i18n/locales/zh-CN.js +37 -0
  84. package/dist/i18n/locales/zh-CN.js.map +1 -1
  85. package/dist/i18n/locales/zh-TW.d.ts.map +1 -1
  86. package/dist/i18n/locales/zh-TW.js +37 -0
  87. package/dist/i18n/locales/zh-TW.js.map +1 -1
  88. package/dist/i18n/types.d.ts +8 -0
  89. package/dist/i18n/types.d.ts.map +1 -1
  90. package/dist/index.js +38 -29
  91. package/dist/index.js.map +1 -1
  92. package/dist/web/api.d.ts.map +1 -1
  93. package/dist/web/api.js +207 -95
  94. package/dist/web/api.js.map +1 -1
  95. package/dist/web/ui.d.ts.map +1 -1
  96. package/dist/web/ui.js +506 -109
  97. package/dist/web/ui.js.map +1 -1
  98. package/migrations/000_fresh_schema.sql +227 -9
  99. package/migrations/admin/006_admin_setup_tokens.sql +91 -91
  100. package/migrations/admin/007_admin_role_inheritance.sql +32 -0
  101. package/migrations/admin/008_admin_rebac_definitions.sql +117 -0
  102. package/migrations/admin/009_optimize_admin_audit_indexes.sql +15 -0
  103. package/package.json +5 -5
@@ -15,11 +15,54 @@ import { execa } from 'execa';
15
15
  import { createDefaultConfig, parseConfig } from '../../core/config.js';
16
16
  import { generateAllSecrets, saveKeysToDirectory, generateKeyId, keysExistForEnvironment, } from '../../core/keys.js';
17
17
  import { isRunningFromSource, getCommandPrefix } from '../../core/source-context.js';
18
- import { isWranglerInstalled, checkAuth, provisionResources, toResourceIds, getAccountId, detectEnvironments, getWorkersSubdomain, } from '../../core/cloudflare.js';
18
+ import { isWranglerInstalled, checkAuth, provisionResources, toResourceIds, getAccountId, detectEnvironments, getWorkersSubdomain, checkZoneExists, extractZoneName, } from '../../core/cloudflare.js';
19
19
  import { createLockFile, saveLockFile, loadLockFile } from '../../core/lock.js';
20
- import { getEnvironmentPaths, getExternalKeysDir, getExternalKeysPathForConfig, AUTHRIM_DIR } from '../../core/paths.js';
20
+ import { getEnvironmentPaths, getExternalKeysDir, getExternalKeysPathForConfig, AUTHRIM_DIR, } from '../../core/paths.js';
21
21
  import { downloadSource, verifySourceStructure, checkForUpdate, getLocalVersion, } from '../../core/source.js';
22
- import { saveUiEnv } from '../../core/ui-env.js';
22
+ import { saveUiEnv, buildInitialUiEnvConfig } from '../../core/ui-env.js';
23
+ import { buildUrlsConfig, getPagesDevUrl, getWorkersDevUrl, } from '../../core/url-config.js';
24
+ /**
25
+ * Check Cloudflare zone for a domain and prompt user for binding configuration.
26
+ * Never blocks setup - all errors are handled gracefully.
27
+ */
28
+ async function checkAndPromptZone(domain, domainConfig) {
29
+ const spinner = ora(t('domain.checkingZone', { domain })).start();
30
+ try {
31
+ const result = await checkZoneExists(domain);
32
+ spinner.stop();
33
+ if (result.error) {
34
+ console.log(chalk.yellow(` ⚠ ${t('domain.zoneCheckFailed')}: ${result.error}`));
35
+ console.log(chalk.gray(` ${t('domain.zoneCheckSkipped')}`));
36
+ return;
37
+ }
38
+ if (!result.found) {
39
+ const zoneName = extractZoneName(domain);
40
+ console.log(chalk.yellow(` ⚠ ${t('domain.zoneNotFound', { zone: zoneName })}`));
41
+ console.log(chalk.gray(` ${t('domain.zoneNotFoundHint')}`));
42
+ console.log('');
43
+ const ok = await confirm({ message: t('domain.continueWithoutZone'), default: true });
44
+ if (!ok) {
45
+ throw new Error('USER_CANCELLED_DOMAIN');
46
+ }
47
+ return;
48
+ }
49
+ // Zone found
50
+ console.log(chalk.green(` ✓ ${t('domain.zoneFound', { zone: result.zone.name, status: result.zone.status })}`));
51
+ domainConfig.zoneId = result.zone.id;
52
+ console.log('');
53
+ const bind = await confirm({ message: t('domain.configureBinding'), default: true });
54
+ domainConfig.customDomainBinding = bind;
55
+ }
56
+ catch (error) {
57
+ spinner.stop();
58
+ if (error instanceof Error && error.message === 'USER_CANCELLED_DOMAIN') {
59
+ throw error;
60
+ }
61
+ // Unexpected error - don't block setup
62
+ console.log(chalk.yellow(` ⚠ ${t('domain.zoneCheckFailed')}`));
63
+ console.log(chalk.gray(` ${t('domain.zoneCheckSkipped')}`));
64
+ }
65
+ }
23
66
  // =============================================================================
24
67
  // WSL Detection
25
68
  // =============================================================================
@@ -139,21 +182,12 @@ function printBanner() {
139
182
  // Store the workers.dev subdomain for URL generation
140
183
  let workersSubdomain = null;
141
184
  /**
142
- * Get the correct workers.dev URL with account subdomain
143
- * Format: {worker}.{subdomain}.workers.dev
185
+ * Strip the protocol from a URL for display in domain-only prompts.
144
186
  */
145
- function getWorkersDevUrl(workerName) {
146
- if (workersSubdomain) {
147
- return `https://${workerName}.${workersSubdomain}.workers.dev`;
148
- }
149
- return `https://${workerName}.workers.dev`;
150
- }
151
- /**
152
- * Get the correct pages.dev URL
153
- * Note: Pages uses {project}.pages.dev format (no account subdomain, unlike Workers)
154
- */
155
- function getPagesDevUrl(projectName) {
156
- return `https://${projectName}.pages.dev`;
187
+ function stripProtocol(url) {
188
+ if (!url)
189
+ return '';
190
+ return url.replace(/^https?:\/\//, '');
157
191
  }
158
192
  // =============================================================================
159
193
  // Source Directory Detection
@@ -978,6 +1012,7 @@ async function runQuickSetup(options) {
978
1012
  let apiDomain = null;
979
1013
  let loginUiDomain = null;
980
1014
  let adminUiDomain = null;
1015
+ const quickDomainConfig = {};
981
1016
  if (useCustomDomain) {
982
1017
  console.log('');
983
1018
  console.log(chalk.gray(' ' + t('domain.singleTenantNote')));
@@ -993,6 +1028,18 @@ async function runQuickSetup(options) {
993
1028
  return true;
994
1029
  },
995
1030
  });
1031
+ // Check Cloudflare zone for the domain
1032
+ if (apiDomain) {
1033
+ console.log('');
1034
+ try {
1035
+ await checkAndPromptZone(apiDomain, quickDomainConfig);
1036
+ }
1037
+ catch {
1038
+ // User cancelled - clear domain and continue
1039
+ apiDomain = null;
1040
+ }
1041
+ console.log('');
1042
+ }
996
1043
  loginUiDomain = await input({
997
1044
  message: t('domain.loginUiDomain'),
998
1045
  default: '',
@@ -1112,22 +1159,15 @@ async function runQuickSetup(options) {
1112
1159
  configured: emailConfig.provider === 'resend',
1113
1160
  },
1114
1161
  };
1115
- config.urls = {
1116
- api: {
1117
- custom: apiDomain || null,
1118
- auto: getWorkersDevUrl(envPrefix + '-ar-router'),
1119
- },
1120
- loginUi: {
1121
- custom: loginUiDomain || null,
1122
- auto: getPagesDevUrl(envPrefix + '-ar-login-ui'),
1123
- sameAsApi: false,
1124
- },
1125
- adminUi: {
1126
- custom: adminUiDomain || null,
1127
- auto: getPagesDevUrl(envPrefix + '-ar-admin-ui'),
1128
- sameAsApi: false,
1129
- },
1130
- };
1162
+ config.urls = buildUrlsConfig({
1163
+ env: envPrefix,
1164
+ apiDomain,
1165
+ loginUiDomain,
1166
+ adminUiDomain,
1167
+ zoneId: quickDomainConfig.zoneId ?? null,
1168
+ customDomainBinding: quickDomainConfig.customDomainBinding ?? false,
1169
+ workersSubdomain,
1170
+ });
1131
1171
  // Show summary
1132
1172
  console.log('');
1133
1173
  console.log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
@@ -1278,44 +1318,58 @@ async function runNormalSetup(options) {
1278
1318
  // Step 5: Tenant configuration
1279
1319
  console.log(chalk.blue('━━━ ' + t('tenant.title') + ' ━━━'));
1280
1320
  console.log('');
1281
- const multiTenant = await confirm({
1282
- message: t('tenant.multiTenantPrompt'),
1283
- default: false,
1284
- });
1285
1321
  let tenantName = 'default';
1286
1322
  let tenantDisplayName = 'Default Tenant';
1287
1323
  let baseDomain;
1288
- // Step 6: URL configuration (depends on tenant mode)
1324
+ let primaryTenant;
1325
+ let nakedDomain = false;
1326
+ let userIdFormat = 'nanoid';
1327
+ // Step 6: URL configuration
1289
1328
  let apiDomain = null;
1290
1329
  let loginUiDomain = null;
1291
1330
  let adminUiDomain = null;
1292
- if (multiTenant) {
1293
- // Multi-tenant mode: base domain is required, becomes the issuer base
1294
- console.log('');
1295
- console.log(chalk.blue('━━━ ' + t('tenant.multiTenantTitle') + ' ━━━'));
1296
- console.log('');
1297
- console.log(chalk.gray(' ' + t('tenant.multiTenantNote1')));
1298
- console.log(chalk.gray(' ' + t('tenant.multiTenantNote2')));
1299
- console.log(chalk.gray(' • ' + t('tenant.multiTenantNote3')));
1300
- console.log(chalk.gray(' • ' + t('tenant.multiTenantNote4')));
1301
- console.log('');
1302
- baseDomain = await input({
1303
- message: t('tenant.baseDomainPrompt'),
1304
- validate: (value) => {
1305
- if (!value)
1306
- return t('tenant.baseDomainRequired');
1307
- if (!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(value)) {
1308
- return t('tenant.baseDomainValidation');
1309
- }
1331
+ const fullDomainConfig = {};
1332
+ // Base domain configuration
1333
+ console.log('');
1334
+ console.log(chalk.blue('━━━ ' + t('tenant.multiTenantTitle') + ' ━━━'));
1335
+ console.log('');
1336
+ console.log(chalk.gray(' Leave empty to use workers.dev and single-tenant mode.'));
1337
+ console.log(chalk.gray(' With a custom domain:'));
1338
+ console.log(chalk.gray(' • https://example.com (naked domain issuer)'));
1339
+ console.log(chalk.gray(' • https://acme.example.com (tenant subdomain issuer)'));
1340
+ console.log('');
1341
+ baseDomain = await input({
1342
+ message: t('tenant.baseDomainPrompt'),
1343
+ validate: (value) => {
1344
+ if (!value)
1310
1345
  return true;
1311
- },
1312
- });
1313
- console.log('');
1314
- console.log(chalk.green(' ✓ ' + t('tenant.issuerFormat', { domain: baseDomain })));
1315
- console.log(chalk.gray(' ' + t('tenant.issuerExample', { domain: baseDomain })));
1316
- console.log('');
1317
- // API domain in multi-tenant is the base domain (or custom apex)
1318
- apiDomain = baseDomain;
1346
+ if (!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(value)) {
1347
+ return t('tenant.baseDomainValidation');
1348
+ }
1349
+ return true;
1350
+ },
1351
+ });
1352
+ baseDomain = baseDomain || undefined;
1353
+ console.log('');
1354
+ if (baseDomain) {
1355
+ console.log(chalk.green(' ✓ Base domain: ' + baseDomain));
1356
+ }
1357
+ else {
1358
+ console.log(chalk.green(' ✓ Using workers.dev (single-tenant mode)'));
1359
+ }
1360
+ // Check Cloudflare zone for the base domain
1361
+ if (baseDomain) {
1362
+ try {
1363
+ await checkAndPromptZone(baseDomain, fullDomainConfig);
1364
+ }
1365
+ catch {
1366
+ // User cancelled - this is non-fatal, continue with setup
1367
+ }
1368
+ }
1369
+ console.log('');
1370
+ // API domain is the base domain
1371
+ apiDomain = baseDomain || null;
1372
+ if (baseDomain) {
1319
1373
  tenantName = await input({
1320
1374
  message: t('tenant.defaultTenantPrompt'),
1321
1375
  default: 'default',
@@ -1326,80 +1380,78 @@ async function runNormalSetup(options) {
1326
1380
  return true;
1327
1381
  },
1328
1382
  });
1329
- tenantDisplayName = await input({
1330
- message: t('tenant.displayNamePrompt'),
1331
- default: 'Default Tenant',
1332
- });
1333
- // UI domains for multi-tenant
1334
- console.log('');
1335
- console.log(chalk.blue('━━━ ' + t('tenant.uiDomainTitle') + ' ━━━'));
1336
- console.log('');
1337
- const useCustomUiDomain = await confirm({
1338
- message: t('tenant.customUiDomainPrompt'),
1339
- default: false,
1340
- });
1341
- if (useCustomUiDomain) {
1342
- loginUiDomain = await input({
1343
- message: t('tenant.loginUiDomain'),
1344
- default: '',
1345
- });
1346
- adminUiDomain = await input({
1347
- message: t('tenant.adminUiDomain'),
1348
- default: '',
1349
- });
1350
- }
1351
1383
  }
1352
1384
  else {
1353
- // Single-tenant mode
1354
- console.log('');
1355
- console.log(chalk.blue('━━━ ' + t('tenant.singleTenantTitle') + ' ━━━'));
1356
- console.log('');
1357
- console.log(chalk.gray(' ' + t('tenant.singleTenantNote1')));
1358
- console.log(chalk.gray(' • ' + t('tenant.singleTenantNote2')));
1359
- console.log(chalk.gray(' ' + t('tenant.singleTenantNote3')));
1360
- console.log('');
1361
- tenantDisplayName = await input({
1362
- message: t('tenant.organizationName'),
1363
- default: 'Default Tenant',
1364
- });
1365
- const useCustomDomain = await confirm({
1366
- message: t('domain.prompt'),
1385
+ tenantName = 'default';
1386
+ }
1387
+ tenantDisplayName = await input({
1388
+ message: t('tenant.displayNamePrompt'),
1389
+ default: 'Default Tenant',
1390
+ });
1391
+ if (baseDomain) {
1392
+ nakedDomain = await confirm({
1393
+ message: 'Use naked domain as the issuer for the primary tenant?',
1367
1394
  default: false,
1368
1395
  });
1369
- if (useCustomDomain) {
1370
- console.log('');
1371
- console.log(chalk.gray(' ' + t('domain.enterDomains')));
1372
- console.log('');
1373
- apiDomain = await input({
1374
- message: t('domain.apiDomain'),
1396
+ if (nakedDomain) {
1397
+ primaryTenant = await input({
1398
+ message: 'Primary tenant ID for naked domain (leave empty for default tenant)',
1399
+ default: '',
1375
1400
  validate: (value) => {
1376
- if (!value)
1401
+ if (!value) {
1377
1402
  return true;
1378
- if (!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(value)) {
1379
- return t('domain.customValidation');
1403
+ }
1404
+ if (!/^[a-z][a-z0-9-]*$/.test(value)) {
1405
+ return 'Tenant ID must start with a letter and contain only lowercase letters, numbers, and hyphens';
1380
1406
  }
1381
1407
  return true;
1382
1408
  },
1383
1409
  });
1384
- loginUiDomain = await input({
1385
- message: t('domain.loginUiDomain'),
1386
- default: '',
1387
- });
1388
- adminUiDomain = await input({
1389
- message: t('domain.adminUiDomain'),
1390
- default: '',
1391
- });
1392
- }
1393
- if (apiDomain) {
1394
- console.log('');
1395
- console.log(chalk.green(' ✓ ' + t('domain.issuerUrl', { url: 'https://' + apiDomain })));
1396
- }
1397
- else {
1398
- console.log('');
1399
- console.log(chalk.green(' ✓ ' + t('domain.issuerUrl', { url: getWorkersDevUrl(envPrefix + '-ar-router') })));
1400
- console.log(chalk.gray(' ' + t('domain.usingWorkersDev')));
1410
+ primaryTenant = primaryTenant || undefined;
1401
1411
  }
1402
1412
  }
1413
+ // User ID format selection
1414
+ console.log('');
1415
+ console.log(chalk.blue('━━━ ' + t('userId.title') + ' ━━━'));
1416
+ console.log('');
1417
+ console.log(chalk.gray(' ' + t('userId.note')));
1418
+ console.log('');
1419
+ userIdFormat = await select({
1420
+ message: t('userId.prompt'),
1421
+ choices: [
1422
+ {
1423
+ name: t('userId.nanoid'),
1424
+ value: 'nanoid',
1425
+ description: t('userId.nanoidDesc'),
1426
+ },
1427
+ {
1428
+ name: t('userId.uuid'),
1429
+ value: 'uuid',
1430
+ description: t('userId.uuidDesc'),
1431
+ },
1432
+ ],
1433
+ default: 'nanoid',
1434
+ });
1435
+ console.log('');
1436
+ console.log(chalk.green(' ✓ ' + t('userId.selected', { format: userIdFormat })));
1437
+ // UI domains
1438
+ console.log('');
1439
+ console.log(chalk.blue('━━━ ' + t('tenant.uiDomainTitle') + ' ━━━'));
1440
+ console.log('');
1441
+ const useCustomUiDomain = await confirm({
1442
+ message: t('tenant.customUiDomainPrompt'),
1443
+ default: false,
1444
+ });
1445
+ if (useCustomUiDomain) {
1446
+ loginUiDomain = await input({
1447
+ message: t('tenant.loginUiDomain'),
1448
+ default: '',
1449
+ });
1450
+ adminUiDomain = await input({
1451
+ message: t('tenant.adminUiDomain'),
1452
+ default: '',
1453
+ });
1454
+ }
1403
1455
  // Step 5: Optional components
1404
1456
  console.log('');
1405
1457
  console.log(chalk.blue('━━━ ' + t('components.title') + ' ━━━'));
@@ -1660,8 +1712,11 @@ async function runNormalSetup(options) {
1660
1712
  config.tenant = {
1661
1713
  name: tenantName,
1662
1714
  displayName: tenantDisplayName,
1663
- multiTenant,
1715
+ multiTenant: !!baseDomain,
1664
1716
  baseDomain,
1717
+ userIdFormat,
1718
+ primaryTenant,
1719
+ nakedDomain,
1665
1720
  };
1666
1721
  config.components = {
1667
1722
  ...config.components,
@@ -1671,22 +1726,15 @@ async function runNormalSetup(options) {
1671
1726
  bridge: true, // Standard component
1672
1727
  policy: true, // Standard component
1673
1728
  };
1674
- config.urls = {
1675
- api: {
1676
- custom: apiDomain || null,
1677
- auto: getWorkersDevUrl(envPrefix + '-ar-router'),
1678
- },
1679
- loginUi: {
1680
- custom: loginUiDomain || null,
1681
- auto: getPagesDevUrl(envPrefix + '-ar-login-ui'),
1682
- sameAsApi: false,
1683
- },
1684
- adminUi: {
1685
- custom: adminUiDomain || null,
1686
- auto: getPagesDevUrl(envPrefix + '-ar-admin-ui'),
1687
- sameAsApi: false,
1688
- },
1689
- };
1729
+ config.urls = buildUrlsConfig({
1730
+ env: envPrefix,
1731
+ apiDomain,
1732
+ loginUiDomain,
1733
+ adminUiDomain,
1734
+ zoneId: fullDomainConfig.zoneId ?? null,
1735
+ customDomainBinding: fullDomainConfig.customDomainBinding ?? false,
1736
+ workersSubdomain,
1737
+ });
1690
1738
  config.oidc = {
1691
1739
  ...config.oidc,
1692
1740
  accessTokenTtl,
@@ -1729,14 +1777,17 @@ async function runNormalSetup(options) {
1729
1777
  console.log('');
1730
1778
  // Tenant mode and Issuer
1731
1779
  console.log(chalk.bold('Tenant & Issuer:'));
1732
- console.log(` Mode: ${multiTenant ? chalk.cyan('Multi-tenant') : chalk.cyan('Single-tenant')}`);
1733
- if (multiTenant && baseDomain) {
1780
+ console.log(` Mode: ${chalk.cyan(baseDomain ? 'Multi-tenant' : 'Single-tenant')}`);
1781
+ if (baseDomain) {
1734
1782
  console.log(` Base Domain: ${chalk.cyan(baseDomain)}`);
1735
- console.log(` Issuer Format: ${chalk.cyan('https://{tenant}.' + baseDomain)}`);
1736
- console.log(` Example: ${chalk.gray('https://acme.' + baseDomain)}`);
1783
+ console.log(` Domain Pattern: ${chalk.cyan('{tenant}.' + baseDomain)}`);
1784
+ console.log(` Example: ${chalk.gray(nakedDomain ? 'https://' + baseDomain : 'https://acme.' + baseDomain)}`);
1785
+ if (nakedDomain) {
1786
+ console.log(` Naked Domain: ${chalk.cyan(primaryTenant || tenantName)} ${chalk.gray('(primary tenant issuer)')}`);
1787
+ }
1737
1788
  }
1738
1789
  else {
1739
- const issuerUrl = config.urls.api.custom || config.urls.api.auto;
1790
+ const issuerUrl = config.urls?.api?.custom || config.urls?.api?.auto;
1740
1791
  console.log(` Issuer URL: ${chalk.cyan(issuerUrl)}`);
1741
1792
  }
1742
1793
  console.log(` Default Tenant: ${chalk.cyan(tenantName)}`);
@@ -1744,8 +1795,11 @@ async function runNormalSetup(options) {
1744
1795
  console.log('');
1745
1796
  // Public URLs
1746
1797
  console.log(chalk.bold('Public URLs:'));
1747
- if (multiTenant && baseDomain) {
1798
+ if (baseDomain) {
1748
1799
  console.log(` API Router: ${chalk.cyan('*.' + baseDomain)} → ${chalk.gray(envPrefix + '-ar-router')}`);
1800
+ if (nakedDomain) {
1801
+ console.log(` API Router: ${chalk.cyan(baseDomain + ' (naked)')} → ${chalk.gray(envPrefix + '-ar-router')}`);
1802
+ }
1749
1803
  }
1750
1804
  else {
1751
1805
  console.log(` API Router: ${chalk.cyan(config.urls.api.custom || config.urls.api.auto)}`);
@@ -1968,11 +2022,9 @@ async function executeSetup(config, cfApiToken, keepPath) {
1968
2022
  // Step 4.5: Generate ui.env for UI builds
1969
2023
  const uiEnvSpinner = ora('Generating UI environment file...').start();
1970
2024
  try {
1971
- const apiBaseUrl = config.urls?.api?.custom || config.urls?.api?.auto || '';
1972
- if (apiBaseUrl) {
1973
- await saveUiEnv(envPaths.uiEnv, {
1974
- PUBLIC_API_BASE_URL: apiBaseUrl,
1975
- });
2025
+ const initialUiEnv = buildInitialUiEnvConfig(config);
2026
+ if (initialUiEnv) {
2027
+ await saveUiEnv(envPaths.uiEnv, initialUiEnv);
1976
2028
  uiEnvSpinner.succeed(`UI env saved (${envPaths.uiEnv})`);
1977
2029
  }
1978
2030
  else {
@@ -2366,7 +2418,7 @@ async function editUrls(config) {
2366
2418
  console.log('');
2367
2419
  const apiDomain = await input({
2368
2420
  message: 'API (issuer) domain (leave empty for workers.dev)',
2369
- default: config.urls.api?.custom || '',
2421
+ default: stripProtocol(config.urls.api?.custom),
2370
2422
  validate: (value) => {
2371
2423
  if (!value)
2372
2424
  return true;
@@ -2376,28 +2428,36 @@ async function editUrls(config) {
2376
2428
  return true;
2377
2429
  },
2378
2430
  });
2431
+ // Check Cloudflare zone for the domain
2432
+ const updateDomainConfig = {};
2433
+ if (apiDomain) {
2434
+ console.log('');
2435
+ try {
2436
+ await checkAndPromptZone(apiDomain, updateDomainConfig);
2437
+ }
2438
+ catch {
2439
+ // User cancelled - non-fatal
2440
+ }
2441
+ console.log('');
2442
+ }
2379
2443
  const loginUiDomain = await input({
2380
2444
  message: 'Login UI domain (leave empty for pages.dev)',
2381
- default: config.urls.loginUi?.custom || '',
2445
+ default: stripProtocol(config.urls.loginUi?.custom),
2382
2446
  });
2383
2447
  const adminUiDomain = await input({
2384
2448
  message: 'Admin UI domain (leave empty for pages.dev)',
2385
- default: config.urls.adminUi?.custom || '',
2449
+ default: stripProtocol(config.urls.adminUi?.custom),
2450
+ });
2451
+ config.urls = buildUrlsConfig({
2452
+ env,
2453
+ apiDomain,
2454
+ loginUiDomain,
2455
+ adminUiDomain,
2456
+ zoneId: updateDomainConfig.zoneId,
2457
+ customDomainBinding: updateDomainConfig.customDomainBinding,
2458
+ workersSubdomain,
2459
+ existingUrls: config.urls,
2386
2460
  });
2387
- config.urls.api = {
2388
- custom: apiDomain || null,
2389
- auto: config.urls.api?.auto || getWorkersDevUrl(env + '-ar-router'),
2390
- };
2391
- config.urls.loginUi = {
2392
- custom: loginUiDomain || null,
2393
- auto: config.urls.loginUi?.auto || getPagesDevUrl(env + '-ar-login-ui'),
2394
- sameAsApi: config.urls.loginUi?.sameAsApi ?? false,
2395
- };
2396
- config.urls.adminUi = {
2397
- custom: adminUiDomain || null,
2398
- auto: config.urls.adminUi?.auto || getPagesDevUrl(env + '-ar-admin-ui'),
2399
- sameAsApi: config.urls.adminUi?.sameAsApi ?? false,
2400
- };
2401
2461
  return true;
2402
2462
  }
2403
2463
  // =============================================================================