@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.
- package/dist/__tests__/keys.test.js +73 -2
- package/dist/__tests__/keys.test.js.map +1 -1
- package/dist/__tests__/migrate.test.js +4 -4
- package/dist/__tests__/migrate.test.js.map +1 -1
- package/dist/__tests__/paths.test.js +163 -1
- package/dist/__tests__/paths.test.js.map +1 -1
- package/dist/__tests__/source-context.test.d.ts +2 -0
- package/dist/__tests__/source-context.test.d.ts.map +1 -0
- package/dist/__tests__/source-context.test.js +72 -0
- package/dist/__tests__/source-context.test.js.map +1 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -1
- package/dist/cli/commands/deploy.js +65 -37
- package/dist/cli/commands/deploy.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +277 -198
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/core/admin.d.ts +6 -1
- package/dist/core/admin.d.ts.map +1 -1
- package/dist/core/admin.js +45 -20
- package/dist/core/admin.js.map +1 -1
- package/dist/core/cloudflare.d.ts +38 -1
- package/dist/core/cloudflare.d.ts.map +1 -1
- package/dist/core/cloudflare.js +729 -115
- package/dist/core/cloudflare.js.map +1 -1
- package/dist/core/config.d.ts +164 -34
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +72 -18
- package/dist/core/config.js.map +1 -1
- package/dist/core/deploy.d.ts +18 -0
- package/dist/core/deploy.d.ts.map +1 -1
- package/dist/core/deploy.js +126 -25
- package/dist/core/deploy.js.map +1 -1
- package/dist/core/keys.d.ts +20 -4
- package/dist/core/keys.d.ts.map +1 -1
- package/dist/core/keys.js +77 -17
- package/dist/core/keys.js.map +1 -1
- package/dist/core/login-ui-client.d.ts +42 -0
- package/dist/core/login-ui-client.d.ts.map +1 -0
- package/dist/core/login-ui-client.js +173 -0
- package/dist/core/login-ui-client.js.map +1 -0
- package/dist/core/migrate.d.ts +37 -0
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +92 -2
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/paths.d.ts +78 -13
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +135 -17
- package/dist/core/paths.js.map +1 -1
- package/dist/core/source-context.d.ts +22 -0
- package/dist/core/source-context.d.ts.map +1 -0
- package/dist/core/source-context.js +46 -0
- package/dist/core/source-context.js.map +1 -0
- package/dist/core/tenant-mode.d.ts +4 -0
- package/dist/core/tenant-mode.d.ts.map +1 -0
- package/dist/core/tenant-mode.js +17 -0
- package/dist/core/tenant-mode.js.map +1 -0
- package/dist/core/ui-deployment.d.ts +21 -0
- package/dist/core/ui-deployment.d.ts.map +1 -0
- package/dist/core/ui-deployment.js +90 -0
- package/dist/core/ui-deployment.js.map +1 -0
- package/dist/core/ui-env.d.ts +28 -0
- package/dist/core/ui-env.d.ts.map +1 -1
- package/dist/core/ui-env.js +16 -0
- package/dist/core/ui-env.js.map +1 -1
- package/dist/core/url-config.d.ts +16 -0
- package/dist/core/url-config.d.ts.map +1 -0
- package/dist/core/url-config.js +46 -0
- package/dist/core/url-config.js.map +1 -0
- package/dist/core/wrangler.d.ts +50 -1
- package/dist/core/wrangler.d.ts.map +1 -1
- package/dist/core/wrangler.js +171 -57
- package/dist/core/wrangler.js.map +1 -1
- package/dist/i18n/locales/de.d.ts.map +1 -1
- package/dist/i18n/locales/de.js +38 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +38 -1
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts.map +1 -1
- package/dist/i18n/locales/es.js +38 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts.map +1 -1
- package/dist/i18n/locales/fr.js +38 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/id.d.ts.map +1 -1
- package/dist/i18n/locales/id.js +38 -1
- package/dist/i18n/locales/id.js.map +1 -1
- package/dist/i18n/locales/ja.d.ts.map +1 -1
- package/dist/i18n/locales/ja.js +38 -1
- package/dist/i18n/locales/ja.js.map +1 -1
- package/dist/i18n/locales/ko.d.ts.map +1 -1
- package/dist/i18n/locales/ko.js +38 -1
- package/dist/i18n/locales/ko.js.map +1 -1
- package/dist/i18n/locales/pt.d.ts.map +1 -1
- package/dist/i18n/locales/pt.js +38 -1
- package/dist/i18n/locales/pt.js.map +1 -1
- package/dist/i18n/locales/ru.d.ts.map +1 -1
- package/dist/i18n/locales/ru.js +38 -1
- package/dist/i18n/locales/ru.js.map +1 -1
- package/dist/i18n/locales/zh-CN.d.ts.map +1 -1
- package/dist/i18n/locales/zh-CN.js +38 -1
- package/dist/i18n/locales/zh-CN.js.map +1 -1
- package/dist/i18n/locales/zh-TW.d.ts.map +1 -1
- package/dist/i18n/locales/zh-TW.js +38 -1
- package/dist/i18n/locales/zh-TW.js.map +1 -1
- package/dist/i18n/types.d.ts +8 -0
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -30
- package/dist/index.js.map +1 -1
- package/dist/web/api.d.ts.map +1 -1
- package/dist/web/api.js +243 -116
- package/dist/web/api.js.map +1 -1
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +513 -115
- package/dist/web/ui.js.map +1 -1
- package/migrations/000_fresh_schema.sql +229 -10
- package/migrations/admin/007_admin_role_inheritance.sql +32 -0
- package/migrations/admin/008_admin_rebac_definitions.sql +117 -0
- package/migrations/admin/009_optimize_admin_audit_indexes.sql +15 -0
- 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 {
|
|
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,
|
|
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
|
-
*
|
|
142
|
-
* Format: {worker}.{subdomain}.workers.dev
|
|
185
|
+
* Strip the protocol from a URL for display in domain-only prompts.
|
|
143
186
|
*/
|
|
144
|
-
function
|
|
145
|
-
if (
|
|
146
|
-
return
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
//
|
|
1162
|
-
const
|
|
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
|
-
|
|
1167
|
-
await fs.writeFile(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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 (
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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: ${
|
|
1721
|
-
if (
|
|
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(`
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
1801
|
-
const
|
|
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
|
-
|
|
1806
|
-
await fs.writeFile(
|
|
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
|
-
|
|
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 (
|
|
1935
|
+
// Step 1: Generate keys (external directory: .authrim-keys/{env}/)
|
|
1865
1936
|
// Check if keys already exist for this environment
|
|
1866
|
-
|
|
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
|
|
1876
|
-
const
|
|
1877
|
-
await saveKeysToDirectory(secrets, {
|
|
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:
|
|
1953
|
+
secretsPath: getExternalKeysPathForConfig(env, cwdDir),
|
|
1882
1954
|
includeSecrets: false,
|
|
1955
|
+
storageType: 'external',
|
|
1883
1956
|
};
|
|
1884
|
-
keysSpinner.succeed(`Keys generated (${
|
|
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
|
|
1953
|
-
if (
|
|
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(`
|
|
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
|
// =============================================================================
|