@authrim/setup 0.1.140 → 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 (122) hide show
  1. package/dist/__tests__/keys.test.js +73 -2
  2. package/dist/__tests__/keys.test.js.map +1 -1
  3. package/dist/__tests__/migrate.test.js +4 -4
  4. package/dist/__tests__/migrate.test.js.map +1 -1
  5. package/dist/__tests__/paths.test.js +163 -1
  6. package/dist/__tests__/paths.test.js.map +1 -1
  7. package/dist/__tests__/source-context.test.d.ts +2 -0
  8. package/dist/__tests__/source-context.test.d.ts.map +1 -0
  9. package/dist/__tests__/source-context.test.js +72 -0
  10. package/dist/__tests__/source-context.test.js.map +1 -0
  11. package/dist/cli/commands/deploy.d.ts.map +1 -1
  12. package/dist/cli/commands/deploy.js +65 -37
  13. package/dist/cli/commands/deploy.js.map +1 -1
  14. package/dist/cli/commands/init.d.ts.map +1 -1
  15. package/dist/cli/commands/init.js +277 -198
  16. package/dist/cli/commands/init.js.map +1 -1
  17. package/dist/core/admin.d.ts +6 -1
  18. package/dist/core/admin.d.ts.map +1 -1
  19. package/dist/core/admin.js +45 -20
  20. package/dist/core/admin.js.map +1 -1
  21. package/dist/core/cloudflare.d.ts +38 -1
  22. package/dist/core/cloudflare.d.ts.map +1 -1
  23. package/dist/core/cloudflare.js +729 -115
  24. package/dist/core/cloudflare.js.map +1 -1
  25. package/dist/core/config.d.ts +164 -34
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js +72 -18
  28. package/dist/core/config.js.map +1 -1
  29. package/dist/core/deploy.d.ts +18 -0
  30. package/dist/core/deploy.d.ts.map +1 -1
  31. package/dist/core/deploy.js +126 -25
  32. package/dist/core/deploy.js.map +1 -1
  33. package/dist/core/keys.d.ts +20 -4
  34. package/dist/core/keys.d.ts.map +1 -1
  35. package/dist/core/keys.js +77 -17
  36. package/dist/core/keys.js.map +1 -1
  37. package/dist/core/login-ui-client.d.ts +42 -0
  38. package/dist/core/login-ui-client.d.ts.map +1 -0
  39. package/dist/core/login-ui-client.js +173 -0
  40. package/dist/core/login-ui-client.js.map +1 -0
  41. package/dist/core/migrate.d.ts +37 -0
  42. package/dist/core/migrate.d.ts.map +1 -1
  43. package/dist/core/migrate.js +92 -2
  44. package/dist/core/migrate.js.map +1 -1
  45. package/dist/core/paths.d.ts +78 -13
  46. package/dist/core/paths.d.ts.map +1 -1
  47. package/dist/core/paths.js +135 -17
  48. package/dist/core/paths.js.map +1 -1
  49. package/dist/core/source-context.d.ts +22 -0
  50. package/dist/core/source-context.d.ts.map +1 -0
  51. package/dist/core/source-context.js +46 -0
  52. package/dist/core/source-context.js.map +1 -0
  53. package/dist/core/tenant-mode.d.ts +4 -0
  54. package/dist/core/tenant-mode.d.ts.map +1 -0
  55. package/dist/core/tenant-mode.js +17 -0
  56. package/dist/core/tenant-mode.js.map +1 -0
  57. package/dist/core/ui-deployment.d.ts +21 -0
  58. package/dist/core/ui-deployment.d.ts.map +1 -0
  59. package/dist/core/ui-deployment.js +90 -0
  60. package/dist/core/ui-deployment.js.map +1 -0
  61. package/dist/core/ui-env.d.ts +28 -0
  62. package/dist/core/ui-env.d.ts.map +1 -1
  63. package/dist/core/ui-env.js +16 -0
  64. package/dist/core/ui-env.js.map +1 -1
  65. package/dist/core/url-config.d.ts +16 -0
  66. package/dist/core/url-config.d.ts.map +1 -0
  67. package/dist/core/url-config.js +46 -0
  68. package/dist/core/url-config.js.map +1 -0
  69. package/dist/core/wrangler.d.ts +50 -1
  70. package/dist/core/wrangler.d.ts.map +1 -1
  71. package/dist/core/wrangler.js +171 -57
  72. package/dist/core/wrangler.js.map +1 -1
  73. package/dist/i18n/locales/de.d.ts.map +1 -1
  74. package/dist/i18n/locales/de.js +38 -1
  75. package/dist/i18n/locales/de.js.map +1 -1
  76. package/dist/i18n/locales/en.d.ts.map +1 -1
  77. package/dist/i18n/locales/en.js +38 -1
  78. package/dist/i18n/locales/en.js.map +1 -1
  79. package/dist/i18n/locales/es.d.ts.map +1 -1
  80. package/dist/i18n/locales/es.js +38 -1
  81. package/dist/i18n/locales/es.js.map +1 -1
  82. package/dist/i18n/locales/fr.d.ts.map +1 -1
  83. package/dist/i18n/locales/fr.js +38 -1
  84. package/dist/i18n/locales/fr.js.map +1 -1
  85. package/dist/i18n/locales/id.d.ts.map +1 -1
  86. package/dist/i18n/locales/id.js +38 -1
  87. package/dist/i18n/locales/id.js.map +1 -1
  88. package/dist/i18n/locales/ja.d.ts.map +1 -1
  89. package/dist/i18n/locales/ja.js +38 -1
  90. package/dist/i18n/locales/ja.js.map +1 -1
  91. package/dist/i18n/locales/ko.d.ts.map +1 -1
  92. package/dist/i18n/locales/ko.js +38 -1
  93. package/dist/i18n/locales/ko.js.map +1 -1
  94. package/dist/i18n/locales/pt.d.ts.map +1 -1
  95. package/dist/i18n/locales/pt.js +38 -1
  96. package/dist/i18n/locales/pt.js.map +1 -1
  97. package/dist/i18n/locales/ru.d.ts.map +1 -1
  98. package/dist/i18n/locales/ru.js +38 -1
  99. package/dist/i18n/locales/ru.js.map +1 -1
  100. package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
  101. package/dist/i18n/locales/zh-CN.js +38 -1
  102. package/dist/i18n/locales/zh-CN.js.map +1 -1
  103. package/dist/i18n/locales/zh-TW.d.ts.map +1 -1
  104. package/dist/i18n/locales/zh-TW.js +38 -1
  105. package/dist/i18n/locales/zh-TW.js.map +1 -1
  106. package/dist/i18n/types.d.ts +8 -0
  107. package/dist/i18n/types.d.ts.map +1 -1
  108. package/dist/index.d.ts +8 -1
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +46 -30
  111. package/dist/index.js.map +1 -1
  112. package/dist/web/api.d.ts.map +1 -1
  113. package/dist/web/api.js +243 -116
  114. package/dist/web/api.js.map +1 -1
  115. package/dist/web/ui.d.ts.map +1 -1
  116. package/dist/web/ui.js +513 -115
  117. package/dist/web/ui.js.map +1 -1
  118. package/migrations/000_fresh_schema.sql +229 -10
  119. package/migrations/admin/007_admin_role_inheritance.sql +32 -0
  120. package/migrations/admin/008_admin_rebac_definitions.sql +117 -0
  121. package/migrations/admin/009_optimize_admin_audit_indexes.sql +15 -0
  122. package/package.json +5 -5
@@ -14,11 +14,55 @@ import { createRequire } from 'node:module';
14
14
  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
- import { isWranglerInstalled, checkAuth, provisionResources, toResourceIds, getAccountId, detectEnvironments, getWorkersSubdomain, } from '../../core/cloudflare.js';
17
+ import { isRunningFromSource, getCommandPrefix } from '../../core/source-context.js';
18
+ import { isWranglerInstalled, checkAuth, provisionResources, toResourceIds, getAccountId, detectEnvironments, getWorkersSubdomain, checkZoneExists, extractZoneName, } from '../../core/cloudflare.js';
18
19
  import { createLockFile, saveLockFile, loadLockFile } from '../../core/lock.js';
19
- import { getEnvironmentPaths, getRelativeKeysPath, AUTHRIM_DIR } from '../../core/paths.js';
20
- import { downloadSource, verifySourceStructure, checkForUpdate } from '../../core/source.js';
21
- import { saveUiEnv } from '../../core/ui-env.js';
20
+ import { getEnvironmentPaths, getExternalKeysDir, getExternalKeysPathForConfig, AUTHRIM_DIR, } from '../../core/paths.js';
21
+ import { downloadSource, verifySourceStructure, checkForUpdate, getLocalVersion, } from '../../core/source.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
+ }
22
66
  // =============================================================================
23
67
  // WSL Detection
24
68
  // =============================================================================
@@ -138,21 +182,12 @@ function printBanner() {
138
182
  // Store the workers.dev subdomain for URL generation
139
183
  let workersSubdomain = null;
140
184
  /**
141
- * Get the correct workers.dev URL with account subdomain
142
- * Format: {worker}.{subdomain}.workers.dev
185
+ * Strip the protocol from a URL for display in domain-only prompts.
143
186
  */
144
- function getWorkersDevUrl(workerName) {
145
- if (workersSubdomain) {
146
- return `https://${workerName}.${workersSubdomain}.workers.dev`;
147
- }
148
- return `https://${workerName}.workers.dev`;
149
- }
150
- /**
151
- * Get the correct pages.dev URL
152
- * Note: Pages uses {project}.pages.dev format (no account subdomain, unlike Workers)
153
- */
154
- function getPagesDevUrl(projectName) {
155
- return `https://${projectName}.pages.dev`;
187
+ function stripProtocol(url) {
188
+ if (!url)
189
+ return '';
190
+ return url.replace(/^https?:\/\//, '');
156
191
  }
157
192
  // =============================================================================
158
193
  // Source Directory Detection
@@ -179,6 +214,12 @@ function isAuthrimSourceDir(dir = '.') {
179
214
  */
180
215
  async function ensureAuthrimSource(options) {
181
216
  const currentDir = resolve('.');
217
+ // If running from source repository (pnpm setup), skip update check entirely
218
+ if (isRunningFromSource(currentDir)) {
219
+ const localVersion = await getLocalVersion(currentDir);
220
+ console.log(chalk.green(`✓ Using Authrim source (v${localVersion || 'unknown'}) [from source]`));
221
+ return currentDir;
222
+ }
182
223
  // Check if we're already in an Authrim source directory
183
224
  if (isAuthrimSourceDir(currentDir)) {
184
225
  // Check for updates
@@ -538,7 +579,7 @@ export async function initCommand(options) {
538
579
  console.log(chalk.gray(t('startup.cancelled')));
539
580
  console.log('');
540
581
  console.log(chalk.gray(t('startup.resumeLater')));
541
- console.log(chalk.cyan(' npx @authrim/setup'));
582
+ console.log(chalk.cyan(` ${getCommandPrefix()}`));
542
583
  console.log('');
543
584
  return;
544
585
  }
@@ -877,7 +918,7 @@ async function runLoadConfig() {
877
918
  console.log(chalk.yellow('No configuration found in current directory.'));
878
919
  console.log('');
879
920
  console.log(chalk.gray('💡 Tip: You can specify a config file with:'));
880
- console.log(chalk.cyan(' npx @authrim/setup --config /path/to/.authrim/{env}/config.json'));
921
+ console.log(chalk.cyan(` ${getCommandPrefix()} --config /path/to/.authrim/{env}/config.json`));
881
922
  console.log('');
882
923
  const action = await select({
883
924
  message: 'What would you like to do?',
@@ -936,7 +977,7 @@ async function runQuickSetup(options) {
936
977
  console.log(` ${t('env.d1Databases', { count: String(existingEnv.d1.length) })}`);
937
978
  console.log(` ${t('env.kvNamespaces', { count: String(existingEnv.kv.length) })}`);
938
979
  console.log('');
939
- console.log(chalk.yellow(' ' + t('env.chooseAnother')));
980
+ console.log(chalk.yellow(' ' + t('env.chooseAnother', { command: getCommandPrefix() })));
940
981
  return;
941
982
  }
942
983
  checkSpinner.succeed(t('env.available'));
@@ -971,6 +1012,7 @@ async function runQuickSetup(options) {
971
1012
  let apiDomain = null;
972
1013
  let loginUiDomain = null;
973
1014
  let adminUiDomain = null;
1015
+ const quickDomainConfig = {};
974
1016
  if (useCustomDomain) {
975
1017
  console.log('');
976
1018
  console.log(chalk.gray(' ' + t('domain.singleTenantNote')));
@@ -986,6 +1028,18 @@ async function runQuickSetup(options) {
986
1028
  return true;
987
1029
  },
988
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
+ }
989
1043
  loginUiDomain = await input({
990
1044
  message: t('domain.loginUiDomain'),
991
1045
  default: '',
@@ -1105,22 +1159,15 @@ async function runQuickSetup(options) {
1105
1159
  configured: emailConfig.provider === 'resend',
1106
1160
  },
1107
1161
  };
1108
- config.urls = {
1109
- api: {
1110
- custom: apiDomain || null,
1111
- auto: getWorkersDevUrl(envPrefix + '-ar-router'),
1112
- },
1113
- loginUi: {
1114
- custom: loginUiDomain || null,
1115
- auto: getPagesDevUrl(envPrefix + '-ar-login-ui'),
1116
- sameAsApi: false,
1117
- },
1118
- adminUi: {
1119
- custom: adminUiDomain || null,
1120
- auto: getPagesDevUrl(envPrefix + '-ar-admin-ui'),
1121
- sameAsApi: false,
1122
- },
1123
- };
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
+ });
1124
1171
  // Show summary
1125
1172
  console.log('');
1126
1173
  console.log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
@@ -1158,15 +1205,20 @@ async function runQuickSetup(options) {
1158
1205
  }
1159
1206
  // Save email secrets if configured
1160
1207
  if (emailConfig.provider === 'resend' && emailConfig.apiKey) {
1161
- // Use new structure for fresh setups
1162
- const paths = getEnvironmentPaths({ baseDir: process.cwd(), env: envPrefix });
1163
- const keysDir = paths.keys;
1208
+ // Save to external keys directory: {cwd}/.authrim-keys/{env}/
1209
+ const keysDir = getExternalKeysDir(envPrefix, process.cwd());
1164
1210
  await import('node:fs/promises').then(async (fs) => {
1165
- await fs.mkdir(keysDir, { recursive: true });
1166
- await fs.writeFile(paths.keyFiles.resendApiKey, emailConfig.apiKey.trim());
1167
- await fs.writeFile(paths.keyFiles.emailFrom, emailConfig.fromAddress.trim());
1211
+ await fs.mkdir(keysDir, { recursive: true, mode: 0o700 });
1212
+ const resendApiKeyPath = join(keysDir, 'resend_api_key.txt');
1213
+ await fs.writeFile(resendApiKeyPath, emailConfig.apiKey.trim());
1214
+ await fs.chmod(resendApiKeyPath, 0o600);
1215
+ const emailFromPath = join(keysDir, 'email_from.txt');
1216
+ await fs.writeFile(emailFromPath, emailConfig.fromAddress.trim());
1217
+ await fs.chmod(emailFromPath, 0o600);
1168
1218
  if (emailConfig.fromName) {
1169
- await fs.writeFile(`${keysDir}/email_from_name.txt`, emailConfig.fromName.trim());
1219
+ const emailFromNamePath = join(keysDir, 'email_from_name.txt');
1220
+ await fs.writeFile(emailFromNamePath, emailConfig.fromName.trim());
1221
+ await fs.chmod(emailFromNamePath, 0o600);
1170
1222
  }
1171
1223
  });
1172
1224
  console.log(chalk.gray(`📧 Email secrets saved to ${keysDir}/`));
@@ -1207,7 +1259,7 @@ async function runNormalSetup(options) {
1207
1259
  console.log(` ${t('env.d1Databases', { count: String(existingEnv.d1.length) })}`);
1208
1260
  console.log(` ${t('env.kvNamespaces', { count: String(existingEnv.kv.length) })}`);
1209
1261
  console.log('');
1210
- console.log(chalk.yellow(' ' + t('env.chooseAnother')));
1262
+ console.log(chalk.yellow(' ' + t('env.chooseAnother', { command: getCommandPrefix() })));
1211
1263
  return;
1212
1264
  }
1213
1265
  checkSpinner.succeed(t('env.available'));
@@ -1266,44 +1318,58 @@ async function runNormalSetup(options) {
1266
1318
  // Step 5: Tenant configuration
1267
1319
  console.log(chalk.blue('━━━ ' + t('tenant.title') + ' ━━━'));
1268
1320
  console.log('');
1269
- const multiTenant = await confirm({
1270
- message: t('tenant.multiTenantPrompt'),
1271
- default: false,
1272
- });
1273
1321
  let tenantName = 'default';
1274
1322
  let tenantDisplayName = 'Default Tenant';
1275
1323
  let baseDomain;
1276
- // Step 6: URL configuration (depends on tenant mode)
1324
+ let primaryTenant;
1325
+ let nakedDomain = false;
1326
+ let userIdFormat = 'nanoid';
1327
+ // Step 6: URL configuration
1277
1328
  let apiDomain = null;
1278
1329
  let loginUiDomain = null;
1279
1330
  let adminUiDomain = null;
1280
- if (multiTenant) {
1281
- // Multi-tenant mode: base domain is required, becomes the issuer base
1282
- console.log('');
1283
- console.log(chalk.blue('━━━ ' + t('tenant.multiTenantTitle') + ' ━━━'));
1284
- console.log('');
1285
- console.log(chalk.gray(' ' + t('tenant.multiTenantNote1')));
1286
- console.log(chalk.gray(' ' + t('tenant.multiTenantNote2')));
1287
- console.log(chalk.gray(' • ' + t('tenant.multiTenantNote3')));
1288
- console.log(chalk.gray(' • ' + t('tenant.multiTenantNote4')));
1289
- console.log('');
1290
- baseDomain = await input({
1291
- message: t('tenant.baseDomainPrompt'),
1292
- validate: (value) => {
1293
- if (!value)
1294
- return t('tenant.baseDomainRequired');
1295
- if (!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(value)) {
1296
- return t('tenant.baseDomainValidation');
1297
- }
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)
1298
1345
  return true;
1299
- },
1300
- });
1301
- console.log('');
1302
- console.log(chalk.green(' ✓ ' + t('tenant.issuerFormat', { domain: baseDomain })));
1303
- console.log(chalk.gray(' ' + t('tenant.issuerExample', { domain: baseDomain })));
1304
- console.log('');
1305
- // API domain in multi-tenant is the base domain (or custom apex)
1306
- 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) {
1307
1373
  tenantName = await input({
1308
1374
  message: t('tenant.defaultTenantPrompt'),
1309
1375
  default: 'default',
@@ -1314,80 +1380,78 @@ async function runNormalSetup(options) {
1314
1380
  return true;
1315
1381
  },
1316
1382
  });
1317
- tenantDisplayName = await input({
1318
- message: t('tenant.displayNamePrompt'),
1319
- default: 'Default Tenant',
1320
- });
1321
- // UI domains for multi-tenant
1322
- console.log('');
1323
- console.log(chalk.blue('━━━ ' + t('tenant.uiDomainTitle') + ' ━━━'));
1324
- console.log('');
1325
- const useCustomUiDomain = await confirm({
1326
- message: t('tenant.customUiDomainPrompt'),
1327
- default: false,
1328
- });
1329
- if (useCustomUiDomain) {
1330
- loginUiDomain = await input({
1331
- message: t('tenant.loginUiDomain'),
1332
- default: '',
1333
- });
1334
- adminUiDomain = await input({
1335
- message: t('tenant.adminUiDomain'),
1336
- default: '',
1337
- });
1338
- }
1339
1383
  }
1340
1384
  else {
1341
- // Single-tenant mode
1342
- console.log('');
1343
- console.log(chalk.blue('━━━ ' + t('tenant.singleTenantTitle') + ' ━━━'));
1344
- console.log('');
1345
- console.log(chalk.gray(' ' + t('tenant.singleTenantNote1')));
1346
- console.log(chalk.gray(' • ' + t('tenant.singleTenantNote2')));
1347
- console.log(chalk.gray(' ' + t('tenant.singleTenantNote3')));
1348
- console.log('');
1349
- tenantDisplayName = await input({
1350
- message: t('tenant.organizationName'),
1351
- default: 'Default Tenant',
1352
- });
1353
- const useCustomDomain = await confirm({
1354
- 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?',
1355
1394
  default: false,
1356
1395
  });
1357
- if (useCustomDomain) {
1358
- console.log('');
1359
- console.log(chalk.gray(' ' + t('domain.enterDomains')));
1360
- console.log('');
1361
- apiDomain = await input({
1362
- 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: '',
1363
1400
  validate: (value) => {
1364
- if (!value)
1401
+ if (!value) {
1365
1402
  return true;
1366
- if (!/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/.test(value)) {
1367
- 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';
1368
1406
  }
1369
1407
  return true;
1370
1408
  },
1371
1409
  });
1372
- loginUiDomain = await input({
1373
- message: t('domain.loginUiDomain'),
1374
- default: '',
1375
- });
1376
- adminUiDomain = await input({
1377
- message: t('domain.adminUiDomain'),
1378
- default: '',
1379
- });
1380
- }
1381
- if (apiDomain) {
1382
- console.log('');
1383
- console.log(chalk.green(' ✓ ' + t('domain.issuerUrl', { url: 'https://' + apiDomain })));
1384
- }
1385
- else {
1386
- console.log('');
1387
- console.log(chalk.green(' ✓ ' + t('domain.issuerUrl', { url: getWorkersDevUrl(envPrefix + '-ar-router') })));
1388
- console.log(chalk.gray(' ' + t('domain.usingWorkersDev')));
1410
+ primaryTenant = primaryTenant || undefined;
1389
1411
  }
1390
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
+ }
1391
1455
  // Step 5: Optional components
1392
1456
  console.log('');
1393
1457
  console.log(chalk.blue('━━━ ' + t('components.title') + ' ━━━'));
@@ -1648,8 +1712,11 @@ async function runNormalSetup(options) {
1648
1712
  config.tenant = {
1649
1713
  name: tenantName,
1650
1714
  displayName: tenantDisplayName,
1651
- multiTenant,
1715
+ multiTenant: !!baseDomain,
1652
1716
  baseDomain,
1717
+ userIdFormat,
1718
+ primaryTenant,
1719
+ nakedDomain,
1653
1720
  };
1654
1721
  config.components = {
1655
1722
  ...config.components,
@@ -1659,22 +1726,15 @@ async function runNormalSetup(options) {
1659
1726
  bridge: true, // Standard component
1660
1727
  policy: true, // Standard component
1661
1728
  };
1662
- config.urls = {
1663
- api: {
1664
- custom: apiDomain || null,
1665
- auto: getWorkersDevUrl(envPrefix + '-ar-router'),
1666
- },
1667
- loginUi: {
1668
- custom: loginUiDomain || null,
1669
- auto: getPagesDevUrl(envPrefix + '-ar-login-ui'),
1670
- sameAsApi: false,
1671
- },
1672
- adminUi: {
1673
- custom: adminUiDomain || null,
1674
- auto: getPagesDevUrl(envPrefix + '-ar-admin-ui'),
1675
- sameAsApi: false,
1676
- },
1677
- };
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
+ });
1678
1738
  config.oidc = {
1679
1739
  ...config.oidc,
1680
1740
  accessTokenTtl,
@@ -1717,14 +1777,17 @@ async function runNormalSetup(options) {
1717
1777
  console.log('');
1718
1778
  // Tenant mode and Issuer
1719
1779
  console.log(chalk.bold('Tenant & Issuer:'));
1720
- console.log(` Mode: ${multiTenant ? chalk.cyan('Multi-tenant') : chalk.cyan('Single-tenant')}`);
1721
- if (multiTenant && baseDomain) {
1780
+ console.log(` Mode: ${chalk.cyan(baseDomain ? 'Multi-tenant' : 'Single-tenant')}`);
1781
+ if (baseDomain) {
1722
1782
  console.log(` Base Domain: ${chalk.cyan(baseDomain)}`);
1723
- console.log(` Issuer Format: ${chalk.cyan('https://{tenant}.' + baseDomain)}`);
1724
- 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
+ }
1725
1788
  }
1726
1789
  else {
1727
- const issuerUrl = config.urls.api.custom || config.urls.api.auto;
1790
+ const issuerUrl = config.urls?.api?.custom || config.urls?.api?.auto;
1728
1791
  console.log(` Issuer URL: ${chalk.cyan(issuerUrl)}`);
1729
1792
  }
1730
1793
  console.log(` Default Tenant: ${chalk.cyan(tenantName)}`);
@@ -1732,8 +1795,11 @@ async function runNormalSetup(options) {
1732
1795
  console.log('');
1733
1796
  // Public URLs
1734
1797
  console.log(chalk.bold('Public URLs:'));
1735
- if (multiTenant && baseDomain) {
1798
+ if (baseDomain) {
1736
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
+ }
1737
1803
  }
1738
1804
  else {
1739
1805
  console.log(` API Router: ${chalk.cyan(config.urls.api.custom || config.urls.api.auto)}`);
@@ -1797,15 +1863,20 @@ async function runNormalSetup(options) {
1797
1863
  }
1798
1864
  // Save email secrets if configured
1799
1865
  if (emailConfigNormal.provider === 'resend' && emailConfigNormal.apiKey) {
1800
- // Use new structure for fresh setups
1801
- const paths = getEnvironmentPaths({ baseDir: process.cwd(), env: envPrefix });
1802
- const keysDir = paths.keys;
1866
+ // Save to external keys directory: {cwd}/.authrim-keys/{env}/
1867
+ const keysDir = getExternalKeysDir(envPrefix, process.cwd());
1803
1868
  await import('node:fs/promises').then(async (fs) => {
1804
- await fs.mkdir(keysDir, { recursive: true });
1805
- await fs.writeFile(paths.keyFiles.resendApiKey, emailConfigNormal.apiKey.trim());
1806
- await fs.writeFile(paths.keyFiles.emailFrom, emailConfigNormal.fromAddress.trim());
1869
+ await fs.mkdir(keysDir, { recursive: true, mode: 0o700 });
1870
+ const resendApiKeyPath = join(keysDir, 'resend_api_key.txt');
1871
+ await fs.writeFile(resendApiKeyPath, emailConfigNormal.apiKey.trim());
1872
+ await fs.chmod(resendApiKeyPath, 0o600);
1873
+ const emailFromPath = join(keysDir, 'email_from.txt');
1874
+ await fs.writeFile(emailFromPath, emailConfigNormal.fromAddress.trim());
1875
+ await fs.chmod(emailFromPath, 0o600);
1807
1876
  if (emailConfigNormal.fromName) {
1808
- await fs.writeFile(`${keysDir}/email_from_name.txt`, emailConfigNormal.fromName.trim());
1877
+ const emailFromNamePath = join(keysDir, 'email_from_name.txt');
1878
+ await fs.writeFile(emailFromNamePath, emailConfigNormal.fromName.trim());
1879
+ await fs.chmod(emailFromNamePath, 0o600);
1809
1880
  }
1810
1881
  });
1811
1882
  console.log(chalk.gray(`📧 Email secrets saved to ${keysDir}/`));
@@ -1861,9 +1932,10 @@ async function executeSetup(config, cfApiToken, keepPath) {
1861
1932
  console.error(error);
1862
1933
  return;
1863
1934
  }
1864
- // Step 1: Generate keys (environment-specific directory)
1935
+ // Step 1: Generate keys (external directory: .authrim-keys/{env}/)
1865
1936
  // Check if keys already exist for this environment
1866
- if (keysExistForEnvironment(outputDir, env)) {
1937
+ const cwdDir = process.cwd();
1938
+ if (keysExistForEnvironment(outputDir, env, cwdDir)) {
1867
1939
  console.log(chalk.yellow(`⚠️ Warning: Keys already exist for environment "${env}"`));
1868
1940
  console.log(chalk.yellow(' Existing keys will be overwritten.'));
1869
1941
  console.log('');
@@ -1872,16 +1944,17 @@ async function executeSetup(config, cfApiToken, keepPath) {
1872
1944
  try {
1873
1945
  const keyId = generateKeyId(env);
1874
1946
  secrets = generateAllSecrets(keyId);
1875
- // Save to new structure: .authrim/{env}/keys/
1876
- const envPaths = getEnvironmentPaths({ baseDir: outputDir, env });
1877
- await saveKeysToDirectory(secrets, { baseDir: outputDir, env });
1947
+ // Save to external structure: {cwd}/.authrim-keys/{env}/
1948
+ const externalKeysDir = getExternalKeysDir(env, cwdDir);
1949
+ await saveKeysToDirectory(secrets, { keysBaseDir: cwdDir, env });
1878
1950
  config.keys = {
1879
1951
  keyId: secrets.keyPair.keyId,
1880
1952
  publicKeyJwk: secrets.keyPair.publicKeyJwk,
1881
- secretsPath: getRelativeKeysPath(), // './keys/' (relative from config location)
1953
+ secretsPath: getExternalKeysPathForConfig(env, cwdDir),
1882
1954
  includeSecrets: false,
1955
+ storageType: 'external',
1883
1956
  };
1884
- keysSpinner.succeed(`Keys generated (${envPaths.keys})`);
1957
+ keysSpinner.succeed(`Keys generated (${externalKeysDir})`);
1885
1958
  }
1886
1959
  catch (error) {
1887
1960
  keysSpinner.fail('Failed to generate keys');
@@ -1949,11 +2022,9 @@ async function executeSetup(config, cfApiToken, keepPath) {
1949
2022
  // Step 4.5: Generate ui.env for UI builds
1950
2023
  const uiEnvSpinner = ora('Generating UI environment file...').start();
1951
2024
  try {
1952
- const apiBaseUrl = config.urls?.api?.custom || config.urls?.api?.auto || '';
1953
- if (apiBaseUrl) {
1954
- await saveUiEnv(envPaths.uiEnv, {
1955
- PUBLIC_API_BASE_URL: apiBaseUrl,
1956
- });
2025
+ const initialUiEnv = buildInitialUiEnvConfig(config);
2026
+ if (initialUiEnv) {
2027
+ await saveUiEnv(envPaths.uiEnv, initialUiEnv);
1957
2028
  uiEnvSpinner.succeed(`UI env saved (${envPaths.uiEnv})`);
1958
2029
  }
1959
2030
  else {
@@ -2063,7 +2134,7 @@ async function executeSetup(config, cfApiToken, keepPath) {
2063
2134
  console.log(chalk.bold('📋 Next Steps:'));
2064
2135
  console.log('');
2065
2136
  console.log(' 1. Upload secrets to Cloudflare:');
2066
- console.log(chalk.cyan(` npx @authrim/setup secrets --env=${env}`));
2137
+ console.log(chalk.cyan(` ${getCommandPrefix()} secrets --env=${env}`));
2067
2138
  console.log('');
2068
2139
  console.log(' 2. Deploy Workers:');
2069
2140
  console.log(chalk.cyan(` pnpm deploy --env=${env}`));
@@ -2347,7 +2418,7 @@ async function editUrls(config) {
2347
2418
  console.log('');
2348
2419
  const apiDomain = await input({
2349
2420
  message: 'API (issuer) domain (leave empty for workers.dev)',
2350
- default: config.urls.api?.custom || '',
2421
+ default: stripProtocol(config.urls.api?.custom),
2351
2422
  validate: (value) => {
2352
2423
  if (!value)
2353
2424
  return true;
@@ -2357,28 +2428,36 @@ async function editUrls(config) {
2357
2428
  return true;
2358
2429
  },
2359
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
+ }
2360
2443
  const loginUiDomain = await input({
2361
2444
  message: 'Login UI domain (leave empty for pages.dev)',
2362
- default: config.urls.loginUi?.custom || '',
2445
+ default: stripProtocol(config.urls.loginUi?.custom),
2363
2446
  });
2364
2447
  const adminUiDomain = await input({
2365
2448
  message: 'Admin UI domain (leave empty for pages.dev)',
2366
- 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,
2367
2460
  });
2368
- config.urls.api = {
2369
- custom: apiDomain || null,
2370
- auto: config.urls.api?.auto || getWorkersDevUrl(env + '-ar-router'),
2371
- };
2372
- config.urls.loginUi = {
2373
- custom: loginUiDomain || null,
2374
- auto: config.urls.loginUi?.auto || getPagesDevUrl(env + '-ar-login-ui'),
2375
- sameAsApi: config.urls.loginUi?.sameAsApi ?? false,
2376
- };
2377
- config.urls.adminUi = {
2378
- custom: adminUiDomain || null,
2379
- auto: config.urls.adminUi?.auto || getPagesDevUrl(env + '-ar-admin-ui'),
2380
- sameAsApi: config.urls.adminUi?.sameAsApi ?? false,
2381
- };
2382
2461
  return true;
2383
2462
  }
2384
2463
  // =============================================================================