@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
package/dist/web/api.js CHANGED
@@ -18,15 +18,19 @@ import { isWranglerInstalled, checkAuth, provisionResources, detectEnvironments,
18
18
  import { AuthrimConfigSchema, createDefaultConfig, D1LocationSchema, D1JurisdictionSchema, } from '../core/config.js';
19
19
  import { generateAllSecrets, saveKeysToDirectory, keysExistForEnvironment } from '../core/keys.js';
20
20
  import { createLockFile, saveLockFile } from '../core/lock.js';
21
- import { getEnvironmentPaths, resolvePaths, listEnvironments, findAuthrimBaseDir, } from '../core/paths.js';
21
+ import { getEnvironmentPaths, getExternalKeysDir, findKeysDirectory, resolvePaths, listEnvironments, findAuthrimBaseDir, } from '../core/paths.js';
22
22
  import { generateWranglerConfig, toToml } from '../core/wrangler.js';
23
23
  import { syncWranglerConfigs } from '../core/wrangler-sync.js';
24
+ import { buildUrlsConfig } from '../core/url-config.js';
25
+ import { normalizeTenantConfigForApiDomain } from '../core/tenant-mode.js';
24
26
  import { deployAll, uploadSecrets, buildApiPackages, deployAllPages, deployPagesComponent, deployWorker, PAGES_COMPONENTS, } from '../core/deploy.js';
25
27
  import { getEnabledComponents, WORKER_COMPONENTS } from '../core/naming.js';
26
28
  import { getLocalPackageVersions, compareVersions, getComponentsToUpdate, } from '../core/version.js';
27
29
  import { completeInitialSetup } from '../core/admin.js';
28
- import { writeFile } from 'node:fs/promises';
29
- import { join } from 'node:path';
30
+ import { resolveUiDeploymentSettings } from '../core/ui-deployment.js';
31
+ import { saveUiEnv, buildInitialUiEnvConfig } from '../core/ui-env.js';
32
+ import { writeFile, chmod, mkdir } from 'node:fs/promises';
33
+ import { join, dirname } from 'node:path';
30
34
  // =============================================================================
31
35
  // Session & Security
32
36
  // =============================================================================
@@ -117,6 +121,29 @@ export function createApiRoutes() {
117
121
  api.use('/deploy', validateSession);
118
122
  api.use('/reset', validateSession);
119
123
  api.use('/admin/*', validateSession);
124
+ api.use('/cloudflare/*', validateSession);
125
+ // ==========================================================================
126
+ // Cloudflare Zone Check
127
+ // ==========================================================================
128
+ api.post('/cloudflare/check-zone', async (c) => {
129
+ try {
130
+ const body = (await c.req.json());
131
+ const { domain } = body;
132
+ if (!domain || typeof domain !== 'string') {
133
+ return c.json({ found: false, error: 'domain is required' }, 400);
134
+ }
135
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/.test(domain)) {
136
+ return c.json({ found: false, error: 'Invalid domain format' }, 400);
137
+ }
138
+ const { checkZoneExists } = await import('../core/cloudflare.js');
139
+ const result = await checkZoneExists(domain);
140
+ return c.json(result);
141
+ }
142
+ catch (error) {
143
+ const message = error instanceof Error ? error.message : 'Unknown error';
144
+ return c.json({ found: false, error: message }, 500);
145
+ }
146
+ });
120
147
  // Get current state (no auth required - read-only)
121
148
  api.get('/state', (c) => {
122
149
  return c.json(state);
@@ -248,10 +275,13 @@ export function createApiRoutes() {
248
275
  // Use new structure for saving
249
276
  const envPaths = getEnvironmentPaths({ baseDir, env });
250
277
  // Ensure directory exists
251
- const { mkdir } = await import('node:fs/promises');
252
278
  await mkdir(envPaths.root, { recursive: true });
253
279
  // Save config
254
280
  await writeFile(envPaths.config, JSON.stringify(config, null, 2));
281
+ const initialUiEnv = buildInitialUiEnvConfig(config);
282
+ if (initialUiEnv) {
283
+ await saveUiEnv(envPaths.uiEnv, initialUiEnv);
284
+ }
255
285
  state.config = config;
256
286
  return c.json({ success: true, configPath: envPaths.config, structure: 'new' });
257
287
  }
@@ -268,34 +298,21 @@ export function createApiRoutes() {
268
298
  return withLock(async () => {
269
299
  try {
270
300
  const body = await c.req.json();
271
- const { env = 'prod', apiDomain, loginUiDomain, adminUiDomain, tenant, components } = body;
301
+ const { env = 'prod', apiDomain, loginUiDomain, adminUiDomain, tenant, components, zoneId, customDomainBinding, } = body;
272
302
  const config = createDefaultConfig(env);
273
303
  // Update tenant configuration
274
304
  if (tenant) {
275
- config.tenant = {
276
- name: tenant.name || 'default',
277
- displayName: tenant.displayName || 'Default Tenant',
278
- multiTenant: tenant.multiTenant || false,
279
- baseDomain: tenant.baseDomain,
280
- };
305
+ config.tenant = normalizeTenantConfigForApiDomain(tenant);
281
306
  }
282
307
  // Update URLs with domain configuration
283
- config.urls = {
284
- api: {
285
- custom: apiDomain || null,
286
- auto: `https://${env}-ar-router.workers.dev`,
287
- },
288
- loginUi: {
289
- custom: loginUiDomain || null,
290
- auto: `https://${env}-ar-login-ui.pages.dev`,
291
- sameAsApi: false,
292
- },
293
- adminUi: {
294
- custom: adminUiDomain || null,
295
- auto: `https://${env}-ar-admin-ui.pages.dev`,
296
- sameAsApi: false,
297
- },
298
- };
308
+ config.urls = buildUrlsConfig({
309
+ env,
310
+ apiDomain,
311
+ loginUiDomain,
312
+ adminUiDomain,
313
+ zoneId: zoneId || null,
314
+ customDomainBinding: customDomainBinding ?? false,
315
+ });
299
316
  // Update components if provided
300
317
  if (components) {
301
318
  config.components = {
@@ -315,7 +332,8 @@ export function createApiRoutes() {
315
332
  api.get('/keys/check/:env', async (c) => {
316
333
  try {
317
334
  const env = c.req.param('env');
318
- const exists = keysExistForEnvironment(process.cwd(), env);
335
+ const baseDir = findAuthrimBaseDir(process.cwd());
336
+ const exists = keysExistForEnvironment(baseDir, env, process.cwd());
319
337
  return c.json({ exists, env });
320
338
  }
321
339
  catch (error) {
@@ -323,26 +341,27 @@ export function createApiRoutes() {
323
341
  }
324
342
  });
325
343
  // Generate keys (with lock)
326
- // Saves to new structure: .authrim/{env}/keys/
344
+ // Saves to external structure: {cwd}/.authrim-keys/{env}/
327
345
  api.post('/keys/generate', async (c) => {
328
346
  return withLock(async () => {
329
347
  try {
330
348
  const body = await c.req.json();
331
349
  const { keyId, env } = body;
332
- const baseDir = findAuthrimBaseDir(process.cwd());
350
+ const envName = env || 'default';
351
+ const keysBaseDir = process.cwd();
333
352
  addProgress('Generating cryptographic keys...');
334
353
  const secrets = generateAllSecrets(keyId);
335
- // Use new structure for saving keys
336
- const envPaths = getEnvironmentPaths({ baseDir, env: env || 'default' });
337
- addProgress(`Saving keys to directory: ${envPaths.keys}/`);
338
- await saveKeysToDirectory(secrets, { baseDir, env: env || 'default' });
354
+ // Save to external keys directory: {cwd}/.authrim-keys/{env}/
355
+ const externalKeysDir = getExternalKeysDir(envName, keysBaseDir);
356
+ addProgress(`Saving keys to directory: ${externalKeysDir}/`);
357
+ await saveKeysToDirectory(secrets, { keysBaseDir, env: envName });
339
358
  addProgress('Keys generated successfully');
340
359
  // Only return public information
341
360
  return c.json({
342
361
  success: true,
343
362
  keyId: secrets.keyPair.keyId,
344
363
  publicKeyJwk: secrets.keyPair.publicKeyJwk,
345
- keysPath: envPaths.keys,
364
+ keysPath: externalKeysDir,
346
365
  });
347
366
  }
348
367
  catch (error) {
@@ -377,23 +396,26 @@ export function createApiRoutes() {
377
396
  // Warning but not an error - just log it
378
397
  addProgress('Warning: Resend API key should start with "re_"');
379
398
  }
380
- // Save secrets to keys directory (use new structure)
381
- const baseDir = findAuthrimBaseDir(process.cwd());
382
- const envPaths = getEnvironmentPaths({ baseDir, env });
383
- const keysDir = envPaths.keys;
384
- // Ensure directory exists
385
- const { mkdir } = await import('node:fs/promises');
386
- await mkdir(keysDir, { recursive: true });
387
- // Save API key
399
+ // Save secrets to external keys directory: {cwd}/.authrim-keys/{env}/
400
+ const keysBaseDir = process.cwd();
401
+ const keysDir = getExternalKeysDir(env, keysBaseDir);
402
+ const baseDir = findAuthrimBaseDir(keysBaseDir);
403
+ const envPaths = getEnvironmentPaths({ baseDir, env, keysBaseDir });
404
+ // Ensure directory exists with restrictive permissions
405
+ await mkdir(keysDir, { recursive: true, mode: 0o700 });
406
+ // Save API key with restrictive permissions
388
407
  await writeFile(envPaths.keyFiles.resendApiKey, apiKey.trim());
408
+ await chmod(envPaths.keyFiles.resendApiKey, 0o600);
389
409
  addProgress(`Saved ${provider} API key to ${envPaths.keyFiles.resendApiKey}`);
390
- // Save from address
410
+ // Save from address with restrictive permissions
391
411
  await writeFile(envPaths.keyFiles.emailFrom, fromAddress.trim());
412
+ await chmod(envPaths.keyFiles.emailFrom, 0o600);
392
413
  addProgress(`Saved email from address to ${envPaths.keyFiles.emailFrom}`);
393
414
  // Save from name if provided
394
415
  if (fromName) {
395
416
  const fromNameFile = join(keysDir, 'email_from_name.txt');
396
417
  await writeFile(fromNameFile, fromName.trim());
418
+ await chmod(fromNameFile, 0o600);
397
419
  addProgress(`Saved email from name to ${fromNameFile}`);
398
420
  }
399
421
  addProgress('Email configuration saved successfully');
@@ -469,6 +491,8 @@ export function createApiRoutes() {
469
491
  addProgress('Creating lock file...');
470
492
  const lock = createLockFile(env, resources);
471
493
  const rootDir = findAuthrimBaseDir(process.cwd());
494
+ const envPaths = getEnvironmentPaths({ baseDir: rootDir, env });
495
+ addProgress(`Saving lock.json to ${envPaths.lock} ...`);
472
496
  await saveLockFile(lock, { env, baseDir: rootDir });
473
497
  // Save config.json
474
498
  addProgress('Saving config.json...');
@@ -491,16 +515,32 @@ export function createApiRoutes() {
491
515
  const apiUrl = workersSubdomain
492
516
  ? `https://${env}-ar-router.${workersSubdomain}.workers.dev`
493
517
  : `https://${env}-ar-router.workers.dev`;
494
- const loginUiUrl = `https://${env}-ar-login-ui.pages.dev`;
495
- const adminUiUrl = `https://${env}-ar-admin-ui.pages.dev`;
496
- config.urls = {
497
- api: { auto: apiUrl },
498
- loginUi: { sameAsApi: false, auto: loginUiUrl },
499
- adminUi: { sameAsApi: false, auto: adminUiUrl },
500
- };
518
+ config.urls = buildUrlsConfig({
519
+ env,
520
+ apiDomain: config.urls?.api?.custom || null,
521
+ loginUiDomain: config.urls?.loginUi?.custom || null,
522
+ adminUiDomain: config.urls?.adminUi?.custom || null,
523
+ zoneId: config.urls?.api?.zoneId ?? null,
524
+ customDomainBinding: config.urls?.api?.customDomainBinding ?? false,
525
+ workersSubdomain,
526
+ existingUrls: {
527
+ api: {
528
+ ...config.urls?.api,
529
+ auto: apiUrl,
530
+ },
531
+ loginUi: config.urls?.loginUi,
532
+ adminUi: config.urls?.adminUi,
533
+ },
534
+ });
501
535
  addProgress(`Configured URLs: API=${apiUrl}`);
502
- const envPaths = getEnvironmentPaths({ baseDir: rootDir, env });
536
+ // Explicitly ensure directory exists (defense in depth; saveLockFile also creates it)
537
+ await mkdir(dirname(envPaths.config), { recursive: true });
538
+ addProgress(`Saving config.json to ${envPaths.config} ...`);
503
539
  await writeFile(envPaths.config, JSON.stringify(config, null, 2), 'utf-8');
540
+ const initialUiEnv = buildInitialUiEnvConfig(config);
541
+ if (initialUiEnv) {
542
+ await saveUiEnv(envPaths.uiEnv, initialUiEnv);
543
+ }
504
544
  state.config = config;
505
545
  // Generate wrangler.toml files
506
546
  addProgress('Generating wrangler.toml files...');
@@ -529,11 +569,18 @@ export function createApiRoutes() {
529
569
  }
530
570
  state.status = 'configuring';
531
571
  addProgress('Provisioning complete!');
572
+ addProgress(`📁 Config saved: ${envPaths.config}`);
573
+ addProgress(`📁 Lock saved: ${envPaths.lock}`);
532
574
  return c.json({
533
575
  success: true,
534
576
  resources,
535
577
  lock,
536
578
  config,
579
+ savedPaths: {
580
+ config: envPaths.config,
581
+ lock: envPaths.lock,
582
+ root: envPaths.root,
583
+ },
537
584
  });
538
585
  }
539
586
  catch (error) {
@@ -634,11 +681,19 @@ export function createApiRoutes() {
634
681
  addProgress('Packages built successfully');
635
682
  }
636
683
  // Upload secrets first (secrets are read but not stored in state)
637
- // Check both new (.authrim/{env}/keys/) and legacy (.keys/{env}/) structures
684
+ // Check external (.authrim-keys/), new (.authrim/{env}/keys/), and legacy (.keys/{env}/) structures
638
685
  const baseDir = findAuthrimBaseDir(process.cwd());
639
686
  const resolved = resolvePaths({ baseDir, env });
640
687
  let keysDir;
641
- if (resolved.type === 'new') {
688
+ const foundKeys = findKeysDirectory({
689
+ env,
690
+ sourceDir: baseDir,
691
+ keysBaseDir: process.cwd(),
692
+ });
693
+ if (foundKeys) {
694
+ keysDir = foundKeys.path;
695
+ }
696
+ else if (resolved.type === 'new') {
642
697
  keysDir = resolved.paths.keys;
643
698
  }
644
699
  else {
@@ -649,6 +704,7 @@ export function createApiRoutes() {
649
704
  const secrets = {};
650
705
  const secretFiles = [
651
706
  { file: join(keysDir, 'private.pem'), name: 'PRIVATE_KEY_PEM' },
707
+ { file: join(keysDir, 'public.jwk.json'), name: 'PUBLIC_JWK_JSON' },
652
708
  { file: join(keysDir, 'rp_token_encryption_key.txt'), name: 'RP_TOKEN_ENCRYPTION_KEY' },
653
709
  { file: join(keysDir, 'admin_api_secret.txt'), name: 'ADMIN_API_SECRET' },
654
710
  { file: join(keysDir, 'key_manager_secret.txt'), name: 'KEY_MANAGER_SECRET' },
@@ -792,45 +848,70 @@ export function createApiRoutes() {
792
848
  const apiBaseUrl = cfg?.urls?.api?.custom ||
793
849
  cfg?.urls?.api?.auto ||
794
850
  `https://${env}-ar-router.workers.dev`;
795
- // Get ui.env path for Vite builds (new structure only)
796
- // Always regenerate ui.env from config to ensure sync
797
- let uiEnvPath;
798
- if (resolved.type === 'new') {
799
- uiEnvPath = resolved.paths.uiEnv;
800
- // Regenerate ui.env from config.json to ensure they are in sync
801
- // Detect if custom domains are used (same registrable domain = no need for proxy)
802
- const apiHasCustomDomain = !!cfg?.urls?.api?.custom;
803
- const adminUiHasCustomDomain = !!cfg?.urls?.adminUi?.custom;
804
- const useDirectMode = apiHasCustomDomain && adminUiHasCustomDomain;
805
- const { saveUiEnv } = await import('../core/ui-env.js');
806
- try {
807
- if (useDirectMode) {
808
- await saveUiEnv(uiEnvPath, {
809
- PUBLIC_API_BASE_URL: apiBaseUrl, // Frontend sends directly to backend
810
- API_BACKEND_URL: '', // Proxy disabled
811
- });
812
- addProgress(`ui.env synced (direct mode: ${apiBaseUrl})`);
813
- addProgress(`Custom domains detected - Safari ITP proxy disabled`);
851
+ let loginUiClientId;
852
+ if (cfg?.components?.loginUi && !dryRun) {
853
+ const loginUiUrl = cfg?.urls?.loginUi?.custom ||
854
+ cfg?.urls?.loginUi?.auto ||
855
+ `https://${env}-ar-login-ui.pages.dev`;
856
+ const foundKeys = findKeysDirectory({
857
+ env,
858
+ sourceDir: rootDir,
859
+ keysBaseDir: process.cwd(),
860
+ });
861
+ const adminApiSecretPath = foundKeys
862
+ ? join(foundKeys.path, 'admin_api_secret.txt')
863
+ : resolved.paths.keyFiles.adminApiSecret;
864
+ const { ensureLoginUiClient } = await import('../core/login-ui-client.js');
865
+ const clientResult = await ensureLoginUiClient({
866
+ apiBaseUrl,
867
+ loginUiUrl,
868
+ adminApiSecretPath,
869
+ onProgress: addProgress,
870
+ });
871
+ if (clientResult.success && clientResult.clientId) {
872
+ loginUiClientId = clientResult.clientId;
873
+ if (clientResult.alreadyExists) {
874
+ addProgress(` ✓ Login UI client exists: ${loginUiClientId}`);
814
875
  }
815
876
  else {
816
- await saveUiEnv(uiEnvPath, {
817
- PUBLIC_API_BASE_URL: '', // Empty for proxy mode (same-origin)
818
- API_BACKEND_URL: apiBaseUrl, // Server-side proxy target
819
- });
820
- addProgress(`ui.env synced (proxy mode: ${apiBaseUrl})`);
877
+ addProgress(` ✓ Login UI client created: ${loginUiClientId}`);
821
878
  }
822
879
  }
823
- catch (syncError) {
824
- addProgress(`⚠️ Could not sync ui.env: ${syncError}`);
880
+ else {
881
+ addProgress(` ⚠️ Login UI client creation skipped: ${clientResult.error}`);
825
882
  }
826
883
  }
884
+ const loginUiSettings = resolveUiDeploymentSettings({
885
+ component: 'ar-login-ui',
886
+ config: cfg,
887
+ apiBaseUrl,
888
+ loginUiClientId,
889
+ });
890
+ const adminUiSettings = resolveUiDeploymentSettings({
891
+ component: 'ar-admin-ui',
892
+ config: cfg,
893
+ apiBaseUrl,
894
+ });
827
895
  pagesSummary = await deployAllPages({
828
896
  env,
829
897
  rootDir: resolve(rootDir),
830
898
  dryRun,
831
899
  onProgress: addProgress,
832
900
  apiBaseUrl,
833
- uiEnvPath,
901
+ perComponent: {
902
+ 'ar-login-ui': {
903
+ apiBaseUrl: loginUiSettings.apiBaseUrl,
904
+ runtimeApiBackendUrl: loginUiSettings.runtimeApiBackendUrl,
905
+ uiEnvConfig: loginUiSettings.uiEnv,
906
+ serviceBindingName: loginUiSettings.serviceBindingName,
907
+ },
908
+ 'ar-admin-ui': {
909
+ apiBaseUrl: adminUiSettings.apiBaseUrl,
910
+ runtimeApiBackendUrl: adminUiSettings.runtimeApiBackendUrl,
911
+ uiEnvConfig: adminUiSettings.uiEnv,
912
+ serviceBindingName: adminUiSettings.serviceBindingName,
913
+ },
914
+ },
834
915
  }, {
835
916
  loginUi: cfg?.components?.loginUi ?? true,
836
917
  adminUi: cfg?.components?.adminUi ?? true,
@@ -855,10 +936,13 @@ export function createApiRoutes() {
855
936
  // Update lock file with deployed workers information
856
937
  if (workersSuccess && !dryRun && summary.successCount > 0) {
857
938
  try {
858
- const { loadLockFileAuto, saveLockFile: saveLock } = await import('../core/lock.js');
939
+ const { loadLockFileAuto, saveLockFile: saveLock, AuthrimLockSchema, } = await import('../core/lock.js');
859
940
  const { lock: currentLock, path: lockPath } = await loadLockFileAuto(rootDir, env);
860
- if (currentLock && lockPath) {
861
- const workers = { ...currentLock.workers };
941
+ if (lockPath) {
942
+ // Use existing lock or create minimal one (recovery scenario: lock deleted/missing)
943
+ const now = new Date().toISOString();
944
+ const baseLock = currentLock ?? AuthrimLockSchema.parse({ env, createdAt: now });
945
+ const workers = { ...baseLock.workers };
862
946
  for (const result of summary.results) {
863
947
  if (result.success && result.deployedAt) {
864
948
  workers[result.component] = {
@@ -869,9 +953,9 @@ export function createApiRoutes() {
869
953
  }
870
954
  }
871
955
  const updatedLock = {
872
- ...currentLock,
956
+ ...baseLock,
873
957
  workers,
874
- updatedAt: new Date().toISOString(),
958
+ updatedAt: now,
875
959
  };
876
960
  await saveLock(updatedLock, lockPath);
877
961
  addProgress('Lock file updated with deployment info');
@@ -954,7 +1038,7 @@ export function createApiRoutes() {
954
1038
  });
955
1039
  });
956
1040
  // Complete initial admin setup (store setup token in KV)
957
- // Supports both new (.authrim/{env}/keys/) and legacy (.keys/{env}/) structures
1041
+ // Supports external (.authrim-keys/), internal (.authrim/{env}/keys/), and legacy (.keys/{env}/) structures
958
1042
  api.post('/admin/setup', async (c) => {
959
1043
  return withLock(async () => {
960
1044
  try {
@@ -964,9 +1048,17 @@ export function createApiRoutes() {
964
1048
  // Determine structure type
965
1049
  const resolved = resolvePaths({ baseDir, env });
966
1050
  const isLegacy = resolved.type === 'legacy';
967
- const tokenPath = isLegacy
968
- ? resolved.paths.keyFiles.setupToken
969
- : resolved.paths.keyFiles.setupToken;
1051
+ // Detect actual token path using 3-tier fallback (external → internal → legacy)
1052
+ const foundKeys = findKeysDirectory({
1053
+ env,
1054
+ sourceDir: baseDir,
1055
+ keysBaseDir: process.cwd(),
1056
+ });
1057
+ const tokenPath = foundKeys
1058
+ ? join(foundKeys.path, 'setup_token.txt')
1059
+ : isLegacy
1060
+ ? resolved.paths.keyFiles.setupToken
1061
+ : resolved.paths.keyFiles.setupToken;
970
1062
  addProgress(`Admin setup request: env=${env}, baseUrl=${baseUrl}, structure=${resolved.type}`);
971
1063
  if (!env || !baseUrl) {
972
1064
  addProgress('Error: env and baseUrl are required');
@@ -978,6 +1070,7 @@ export function createApiRoutes() {
978
1070
  env,
979
1071
  baseUrl,
980
1072
  baseDir,
1073
+ keysBaseDir: process.cwd(),
981
1074
  legacy: isLegacy,
982
1075
  onProgress: addProgress,
983
1076
  });
@@ -1032,7 +1125,7 @@ export function createApiRoutes() {
1032
1125
  return withLock(async () => {
1033
1126
  try {
1034
1127
  const body = await c.req.json();
1035
- const { kvNamespaceId, baseUrl } = body;
1128
+ const { kvNamespaceId, baseUrl, env: envName } = body;
1036
1129
  if (!kvNamespaceId || !/^[a-f0-9]{32}$/i.test(kvNamespaceId)) {
1037
1130
  return c.json({ success: false, error: 'Invalid KV namespace ID' }, 400);
1038
1131
  }
@@ -1053,8 +1146,29 @@ export function createApiRoutes() {
1053
1146
  if (!result.success || !result.token) {
1054
1147
  return c.json({ success: false, error: result.error || 'Failed to generate token' }, 500);
1055
1148
  }
1149
+ // Resolve the best base URL: prefer custom API domain from config
1150
+ let resolvedBaseUrl = baseUrl;
1151
+ if (envName) {
1152
+ try {
1153
+ const baseDir = findAuthrimBaseDir(process.cwd());
1154
+ const resolved = resolvePaths({ baseDir, env: envName });
1155
+ const configPath = resolved.type === 'new'
1156
+ ? resolved.paths.config
1157
+ : resolved.paths.config;
1158
+ if (existsSync(configPath)) {
1159
+ const cfg = JSON.parse(await readFile(configPath, 'utf-8'));
1160
+ const configBaseUrl = cfg?.urls?.api?.custom || cfg?.urls?.api?.auto;
1161
+ if (configBaseUrl) {
1162
+ resolvedBaseUrl = configBaseUrl;
1163
+ }
1164
+ }
1165
+ }
1166
+ catch {
1167
+ // Config not available, use the provided baseUrl
1168
+ }
1169
+ }
1056
1170
  // Construct setup URL
1057
- const cleanBaseUrl = baseUrl.replace(/\/+$/, '');
1171
+ const cleanBaseUrl = resolvedBaseUrl.replace(/\/+$/, '');
1058
1172
  const setupUrl = `${cleanBaseUrl}/admin-init-setup?token=${result.token}`;
1059
1173
  return c.json({
1060
1174
  success: true,
@@ -1440,40 +1554,53 @@ export function createApiRoutes() {
1440
1554
  const apiBaseUrl = cfg?.urls?.api?.custom ||
1441
1555
  cfg?.urls?.api?.auto ||
1442
1556
  `https://${env}-ar-router.workers.dev`;
1443
- // Get ui.env path for new structure
1444
- let uiEnvPath;
1445
- if (resolved.type === 'new') {
1446
- uiEnvPath = resolved.paths.uiEnv;
1447
- // Sync ui.env before build
1448
- const apiHasCustomDomain = !!cfg?.urls?.api?.custom;
1449
- const adminUiHasCustomDomain = !!cfg?.urls?.adminUi?.custom;
1450
- const useDirectMode = apiHasCustomDomain && adminUiHasCustomDomain;
1451
- const { saveUiEnv } = await import('../core/ui-env.js');
1452
- try {
1453
- if (useDirectMode) {
1454
- await saveUiEnv(uiEnvPath, {
1455
- PUBLIC_API_BASE_URL: apiBaseUrl,
1456
- API_BACKEND_URL: '',
1457
- });
1557
+ let loginUiClientId;
1558
+ if (componentName === 'ar-login-ui' && !dryRun) {
1559
+ const loginUiUrl = cfg?.urls?.loginUi?.custom ||
1560
+ cfg?.urls?.loginUi?.auto ||
1561
+ `https://${env}-ar-login-ui.pages.dev`;
1562
+ const foundKeys = findKeysDirectory({
1563
+ env,
1564
+ sourceDir: rootDir,
1565
+ keysBaseDir: process.cwd(),
1566
+ });
1567
+ const adminApiSecretPath = foundKeys
1568
+ ? join(foundKeys.path, 'admin_api_secret.txt')
1569
+ : resolved.paths.keyFiles.adminApiSecret;
1570
+ const { ensureLoginUiClient } = await import('../core/login-ui-client.js');
1571
+ const clientResult = await ensureLoginUiClient({
1572
+ apiBaseUrl,
1573
+ loginUiUrl,
1574
+ adminApiSecretPath,
1575
+ onProgress: addProgress,
1576
+ });
1577
+ if (clientResult.success && clientResult.clientId) {
1578
+ loginUiClientId = clientResult.clientId;
1579
+ if (clientResult.alreadyExists) {
1580
+ addProgress(` ✓ Login UI client exists: ${loginUiClientId}`);
1458
1581
  }
1459
1582
  else {
1460
- await saveUiEnv(uiEnvPath, {
1461
- PUBLIC_API_BASE_URL: '',
1462
- API_BACKEND_URL: apiBaseUrl,
1463
- });
1583
+ addProgress(` ✓ Login UI client created: ${loginUiClientId}`);
1464
1584
  }
1465
- addProgress(`ui.env synced for ${componentName}`);
1466
1585
  }
1467
- catch {
1468
- addProgress(`Warning: Could not sync ui.env`);
1586
+ else {
1587
+ addProgress(` ⚠️ Login UI client creation skipped: ${clientResult.error}`);
1469
1588
  }
1470
1589
  }
1590
+ const uiSettings = resolveUiDeploymentSettings({
1591
+ component: componentName,
1592
+ config: cfg,
1593
+ apiBaseUrl,
1594
+ loginUiClientId,
1595
+ });
1471
1596
  const result = await deployPagesComponent(componentName, {
1472
1597
  env,
1473
1598
  rootDir,
1474
1599
  dryRun,
1475
- apiBaseUrl,
1476
- uiEnvPath,
1600
+ apiBaseUrl: uiSettings.apiBaseUrl,
1601
+ runtimeApiBackendUrl: uiSettings.runtimeApiBackendUrl,
1602
+ uiEnvConfig: uiSettings.uiEnv,
1603
+ serviceBindingName: uiSettings.serviceBindingName,
1477
1604
  onProgress: addProgress,
1478
1605
  });
1479
1606
  if (result.success) {