@authrim/setup 0.1.49 → 0.1.52

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/web/ui.js CHANGED
@@ -746,6 +746,101 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
746
746
  color: var(--text-muted);
747
747
  font-size: 0.8rem;
748
748
  }
749
+
750
+ /* Database configuration styles */
751
+ .database-config-grid {
752
+ display: grid;
753
+ grid-template-columns: 1fr 1fr;
754
+ gap: 1.5rem;
755
+ margin-bottom: 1.5rem;
756
+ }
757
+
758
+ .database-config-stack {
759
+ display: flex;
760
+ flex-direction: column;
761
+ gap: 1.5rem;
762
+ margin-bottom: 1.5rem;
763
+ }
764
+
765
+ .database-card {
766
+ background: var(--bg);
767
+ border: 1px solid var(--border);
768
+ border-radius: 0.5rem;
769
+ padding: 1.25rem;
770
+ }
771
+
772
+ .database-card h3 {
773
+ margin: 0 0 1rem 0;
774
+ font-size: 1.1rem;
775
+ }
776
+
777
+ .db-description {
778
+ font-size: 0.875rem;
779
+ color: var(--text-muted);
780
+ margin-bottom: 1rem;
781
+ }
782
+
783
+ .db-description p {
784
+ margin: 0 0 0.5rem 0;
785
+ }
786
+
787
+ .db-description ul {
788
+ margin: 0.5rem 0;
789
+ padding-left: 1.25rem;
790
+ }
791
+
792
+ .db-description li {
793
+ margin-bottom: 0.25rem;
794
+ }
795
+
796
+ .db-hint {
797
+ font-style: italic;
798
+ margin-top: 0.75rem;
799
+ padding: 0.5rem;
800
+ background: #f0f9ff;
801
+ border-radius: 4px;
802
+ }
803
+
804
+ .region-selection h4 {
805
+ margin: 0 0 0.75rem 0;
806
+ font-size: 0.95rem;
807
+ font-weight: 600;
808
+ }
809
+
810
+ .radio-group {
811
+ display: flex;
812
+ flex-direction: column;
813
+ gap: 0.5rem;
814
+ }
815
+
816
+ .radio-item {
817
+ display: flex;
818
+ align-items: center;
819
+ gap: 0.5rem;
820
+ cursor: pointer;
821
+ padding: 0.25rem 0;
822
+ }
823
+
824
+ .radio-item input[type="radio"] {
825
+ margin: 0;
826
+ width: 16px;
827
+ height: 16px;
828
+ }
829
+
830
+ .radio-separator {
831
+ font-size: 0.75rem;
832
+ color: var(--text-muted);
833
+ margin: 0.5rem 0 0.25rem 0;
834
+ font-weight: 500;
835
+ text-transform: uppercase;
836
+ letter-spacing: 0.05em;
837
+ }
838
+
839
+ @media (max-width: 768px) {
840
+ .database-config-grid {
841
+ grid-template-columns: 1fr;
842
+ }
843
+ }
749
844
  </style>
750
845
  </head>
751
846
  <body>
@@ -772,6 +867,14 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
772
867
  <div class="step step-pending" id="step-3">3</div>
773
868
  <div class="step-connector"></div>
774
869
  <div class="step step-pending" id="step-4">4</div>
870
+ <div class="step-connector"></div>
871
+ <div class="step step-pending" id="step-5">5</div>
872
+ <div class="step-connector"></div>
873
+ <div class="step step-pending" id="step-6">6</div>
874
+ <div class="step-connector"></div>
875
+ <div class="step step-pending" id="step-7">7</div>
876
+ <div class="step-connector"></div>
877
+ <div class="step step-pending" id="step-8">8</div>
775
878
  </div>
776
879
 
777
880
  <!-- Step 1: Prerequisites -->
@@ -1035,7 +1138,210 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1035
1138
  </div>
1036
1139
  </div>
1037
1140
 
1038
- <!-- Step 3: Provisioning -->
1141
+ <!-- Step 3: Database Configuration -->
1142
+ <div id="section-database" class="card hidden">
1143
+ <h2 class="card-title">🗄️ Database Configuration</h2>
1144
+
1145
+ <p style="margin-bottom: 1rem; color: var(--text-muted);">
1146
+ Authrim uses two separate D1 databases to isolate personal data from application data.
1147
+ </p>
1148
+
1149
+ <div class="warning-box" style="background: #fef3c7; border-left: 4px solid #f59e0b; padding: 0.75rem 1rem; margin-bottom: 1.5rem; border-radius: 0.375rem;">
1150
+ ⚠️ Database region cannot be changed after creation.
1151
+ </div>
1152
+
1153
+ <div class="database-config-stack">
1154
+ <!-- Core Database (Non-PII) -->
1155
+ <div class="database-card">
1156
+ <h3>🗄️ Core Database <span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">(Non-PII)</span></h3>
1157
+ <div class="db-description">
1158
+ <p>Stores non-personal application data including:</p>
1159
+ <ul>
1160
+ <li>OAuth clients and their configurations</li>
1161
+ <li>Authorization codes and access tokens</li>
1162
+ <li>User sessions and login state</li>
1163
+ <li>Tenant settings and configurations</li>
1164
+ <li>Audit logs and security events</li>
1165
+ </ul>
1166
+ <p class="db-hint">This database handles all authentication flows and should be placed close to your primary user base.</p>
1167
+ </div>
1168
+
1169
+ <div class="region-selection">
1170
+ <h4>Region</h4>
1171
+ <div class="radio-group">
1172
+ <label class="radio-item">
1173
+ <input type="radio" name="db-core-location" value="auto" checked>
1174
+ <span>Automatic (nearest to you)</span>
1175
+ </label>
1176
+ <div class="radio-separator">Location Hints</div>
1177
+ <label class="radio-item">
1178
+ <input type="radio" name="db-core-location" value="wnam">
1179
+ <span>North America (West)</span>
1180
+ </label>
1181
+ <label class="radio-item">
1182
+ <input type="radio" name="db-core-location" value="enam">
1183
+ <span>North America (East)</span>
1184
+ </label>
1185
+ <label class="radio-item">
1186
+ <input type="radio" name="db-core-location" value="weur">
1187
+ <span>Europe (West)</span>
1188
+ </label>
1189
+ <label class="radio-item">
1190
+ <input type="radio" name="db-core-location" value="eeur">
1191
+ <span>Europe (East)</span>
1192
+ </label>
1193
+ <label class="radio-item">
1194
+ <input type="radio" name="db-core-location" value="apac">
1195
+ <span>Asia Pacific</span>
1196
+ </label>
1197
+ <label class="radio-item">
1198
+ <input type="radio" name="db-core-location" value="oc">
1199
+ <span>Oceania</span>
1200
+ </label>
1201
+ <div class="radio-separator">Jurisdiction (Compliance)</div>
1202
+ <label class="radio-item">
1203
+ <input type="radio" name="db-core-location" value="eu">
1204
+ <span>EU Jurisdiction (GDPR compliance)</span>
1205
+ </label>
1206
+ </div>
1207
+ </div>
1208
+ </div>
1209
+
1210
+ <!-- PII Database -->
1211
+ <div class="database-card">
1212
+ <h3>🔒 PII Database <span style="font-size: 0.8rem; font-weight: normal; color: var(--text-muted);">(Personal Identifiable Information)</span></h3>
1213
+ <div class="db-description">
1214
+ <p>Stores personal user data including:</p>
1215
+ <ul>
1216
+ <li>User profiles (name, email, phone)</li>
1217
+ <li>Passkey/WebAuthn credentials</li>
1218
+ <li>User preferences and settings</li>
1219
+ <li>Any custom user attributes</li>
1220
+ </ul>
1221
+ <p class="db-hint">This database contains personal data. Consider placing it in a region that complies with your data protection requirements.</p>
1222
+ </div>
1223
+
1224
+ <div class="region-selection">
1225
+ <h4>Region</h4>
1226
+ <div class="radio-group">
1227
+ <label class="radio-item">
1228
+ <input type="radio" name="db-pii-location" value="auto" checked>
1229
+ <span>Automatic (nearest to you)</span>
1230
+ </label>
1231
+ <div class="radio-separator">Location Hints</div>
1232
+ <label class="radio-item">
1233
+ <input type="radio" name="db-pii-location" value="wnam">
1234
+ <span>North America (West)</span>
1235
+ </label>
1236
+ <label class="radio-item">
1237
+ <input type="radio" name="db-pii-location" value="enam">
1238
+ <span>North America (East)</span>
1239
+ </label>
1240
+ <label class="radio-item">
1241
+ <input type="radio" name="db-pii-location" value="weur">
1242
+ <span>Europe (West)</span>
1243
+ </label>
1244
+ <label class="radio-item">
1245
+ <input type="radio" name="db-pii-location" value="eeur">
1246
+ <span>Europe (East)</span>
1247
+ </label>
1248
+ <label class="radio-item">
1249
+ <input type="radio" name="db-pii-location" value="apac">
1250
+ <span>Asia Pacific</span>
1251
+ </label>
1252
+ <label class="radio-item">
1253
+ <input type="radio" name="db-pii-location" value="oc">
1254
+ <span>Oceania</span>
1255
+ </label>
1256
+ <div class="radio-separator">Jurisdiction (Compliance)</div>
1257
+ <label class="radio-item">
1258
+ <input type="radio" name="db-pii-location" value="eu">
1259
+ <span>EU Jurisdiction (GDPR compliance)</span>
1260
+ </label>
1261
+ </div>
1262
+ </div>
1263
+ </div>
1264
+ </div>
1265
+
1266
+ <div class="button-group">
1267
+ <button class="btn-secondary" id="btn-back-database">Back</button>
1268
+ <button class="btn-primary" id="btn-continue-database">Continue</button>
1269
+ </div>
1270
+ </div>
1271
+
1272
+ <!-- Step 4: Email Provider Configuration -->
1273
+ <div id="section-email" class="card hidden">
1274
+ <h2 class="card-title">📧 Email Provider</h2>
1275
+
1276
+ <p style="margin-bottom: 1rem; color: var(--text-muted);">
1277
+ Configure email sending for magic links and verification codes.
1278
+ You can configure this later if you prefer.
1279
+ </p>
1280
+
1281
+ <div class="radio-group" style="margin-bottom: 1.5rem;">
1282
+ <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px;">
1283
+ <input type="radio" name="email-setup-choice" value="later" checked>
1284
+ <span style="display: flex; flex-direction: column; gap: 0.25rem;">
1285
+ <strong>Configure later</strong>
1286
+ <small style="color: var(--text-muted);">Skip for now. Magic links will return URLs instead of sending emails.</small>
1287
+ </span>
1288
+ </label>
1289
+ <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px; margin-top: 0.5rem;">
1290
+ <input type="radio" name="email-setup-choice" value="configure">
1291
+ <span style="display: flex; flex-direction: column; gap: 0.25rem;">
1292
+ <strong>Configure Resend</strong>
1293
+ <small style="color: var(--text-muted);">Set up email sending with Resend (recommended for production).</small>
1294
+ </span>
1295
+ </label>
1296
+ </div>
1297
+
1298
+ <!-- Resend Configuration Form (hidden by default) -->
1299
+ <div id="resend-config-form" class="hidden" style="background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem;">
1300
+ <h3 style="margin: 0 0 1rem 0; font-size: 1rem;">🔑 Resend Configuration</h3>
1301
+
1302
+ <div class="alert alert-info" style="margin-bottom: 1rem;">
1303
+ <strong>📋 Before you begin:</strong>
1304
+ <ol style="margin: 0.5rem 0 0 1rem; padding: 0;">
1305
+ <li>Create a Resend account at <a href="https://resend.com" target="_blank" style="color: var(--primary);">resend.com</a></li>
1306
+ <li>Add and verify your domain at <a href="https://resend.com/domains" target="_blank" style="color: var(--primary);">Domains Dashboard</a></li>
1307
+ <li>Create an API key at <a href="https://resend.com/api-keys" target="_blank" style="color: var(--primary);">API Keys</a></li>
1308
+ </ol>
1309
+ </div>
1310
+
1311
+ <div class="form-group">
1312
+ <label for="resend-api-key">Resend API Key</label>
1313
+ <input type="password" id="resend-api-key" placeholder="re_xxxxxxxxxx" autocomplete="off">
1314
+ <small style="color: var(--text-muted);">Your API key starts with "re_"</small>
1315
+ </div>
1316
+
1317
+ <div class="form-group">
1318
+ <label for="email-from-address">From Email Address</label>
1319
+ <input type="email" id="email-from-address" placeholder="noreply@yourdomain.com" autocomplete="off">
1320
+ <small style="color: var(--text-muted);">Must be from a verified domain in your Resend account</small>
1321
+ </div>
1322
+
1323
+ <div class="form-group">
1324
+ <label for="email-from-name">From Display Name (optional)</label>
1325
+ <input type="text" id="email-from-name" placeholder="Authrim" autocomplete="off">
1326
+ <small style="color: var(--text-muted);">Displayed as the sender name in email clients</small>
1327
+ </div>
1328
+
1329
+ <div class="alert alert-warning" style="margin-top: 1rem;">
1330
+ <strong>⚠️ Domain Verification Required</strong>
1331
+ <p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">
1332
+ Before your domain is verified, emails can only be sent from <code>onboarding@resend.dev</code> (for testing).
1333
+ <a href="https://resend.com/docs/dashboard/domains/introduction" target="_blank" style="color: var(--primary);">Learn more about domain verification →</a>
1334
+ </p>
1335
+ </div>
1336
+ </div>
1337
+
1338
+ <div class="button-group">
1339
+ <button class="btn-secondary" id="btn-back-email">Back</button>
1340
+ <button class="btn-primary" id="btn-continue-email">Continue</button>
1341
+ </div>
1342
+ </div>
1343
+
1344
+ <!-- Step 5: Provisioning -->
1039
1345
  <div id="section-provision" class="card hidden">
1040
1346
  <h2 class="card-title">
1041
1347
  Resource Provisioning
@@ -1307,6 +1613,10 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1307
1613
  2: document.getElementById('step-2'),
1308
1614
  3: document.getElementById('step-3'),
1309
1615
  4: document.getElementById('step-4'),
1616
+ 5: document.getElementById('step-5'),
1617
+ 6: document.getElementById('step-6'),
1618
+ 7: document.getElementById('step-7'),
1619
+ 8: document.getElementById('step-8'),
1310
1620
  };
1311
1621
 
1312
1622
  const sections = {
@@ -1315,6 +1625,8 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1315
1625
  mode: document.getElementById('section-mode'),
1316
1626
  loadConfig: document.getElementById('section-load-config'),
1317
1627
  config: document.getElementById('section-config'),
1628
+ database: document.getElementById('section-database'),
1629
+ email: document.getElementById('section-email'),
1318
1630
  provision: document.getElementById('section-provision'),
1319
1631
  deploy: document.getElementById('section-deploy'),
1320
1632
  complete: document.getElementById('section-complete'),
@@ -1346,7 +1658,7 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1346
1658
  // Step navigation
1347
1659
  function setStep(step) {
1348
1660
  currentStep = step;
1349
- for (let i = 1; i <= 4; i++) {
1661
+ for (let i = 1; i <= 7; i++) {
1350
1662
  const el = steps[i];
1351
1663
  el.className = 'step ' + (i < step ? 'step-complete' : i === step ? 'step-active' : 'step-pending');
1352
1664
  }
@@ -1577,28 +1889,80 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1577
1889
  document.getElementById('btn-load-config').addEventListener('click', async () => {
1578
1890
  if (!loadedConfig) return;
1579
1891
 
1580
- // Use loaded config
1892
+ // Support both new format (v1.0.0) and old format (v0.1.x)
1893
+ const isNewFormat = loadedConfig.version === '1.0.0' || loadedConfig.environment?.prefix;
1894
+
1895
+ // Extract values (with fallback for old format)
1896
+ const env = isNewFormat
1897
+ ? loadedConfig.environment?.prefix
1898
+ : loadedConfig.env || 'prod';
1899
+
1900
+ const apiDomain = isNewFormat
1901
+ ? loadedConfig.urls?.api?.custom
1902
+ : loadedConfig.apiDomain;
1903
+
1904
+ const loginUiDomain = isNewFormat
1905
+ ? loadedConfig.urls?.loginUi?.custom
1906
+ : loadedConfig.loginUiDomain;
1907
+
1908
+ const adminUiDomain = isNewFormat
1909
+ ? loadedConfig.urls?.adminUi?.custom
1910
+ : loadedConfig.adminUiDomain;
1911
+
1912
+ const tenant = loadedConfig.tenant || {
1913
+ name: 'default',
1914
+ displayName: 'Default Tenant',
1915
+ multiTenant: false,
1916
+ };
1917
+
1918
+ const components = loadedConfig.components || {
1919
+ api: true,
1920
+ loginUi: true,
1921
+ adminUi: true,
1922
+ saml: false,
1923
+ async: false,
1924
+ vc: false,
1925
+ bridge: true,
1926
+ policy: true,
1927
+ };
1928
+
1929
+ // Build internal config
1581
1930
  config = {
1582
- env: loadedConfig.environment?.prefix || 'prod',
1583
- apiDomain: loadedConfig.urls?.api?.custom || null,
1584
- loginUiDomain: loadedConfig.urls?.loginUi?.custom || null,
1585
- adminUiDomain: loadedConfig.urls?.adminUi?.custom || null,
1586
- components: loadedConfig.components || {
1587
- api: true,
1588
- loginUi: true,
1589
- adminUi: true,
1590
- saml: false,
1591
- async: false,
1592
- vc: false,
1593
- },
1931
+ env,
1932
+ apiDomain: apiDomain || null,
1933
+ loginUiDomain: loginUiDomain || null,
1934
+ adminUiDomain: adminUiDomain || null,
1935
+ tenant,
1936
+ components,
1594
1937
  };
1595
1938
 
1596
1939
  // Set form values
1597
1940
  document.getElementById('env').value = config.env;
1598
- document.getElementById('api-domain').value = config.apiDomain || '';
1941
+ document.getElementById('base-domain').value = config.tenant?.baseDomain || config.apiDomain || '';
1599
1942
  document.getElementById('login-domain').value = config.loginUiDomain || '';
1600
1943
  document.getElementById('admin-domain').value = config.adminUiDomain || '';
1601
- // Trigger env input to update default labels
1944
+ document.getElementById('tenant-name').value = config.tenant?.name || 'default';
1945
+ document.getElementById('tenant-display').value = config.tenant?.displayName || 'Default Tenant';
1946
+ document.getElementById('naked-domain').checked = config.tenant?.nakedDomain || false;
1947
+
1948
+ // Set component checkboxes
1949
+ if (document.getElementById('comp-login-ui')) {
1950
+ document.getElementById('comp-login-ui').checked = components.loginUi !== false;
1951
+ }
1952
+ if (document.getElementById('comp-admin-ui')) {
1953
+ document.getElementById('comp-admin-ui').checked = components.adminUi !== false;
1954
+ }
1955
+ if (document.getElementById('comp-saml')) {
1956
+ document.getElementById('comp-saml').checked = components.saml === true;
1957
+ }
1958
+ if (document.getElementById('comp-async')) {
1959
+ document.getElementById('comp-async').checked = components.async === true;
1960
+ }
1961
+ if (document.getElementById('comp-vc')) {
1962
+ document.getElementById('comp-vc').checked = components.vc === true;
1963
+ }
1964
+
1965
+ // Trigger env input to update preview/default labels
1602
1966
  document.getElementById('env').dispatchEvent(new Event('input'));
1603
1967
 
1604
1968
  // Skip to provisioning if resources already exist
@@ -1873,15 +2237,145 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1873
2237
  updateResourcePreview(env);
1874
2238
  updateProvisionButtons();
1875
2239
 
1876
- setStep(3);
1877
- showSection('provision');
2240
+ // Go to database configuration step
2241
+ setStep(4);
2242
+ showSection('database');
1878
2243
  });
1879
2244
 
1880
2245
  document.getElementById('btn-back-config').addEventListener('click', () => {
1881
- setStep(2);
2246
+ // Go back to email configuration (previous step in the flow)
2247
+ setStep(5);
2248
+ showSection('email');
2249
+ });
2250
+
2251
+ // Database configuration handlers
2252
+ document.getElementById('btn-back-database').addEventListener('click', () => {
2253
+ setStep(3);
1882
2254
  showSection('config');
1883
2255
  });
1884
2256
 
2257
+ document.getElementById('btn-continue-database').addEventListener('click', () => {
2258
+ // Get selected values
2259
+ const coreLocation = document.querySelector('input[name="db-core-location"]:checked').value;
2260
+ const piiLocation = document.querySelector('input[name="db-pii-location"]:checked').value;
2261
+
2262
+ // Parse location vs jurisdiction
2263
+ function parseDbLocation(value) {
2264
+ if (value === 'eu') {
2265
+ return { location: 'auto', jurisdiction: 'eu' };
2266
+ }
2267
+ return { location: value, jurisdiction: 'none' };
2268
+ }
2269
+
2270
+ // Add database config to config object
2271
+ config.database = {
2272
+ core: parseDbLocation(coreLocation),
2273
+ pii: parseDbLocation(piiLocation),
2274
+ };
2275
+
2276
+ // Proceed to email configuration
2277
+ setStep(5);
2278
+ showSection('email');
2279
+ });
2280
+
2281
+ // Email configuration handlers
2282
+ // Toggle resend config form visibility
2283
+ document.querySelectorAll('input[name="email-setup-choice"]').forEach(radio => {
2284
+ radio.addEventListener('change', () => {
2285
+ const resendForm = document.getElementById('resend-config-form');
2286
+ const choice = document.querySelector('input[name="email-setup-choice"]:checked').value;
2287
+ if (choice === 'configure') {
2288
+ resendForm.classList.remove('hidden');
2289
+ } else {
2290
+ resendForm.classList.add('hidden');
2291
+ }
2292
+ });
2293
+ });
2294
+
2295
+ document.getElementById('btn-back-email').addEventListener('click', () => {
2296
+ setStep(4);
2297
+ showSection('database');
2298
+ });
2299
+
2300
+ document.getElementById('btn-continue-email').addEventListener('click', async () => {
2301
+ const choice = document.querySelector('input[name="email-setup-choice"]:checked').value;
2302
+ const btn = document.getElementById('btn-continue-email');
2303
+
2304
+ if (choice === 'configure') {
2305
+ // Validate and store email configuration
2306
+ const apiKey = document.getElementById('resend-api-key').value.trim();
2307
+ const fromAddress = document.getElementById('email-from-address').value.trim();
2308
+ const fromName = document.getElementById('email-from-name').value.trim();
2309
+
2310
+ // Validate API key format
2311
+ if (!apiKey) {
2312
+ alert('Please enter your Resend API key');
2313
+ return;
2314
+ }
2315
+ if (!apiKey.startsWith('re_')) {
2316
+ if (!confirm('API key does not start with "re_". This may not be a valid Resend API key. Continue anyway?')) {
2317
+ return;
2318
+ }
2319
+ }
2320
+
2321
+ // Validate email address
2322
+ if (!fromAddress) {
2323
+ alert('Please enter a From email address');
2324
+ return;
2325
+ }
2326
+ if (!fromAddress.includes('@')) {
2327
+ alert('Please enter a valid email address');
2328
+ return;
2329
+ }
2330
+
2331
+ // Save email configuration to server
2332
+ btn.disabled = true;
2333
+ btn.textContent = 'Saving...';
2334
+
2335
+ try {
2336
+ const result = await api('/email/configure', {
2337
+ method: 'POST',
2338
+ body: {
2339
+ env: config.env,
2340
+ provider: 'resend',
2341
+ apiKey: apiKey,
2342
+ fromAddress: fromAddress,
2343
+ fromName: fromName || undefined,
2344
+ },
2345
+ });
2346
+
2347
+ if (!result.success) {
2348
+ throw new Error(result.error || 'Failed to save email configuration');
2349
+ }
2350
+
2351
+ // Store email configuration (without apiKey for config file)
2352
+ config.email = {
2353
+ provider: 'resend',
2354
+ fromAddress: fromAddress,
2355
+ fromName: fromName || undefined,
2356
+ configured: true,
2357
+ };
2358
+ } catch (error) {
2359
+ alert('Failed to save email configuration: ' + error.message);
2360
+ btn.disabled = false;
2361
+ btn.textContent = 'Continue';
2362
+ return;
2363
+ }
2364
+
2365
+ btn.disabled = false;
2366
+ btn.textContent = 'Continue';
2367
+ } else {
2368
+ // Configure later - no email provider
2369
+ config.email = {
2370
+ provider: 'none',
2371
+ };
2372
+ }
2373
+
2374
+ // Proceed to provision
2375
+ setStep(6);
2376
+ showSection('provision');
2377
+ });
2378
+
1885
2379
  // Provision
1886
2380
  document.getElementById('btn-provision').addEventListener('click', async () => {
1887
2381
  const btn = document.getElementById('btn-provision');
@@ -1953,7 +2447,7 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1953
2447
 
1954
2448
  const result = await api('/provision', {
1955
2449
  method: 'POST',
1956
- body: { env: config.env },
2450
+ body: { env: config.env, databaseConfig: config.database },
1957
2451
  });
1958
2452
 
1959
2453
  // Stop polling
@@ -1999,12 +2493,12 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1999
2493
 
2000
2494
  // Continue to Deploy button
2001
2495
  document.getElementById('btn-goto-deploy').addEventListener('click', () => {
2002
- setStep(4);
2496
+ setStep(7);
2003
2497
  showSection('deploy');
2004
2498
  });
2005
2499
 
2006
2500
  document.getElementById('btn-back-provision').addEventListener('click', () => {
2007
- setStep(3);
2501
+ setStep(6);
2008
2502
  // Update buttons based on provisioning status
2009
2503
  updateProvisionButtons();
2010
2504
  // Show resource preview if not completed
@@ -2211,6 +2705,7 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
2211
2705
  urlsEl.appendChild(debugDiv);
2212
2706
  }
2213
2707
 
2708
+ setStep(8);
2214
2709
  showSection('complete');
2215
2710
  }
2216
2711
 
@@ -2293,19 +2788,97 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
2293
2788
  }
2294
2789
  }
2295
2790
 
2296
- // Save configuration to file
2791
+ // Save configuration to file (AuthrimConfigSchema format)
2297
2792
  function saveConfigToFile() {
2298
- if (!config) {
2793
+ if (!config || !config.env) {
2299
2794
  alert('No configuration to save');
2300
2795
  return;
2301
2796
  }
2302
2797
 
2798
+ const now = new Date().toISOString();
2799
+ const env = config.env;
2800
+
2801
+ // Calculate auto-generated URLs
2802
+ const workersDomain = env + '-ar-router.workers.dev';
2803
+ const pagesDomain = env + '-ar-ui.pages.dev';
2804
+
2805
+ // Build issuer URL based on tenant settings
2806
+ let issuerAutoUrl = 'https://' + workersDomain;
2807
+ if (config.tenant && config.tenant.baseDomain) {
2808
+ if (config.tenant.nakedDomain) {
2809
+ issuerAutoUrl = 'https://' + config.tenant.baseDomain;
2810
+ } else {
2811
+ const tenantName = config.tenant.name || 'default';
2812
+ issuerAutoUrl = 'https://' + tenantName + '.' + config.tenant.baseDomain;
2813
+ }
2814
+ }
2815
+
2816
+ // Build config in AuthrimConfigSchema format
2303
2817
  const configToSave = {
2304
- ...config,
2305
- savedAt: new Date().toISOString(),
2306
- version: '0.1.36',
2818
+ version: '1.0.0',
2819
+ createdAt: now,
2820
+ updatedAt: now,
2821
+ environment: {
2822
+ prefix: env,
2823
+ },
2824
+ urls: {
2825
+ api: {
2826
+ custom: config.apiDomain || null,
2827
+ auto: config.apiDomain ? issuerAutoUrl : 'https://' + workersDomain,
2828
+ },
2829
+ loginUi: {
2830
+ custom: config.loginUiDomain || null,
2831
+ auto: 'https://' + pagesDomain,
2832
+ },
2833
+ adminUi: {
2834
+ custom: config.adminUiDomain || null,
2835
+ auto: 'https://' + pagesDomain + '/admin',
2836
+ },
2837
+ },
2838
+ tenant: {
2839
+ name: config.tenant?.name || 'default',
2840
+ displayName: config.tenant?.displayName || 'Default Tenant',
2841
+ multiTenant: config.tenant?.multiTenant || false,
2842
+ baseDomain: config.tenant?.baseDomain || undefined,
2843
+ },
2844
+ components: config.components || {
2845
+ api: true,
2846
+ loginUi: true,
2847
+ adminUi: true,
2848
+ saml: false,
2849
+ async: false,
2850
+ vc: false,
2851
+ bridge: true,
2852
+ policy: true,
2853
+ },
2854
+ keys: {
2855
+ secretsPath: './.keys/',
2856
+ },
2857
+ database: config.database || {
2858
+ core: { location: 'auto', jurisdiction: 'none' },
2859
+ pii: { location: 'auto', jurisdiction: 'none' },
2860
+ },
2861
+ features: {
2862
+ email: {
2863
+ provider: config.email?.provider || 'none',
2864
+ fromAddress: config.email?.fromAddress || undefined,
2865
+ fromName: config.email?.fromName || undefined,
2866
+ configured: config.email?.provider === 'resend' && config.email?.apiKey ? true : false,
2867
+ },
2868
+ },
2307
2869
  };
2308
2870
 
2871
+ // Remove undefined values for cleaner output
2872
+ if (!configToSave.tenant.baseDomain) {
2873
+ delete configToSave.tenant.baseDomain;
2874
+ }
2875
+ if (!configToSave.features.email.fromAddress) {
2876
+ delete configToSave.features.email.fromAddress;
2877
+ }
2878
+ if (!configToSave.features.email.fromName) {
2879
+ delete configToSave.features.email.fromName;
2880
+ }
2881
+
2309
2882
  const blob = new Blob([JSON.stringify(configToSave, null, 2)], { type: 'application/json' });
2310
2883
  const url = URL.createObjectURL(blob);
2311
2884
  const a = document.createElement('a');