@authrim/setup 0.1.47 → 0.1.51

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,94 @@ 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-card {
759
+ background: var(--bg);
760
+ border: 1px solid var(--border);
761
+ border-radius: 0.5rem;
762
+ padding: 1.25rem;
763
+ }
764
+
765
+ .database-card h3 {
766
+ margin: 0 0 1rem 0;
767
+ font-size: 1.1rem;
768
+ }
769
+
770
+ .db-description {
771
+ font-size: 0.875rem;
772
+ color: var(--text-muted);
773
+ margin-bottom: 1rem;
774
+ }
775
+
776
+ .db-description p {
777
+ margin: 0 0 0.5rem 0;
778
+ }
779
+
780
+ .db-description ul {
781
+ margin: 0.5rem 0;
782
+ padding-left: 1.25rem;
783
+ }
784
+
785
+ .db-description li {
786
+ margin-bottom: 0.25rem;
787
+ }
788
+
789
+ .db-hint {
790
+ font-style: italic;
791
+ margin-top: 0.75rem;
792
+ padding: 0.5rem;
793
+ background: #f0f9ff;
794
+ border-radius: 4px;
795
+ }
796
+
797
+ .region-selection h4 {
798
+ margin: 0 0 0.75rem 0;
799
+ font-size: 0.95rem;
800
+ font-weight: 600;
801
+ }
802
+
803
+ .radio-group {
804
+ display: flex;
805
+ flex-direction: column;
806
+ gap: 0.5rem;
807
+ }
808
+
809
+ .radio-item {
810
+ display: flex;
811
+ align-items: center;
812
+ gap: 0.5rem;
813
+ cursor: pointer;
814
+ padding: 0.25rem 0;
815
+ }
816
+
817
+ .radio-item input[type="radio"] {
818
+ margin: 0;
819
+ width: 16px;
820
+ height: 16px;
821
+ }
822
+
823
+ .radio-separator {
824
+ font-size: 0.75rem;
825
+ color: var(--text-muted);
826
+ margin: 0.5rem 0 0.25rem 0;
827
+ font-weight: 500;
828
+ text-transform: uppercase;
829
+ letter-spacing: 0.05em;
830
+ }
831
+
832
+ @media (max-width: 768px) {
833
+ .database-config-grid {
834
+ grid-template-columns: 1fr;
835
+ }
836
+ }
749
837
  </style>
750
838
  </head>
751
839
  <body>
@@ -755,6 +843,15 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
755
843
  <p class="subtitle">OIDC Provider on Cloudflare Workers</p>
756
844
  </header>
757
845
 
846
+ <!-- Development Warning Banner -->
847
+ <div style="background: #fef3c7; border: 2px solid #f59e0b; border-radius: 0.5rem; padding: 1rem; margin-bottom: 1.5rem; text-align: center;">
848
+ <strong style="color: #92400e;">⚠️ WARNING: Under Development!</strong>
849
+ <p style="color: #78350f; margin: 0.5rem 0 0 0; font-size: 0.875rem;">
850
+ This project is still under active development and does not work correctly yet.<br>
851
+ Admin UI is incomplete and does not support login functionality.
852
+ </p>
853
+ </div>
854
+
758
855
  <div class="step-indicator" id="step-indicator">
759
856
  <div class="step step-active" id="step-1">1</div>
760
857
  <div class="step-connector"></div>
@@ -763,6 +860,14 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
763
860
  <div class="step step-pending" id="step-3">3</div>
764
861
  <div class="step-connector"></div>
765
862
  <div class="step step-pending" id="step-4">4</div>
863
+ <div class="step-connector"></div>
864
+ <div class="step step-pending" id="step-5">5</div>
865
+ <div class="step-connector"></div>
866
+ <div class="step step-pending" id="step-6">6</div>
867
+ <div class="step-connector"></div>
868
+ <div class="step step-pending" id="step-7">7</div>
869
+ <div class="step-connector"></div>
870
+ <div class="step step-pending" id="step-8">8</div>
766
871
  </div>
767
872
 
768
873
  <!-- Step 1: Prerequisites -->
@@ -1026,7 +1131,206 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1026
1131
  </div>
1027
1132
  </div>
1028
1133
 
1029
- <!-- Step 3: Provisioning -->
1134
+ <!-- Step 3: Database Configuration -->
1135
+ <div id="section-database" class="card hidden">
1136
+ <h2 class="card-title">🗄️ Database Configuration</h2>
1137
+
1138
+ <div class="warning-box" style="background: #fef3c7; border-left: 4px solid #f59e0b; padding: 0.75rem 1rem; margin-bottom: 1.5rem; border-radius: 0.375rem;">
1139
+ ⚠️ Database region cannot be changed after creation.
1140
+ </div>
1141
+
1142
+ <div class="database-config-grid">
1143
+ <!-- Core Database -->
1144
+ <div class="database-card">
1145
+ <h3>🗄️ Core Database</h3>
1146
+ <div class="db-description">
1147
+ <p>Stores application data including:</p>
1148
+ <ul>
1149
+ <li>OAuth clients and their configurations</li>
1150
+ <li>Authorization codes and access tokens</li>
1151
+ <li>User sessions and login state</li>
1152
+ <li>Tenant settings and configurations</li>
1153
+ <li>Audit logs and security events</li>
1154
+ </ul>
1155
+ <p class="db-hint">This database handles all authentication flows and should be placed close to your primary user base.</p>
1156
+ </div>
1157
+
1158
+ <div class="region-selection">
1159
+ <h4>Region</h4>
1160
+ <div class="radio-group">
1161
+ <label class="radio-item">
1162
+ <input type="radio" name="db-core-location" value="auto" checked>
1163
+ <span>Automatic (nearest to you)</span>
1164
+ </label>
1165
+ <div class="radio-separator">Location Hints</div>
1166
+ <label class="radio-item">
1167
+ <input type="radio" name="db-core-location" value="wnam">
1168
+ <span>North America (West)</span>
1169
+ </label>
1170
+ <label class="radio-item">
1171
+ <input type="radio" name="db-core-location" value="enam">
1172
+ <span>North America (East)</span>
1173
+ </label>
1174
+ <label class="radio-item">
1175
+ <input type="radio" name="db-core-location" value="weur">
1176
+ <span>Europe (West)</span>
1177
+ </label>
1178
+ <label class="radio-item">
1179
+ <input type="radio" name="db-core-location" value="eeur">
1180
+ <span>Europe (East)</span>
1181
+ </label>
1182
+ <label class="radio-item">
1183
+ <input type="radio" name="db-core-location" value="apac">
1184
+ <span>Asia Pacific</span>
1185
+ </label>
1186
+ <label class="radio-item">
1187
+ <input type="radio" name="db-core-location" value="oc">
1188
+ <span>Oceania</span>
1189
+ </label>
1190
+ <div class="radio-separator">Jurisdiction (Compliance)</div>
1191
+ <label class="radio-item">
1192
+ <input type="radio" name="db-core-location" value="eu">
1193
+ <span>EU Jurisdiction (GDPR compliance)</span>
1194
+ </label>
1195
+ </div>
1196
+ </div>
1197
+ </div>
1198
+
1199
+ <!-- PII Database -->
1200
+ <div class="database-card">
1201
+ <h3>🔒 PII Database</h3>
1202
+ <div class="db-description">
1203
+ <p>Stores personal user data including:</p>
1204
+ <ul>
1205
+ <li>User profiles (name, email, phone)</li>
1206
+ <li>Passkey/WebAuthn credentials</li>
1207
+ <li>User preferences and settings</li>
1208
+ <li>Any custom user attributes</li>
1209
+ </ul>
1210
+ <p class="db-hint">This database contains personal data. Consider placing it in a region that complies with your data protection requirements.</p>
1211
+ </div>
1212
+
1213
+ <div class="region-selection">
1214
+ <h4>Region</h4>
1215
+ <div class="radio-group">
1216
+ <label class="radio-item">
1217
+ <input type="radio" name="db-pii-location" value="auto" checked>
1218
+ <span>Automatic (nearest to you)</span>
1219
+ </label>
1220
+ <div class="radio-separator">Location Hints</div>
1221
+ <label class="radio-item">
1222
+ <input type="radio" name="db-pii-location" value="wnam">
1223
+ <span>North America (West)</span>
1224
+ </label>
1225
+ <label class="radio-item">
1226
+ <input type="radio" name="db-pii-location" value="enam">
1227
+ <span>North America (East)</span>
1228
+ </label>
1229
+ <label class="radio-item">
1230
+ <input type="radio" name="db-pii-location" value="weur">
1231
+ <span>Europe (West)</span>
1232
+ </label>
1233
+ <label class="radio-item">
1234
+ <input type="radio" name="db-pii-location" value="eeur">
1235
+ <span>Europe (East)</span>
1236
+ </label>
1237
+ <label class="radio-item">
1238
+ <input type="radio" name="db-pii-location" value="apac">
1239
+ <span>Asia Pacific</span>
1240
+ </label>
1241
+ <label class="radio-item">
1242
+ <input type="radio" name="db-pii-location" value="oc">
1243
+ <span>Oceania</span>
1244
+ </label>
1245
+ <div class="radio-separator">Jurisdiction (Compliance)</div>
1246
+ <label class="radio-item">
1247
+ <input type="radio" name="db-pii-location" value="eu">
1248
+ <span>EU Jurisdiction (GDPR compliance)</span>
1249
+ </label>
1250
+ </div>
1251
+ </div>
1252
+ </div>
1253
+ </div>
1254
+
1255
+ <div class="button-group">
1256
+ <button class="btn-secondary" id="btn-back-database">Back</button>
1257
+ <button class="btn-primary" id="btn-continue-database">Continue</button>
1258
+ </div>
1259
+ </div>
1260
+
1261
+ <!-- Step 4: Email Provider Configuration -->
1262
+ <div id="section-email" class="card hidden">
1263
+ <h2 class="card-title">📧 Email Provider</h2>
1264
+
1265
+ <p style="margin-bottom: 1rem; color: var(--text-muted);">
1266
+ Configure email sending for magic links and verification codes.
1267
+ You can configure this later if you prefer.
1268
+ </p>
1269
+
1270
+ <div class="radio-group" style="margin-bottom: 1.5rem;">
1271
+ <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px;">
1272
+ <input type="radio" name="email-setup-choice" value="later" checked>
1273
+ <span style="display: flex; flex-direction: column; gap: 0.25rem;">
1274
+ <strong>Configure later</strong>
1275
+ <small style="color: var(--text-muted);">Skip for now. Magic links will return URLs instead of sending emails.</small>
1276
+ </span>
1277
+ </label>
1278
+ <label class="radio-item" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 8px; margin-top: 0.5rem;">
1279
+ <input type="radio" name="email-setup-choice" value="configure">
1280
+ <span style="display: flex; flex-direction: column; gap: 0.25rem;">
1281
+ <strong>Configure Resend</strong>
1282
+ <small style="color: var(--text-muted);">Set up email sending with Resend (recommended for production).</small>
1283
+ </span>
1284
+ </label>
1285
+ </div>
1286
+
1287
+ <!-- Resend Configuration Form (hidden by default) -->
1288
+ <div id="resend-config-form" class="hidden" style="background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem;">
1289
+ <h3 style="margin: 0 0 1rem 0; font-size: 1rem;">🔑 Resend Configuration</h3>
1290
+
1291
+ <div class="alert alert-info" style="margin-bottom: 1rem;">
1292
+ <strong>📋 Before you begin:</strong>
1293
+ <ol style="margin: 0.5rem 0 0 1rem; padding: 0;">
1294
+ <li>Create a Resend account at <a href="https://resend.com" target="_blank" style="color: var(--primary);">resend.com</a></li>
1295
+ <li>Add and verify your domain at <a href="https://resend.com/domains" target="_blank" style="color: var(--primary);">Domains Dashboard</a></li>
1296
+ <li>Create an API key at <a href="https://resend.com/api-keys" target="_blank" style="color: var(--primary);">API Keys</a></li>
1297
+ </ol>
1298
+ </div>
1299
+
1300
+ <div class="form-group">
1301
+ <label for="resend-api-key">Resend API Key</label>
1302
+ <input type="password" id="resend-api-key" placeholder="re_xxxxxxxxxx" autocomplete="off">
1303
+ <small style="color: var(--text-muted);">Your API key starts with "re_"</small>
1304
+ </div>
1305
+
1306
+ <div class="form-group">
1307
+ <label for="email-from-address">From Email Address</label>
1308
+ <input type="email" id="email-from-address" placeholder="noreply@yourdomain.com" autocomplete="off">
1309
+ <small style="color: var(--text-muted);">Must be from a verified domain in your Resend account</small>
1310
+ </div>
1311
+
1312
+ <div class="form-group">
1313
+ <label for="email-from-name">From Display Name (optional)</label>
1314
+ <input type="text" id="email-from-name" placeholder="Authrim" autocomplete="off">
1315
+ <small style="color: var(--text-muted);">Displayed as the sender name in email clients</small>
1316
+ </div>
1317
+
1318
+ <div class="alert alert-warning" style="margin-top: 1rem;">
1319
+ <strong>⚠️ Domain Verification Required</strong>
1320
+ <p style="margin: 0.25rem 0 0 0; font-size: 0.875rem;">
1321
+ Before your domain is verified, emails can only be sent from <code>onboarding@resend.dev</code> (for testing).
1322
+ <a href="https://resend.com/docs/dashboard/domains/introduction" target="_blank" style="color: var(--primary);">Learn more about domain verification →</a>
1323
+ </p>
1324
+ </div>
1325
+ </div>
1326
+
1327
+ <div class="button-group">
1328
+ <button class="btn-secondary" id="btn-back-email">Back</button>
1329
+ <button class="btn-primary" id="btn-continue-email">Continue</button>
1330
+ </div>
1331
+ </div>
1332
+
1333
+ <!-- Step 5: Provisioning -->
1030
1334
  <div id="section-provision" class="card hidden">
1031
1335
  <h2 class="card-title">
1032
1336
  Resource Provisioning
@@ -1298,6 +1602,10 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1298
1602
  2: document.getElementById('step-2'),
1299
1603
  3: document.getElementById('step-3'),
1300
1604
  4: document.getElementById('step-4'),
1605
+ 5: document.getElementById('step-5'),
1606
+ 6: document.getElementById('step-6'),
1607
+ 7: document.getElementById('step-7'),
1608
+ 8: document.getElementById('step-8'),
1301
1609
  };
1302
1610
 
1303
1611
  const sections = {
@@ -1306,6 +1614,8 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1306
1614
  mode: document.getElementById('section-mode'),
1307
1615
  loadConfig: document.getElementById('section-load-config'),
1308
1616
  config: document.getElementById('section-config'),
1617
+ database: document.getElementById('section-database'),
1618
+ email: document.getElementById('section-email'),
1309
1619
  provision: document.getElementById('section-provision'),
1310
1620
  deploy: document.getElementById('section-deploy'),
1311
1621
  complete: document.getElementById('section-complete'),
@@ -1337,7 +1647,7 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1337
1647
  // Step navigation
1338
1648
  function setStep(step) {
1339
1649
  currentStep = step;
1340
- for (let i = 1; i <= 4; i++) {
1650
+ for (let i = 1; i <= 7; i++) {
1341
1651
  const el = steps[i];
1342
1652
  el.className = 'step ' + (i < step ? 'step-complete' : i === step ? 'step-active' : 'step-pending');
1343
1653
  }
@@ -1568,28 +1878,80 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1568
1878
  document.getElementById('btn-load-config').addEventListener('click', async () => {
1569
1879
  if (!loadedConfig) return;
1570
1880
 
1571
- // Use loaded config
1881
+ // Support both new format (v1.0.0) and old format (v0.1.x)
1882
+ const isNewFormat = loadedConfig.version === '1.0.0' || loadedConfig.environment?.prefix;
1883
+
1884
+ // Extract values (with fallback for old format)
1885
+ const env = isNewFormat
1886
+ ? loadedConfig.environment?.prefix
1887
+ : loadedConfig.env || 'prod';
1888
+
1889
+ const apiDomain = isNewFormat
1890
+ ? loadedConfig.urls?.api?.custom
1891
+ : loadedConfig.apiDomain;
1892
+
1893
+ const loginUiDomain = isNewFormat
1894
+ ? loadedConfig.urls?.loginUi?.custom
1895
+ : loadedConfig.loginUiDomain;
1896
+
1897
+ const adminUiDomain = isNewFormat
1898
+ ? loadedConfig.urls?.adminUi?.custom
1899
+ : loadedConfig.adminUiDomain;
1900
+
1901
+ const tenant = loadedConfig.tenant || {
1902
+ name: 'default',
1903
+ displayName: 'Default Tenant',
1904
+ multiTenant: false,
1905
+ };
1906
+
1907
+ const components = loadedConfig.components || {
1908
+ api: true,
1909
+ loginUi: true,
1910
+ adminUi: true,
1911
+ saml: false,
1912
+ async: false,
1913
+ vc: false,
1914
+ bridge: true,
1915
+ policy: true,
1916
+ };
1917
+
1918
+ // Build internal config
1572
1919
  config = {
1573
- env: loadedConfig.environment?.prefix || 'prod',
1574
- apiDomain: loadedConfig.urls?.api?.custom || null,
1575
- loginUiDomain: loadedConfig.urls?.loginUi?.custom || null,
1576
- adminUiDomain: loadedConfig.urls?.adminUi?.custom || null,
1577
- components: loadedConfig.components || {
1578
- api: true,
1579
- loginUi: true,
1580
- adminUi: true,
1581
- saml: false,
1582
- async: false,
1583
- vc: false,
1584
- },
1920
+ env,
1921
+ apiDomain: apiDomain || null,
1922
+ loginUiDomain: loginUiDomain || null,
1923
+ adminUiDomain: adminUiDomain || null,
1924
+ tenant,
1925
+ components,
1585
1926
  };
1586
1927
 
1587
1928
  // Set form values
1588
1929
  document.getElementById('env').value = config.env;
1589
- document.getElementById('api-domain').value = config.apiDomain || '';
1930
+ document.getElementById('base-domain').value = config.tenant?.baseDomain || config.apiDomain || '';
1590
1931
  document.getElementById('login-domain').value = config.loginUiDomain || '';
1591
1932
  document.getElementById('admin-domain').value = config.adminUiDomain || '';
1592
- // Trigger env input to update default labels
1933
+ document.getElementById('tenant-name').value = config.tenant?.name || 'default';
1934
+ document.getElementById('tenant-display').value = config.tenant?.displayName || 'Default Tenant';
1935
+ document.getElementById('naked-domain').checked = config.tenant?.nakedDomain || false;
1936
+
1937
+ // Set component checkboxes
1938
+ if (document.getElementById('comp-login-ui')) {
1939
+ document.getElementById('comp-login-ui').checked = components.loginUi !== false;
1940
+ }
1941
+ if (document.getElementById('comp-admin-ui')) {
1942
+ document.getElementById('comp-admin-ui').checked = components.adminUi !== false;
1943
+ }
1944
+ if (document.getElementById('comp-saml')) {
1945
+ document.getElementById('comp-saml').checked = components.saml === true;
1946
+ }
1947
+ if (document.getElementById('comp-async')) {
1948
+ document.getElementById('comp-async').checked = components.async === true;
1949
+ }
1950
+ if (document.getElementById('comp-vc')) {
1951
+ document.getElementById('comp-vc').checked = components.vc === true;
1952
+ }
1953
+
1954
+ // Trigger env input to update preview/default labels
1593
1955
  document.getElementById('env').dispatchEvent(new Event('input'));
1594
1956
 
1595
1957
  // Skip to provisioning if resources already exist
@@ -1864,15 +2226,145 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1864
2226
  updateResourcePreview(env);
1865
2227
  updateProvisionButtons();
1866
2228
 
1867
- setStep(3);
1868
- showSection('provision');
2229
+ // Go to database configuration step
2230
+ setStep(4);
2231
+ showSection('database');
1869
2232
  });
1870
2233
 
1871
2234
  document.getElementById('btn-back-config').addEventListener('click', () => {
1872
- setStep(2);
2235
+ // Go back to email configuration (previous step in the flow)
2236
+ setStep(5);
2237
+ showSection('email');
2238
+ });
2239
+
2240
+ // Database configuration handlers
2241
+ document.getElementById('btn-back-database').addEventListener('click', () => {
2242
+ setStep(3);
1873
2243
  showSection('config');
1874
2244
  });
1875
2245
 
2246
+ document.getElementById('btn-continue-database').addEventListener('click', () => {
2247
+ // Get selected values
2248
+ const coreLocation = document.querySelector('input[name="db-core-location"]:checked').value;
2249
+ const piiLocation = document.querySelector('input[name="db-pii-location"]:checked').value;
2250
+
2251
+ // Parse location vs jurisdiction
2252
+ function parseDbLocation(value) {
2253
+ if (value === 'eu') {
2254
+ return { location: 'auto', jurisdiction: 'eu' };
2255
+ }
2256
+ return { location: value, jurisdiction: 'none' };
2257
+ }
2258
+
2259
+ // Add database config to config object
2260
+ config.database = {
2261
+ core: parseDbLocation(coreLocation),
2262
+ pii: parseDbLocation(piiLocation),
2263
+ };
2264
+
2265
+ // Proceed to email configuration
2266
+ setStep(5);
2267
+ showSection('email');
2268
+ });
2269
+
2270
+ // Email configuration handlers
2271
+ // Toggle resend config form visibility
2272
+ document.querySelectorAll('input[name="email-setup-choice"]').forEach(radio => {
2273
+ radio.addEventListener('change', () => {
2274
+ const resendForm = document.getElementById('resend-config-form');
2275
+ const choice = document.querySelector('input[name="email-setup-choice"]:checked').value;
2276
+ if (choice === 'configure') {
2277
+ resendForm.classList.remove('hidden');
2278
+ } else {
2279
+ resendForm.classList.add('hidden');
2280
+ }
2281
+ });
2282
+ });
2283
+
2284
+ document.getElementById('btn-back-email').addEventListener('click', () => {
2285
+ setStep(4);
2286
+ showSection('database');
2287
+ });
2288
+
2289
+ document.getElementById('btn-continue-email').addEventListener('click', async () => {
2290
+ const choice = document.querySelector('input[name="email-setup-choice"]:checked').value;
2291
+ const btn = document.getElementById('btn-continue-email');
2292
+
2293
+ if (choice === 'configure') {
2294
+ // Validate and store email configuration
2295
+ const apiKey = document.getElementById('resend-api-key').value.trim();
2296
+ const fromAddress = document.getElementById('email-from-address').value.trim();
2297
+ const fromName = document.getElementById('email-from-name').value.trim();
2298
+
2299
+ // Validate API key format
2300
+ if (!apiKey) {
2301
+ alert('Please enter your Resend API key');
2302
+ return;
2303
+ }
2304
+ if (!apiKey.startsWith('re_')) {
2305
+ if (!confirm('API key does not start with "re_". This may not be a valid Resend API key. Continue anyway?')) {
2306
+ return;
2307
+ }
2308
+ }
2309
+
2310
+ // Validate email address
2311
+ if (!fromAddress) {
2312
+ alert('Please enter a From email address');
2313
+ return;
2314
+ }
2315
+ if (!fromAddress.includes('@')) {
2316
+ alert('Please enter a valid email address');
2317
+ return;
2318
+ }
2319
+
2320
+ // Save email configuration to server
2321
+ btn.disabled = true;
2322
+ btn.textContent = 'Saving...';
2323
+
2324
+ try {
2325
+ const result = await api('/email/configure', {
2326
+ method: 'POST',
2327
+ body: {
2328
+ env: config.env,
2329
+ provider: 'resend',
2330
+ apiKey: apiKey,
2331
+ fromAddress: fromAddress,
2332
+ fromName: fromName || undefined,
2333
+ },
2334
+ });
2335
+
2336
+ if (!result.success) {
2337
+ throw new Error(result.error || 'Failed to save email configuration');
2338
+ }
2339
+
2340
+ // Store email configuration (without apiKey for config file)
2341
+ config.email = {
2342
+ provider: 'resend',
2343
+ fromAddress: fromAddress,
2344
+ fromName: fromName || undefined,
2345
+ configured: true,
2346
+ };
2347
+ } catch (error) {
2348
+ alert('Failed to save email configuration: ' + error.message);
2349
+ btn.disabled = false;
2350
+ btn.textContent = 'Continue';
2351
+ return;
2352
+ }
2353
+
2354
+ btn.disabled = false;
2355
+ btn.textContent = 'Continue';
2356
+ } else {
2357
+ // Configure later - no email provider
2358
+ config.email = {
2359
+ provider: 'none',
2360
+ };
2361
+ }
2362
+
2363
+ // Proceed to provision
2364
+ setStep(6);
2365
+ showSection('provision');
2366
+ });
2367
+
1876
2368
  // Provision
1877
2369
  document.getElementById('btn-provision').addEventListener('click', async () => {
1878
2370
  const btn = document.getElementById('btn-provision');
@@ -1944,7 +2436,7 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1944
2436
 
1945
2437
  const result = await api('/provision', {
1946
2438
  method: 'POST',
1947
- body: { env: config.env },
2439
+ body: { env: config.env, databaseConfig: config.database },
1948
2440
  });
1949
2441
 
1950
2442
  // Stop polling
@@ -1990,12 +2482,12 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
1990
2482
 
1991
2483
  // Continue to Deploy button
1992
2484
  document.getElementById('btn-goto-deploy').addEventListener('click', () => {
1993
- setStep(4);
2485
+ setStep(7);
1994
2486
  showSection('deploy');
1995
2487
  });
1996
2488
 
1997
2489
  document.getElementById('btn-back-provision').addEventListener('click', () => {
1998
- setStep(3);
2490
+ setStep(6);
1999
2491
  // Update buttons based on provisioning status
2000
2492
  updateProvisionButtons();
2001
2493
  // Show resource preview if not completed
@@ -2077,7 +2569,12 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
2077
2569
  // Workers.dev - no tenant prefix (wildcard subdomains not supported)
2078
2570
  apiUrl = 'https://' + workersDomain;
2079
2571
  }
2572
+ // Login UI URL for setup page (setup page is in Login UI, not API)
2573
+ const pagesDomain = config.env + '-ar-ui.pages.dev';
2574
+ const loginUiUrl = config.loginUiDomain ? 'https://' + config.loginUiDomain : 'https://' + pagesDomain;
2575
+
2080
2576
  output.textContent += ' API URL: ' + apiUrl + '\\n';
2577
+ output.textContent += ' Login UI URL: ' + loginUiUrl + '\\n';
2081
2578
  output.textContent += ' Keys Dir: .keys/' + config.env + '\\n';
2082
2579
  scrollToBottom(log);
2083
2580
 
@@ -2087,7 +2584,7 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
2087
2584
  method: 'POST',
2088
2585
  body: {
2089
2586
  env: config.env,
2090
- baseUrl: apiUrl,
2587
+ baseUrl: loginUiUrl, // Setup page is in Login UI
2091
2588
  keysDir: '.keys',
2092
2589
  },
2093
2590
  });
@@ -2197,6 +2694,7 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
2197
2694
  urlsEl.appendChild(debugDiv);
2198
2695
  }
2199
2696
 
2697
+ setStep(8);
2200
2698
  showSection('complete');
2201
2699
  }
2202
2700
 
@@ -2279,19 +2777,97 @@ export function getHtmlTemplate(sessionToken, manageOnly) {
2279
2777
  }
2280
2778
  }
2281
2779
 
2282
- // Save configuration to file
2780
+ // Save configuration to file (AuthrimConfigSchema format)
2283
2781
  function saveConfigToFile() {
2284
- if (!config) {
2782
+ if (!config || !config.env) {
2285
2783
  alert('No configuration to save');
2286
2784
  return;
2287
2785
  }
2288
2786
 
2787
+ const now = new Date().toISOString();
2788
+ const env = config.env;
2789
+
2790
+ // Calculate auto-generated URLs
2791
+ const workersDomain = env + '-ar-router.workers.dev';
2792
+ const pagesDomain = env + '-ar-ui.pages.dev';
2793
+
2794
+ // Build issuer URL based on tenant settings
2795
+ let issuerAutoUrl = 'https://' + workersDomain;
2796
+ if (config.tenant && config.tenant.baseDomain) {
2797
+ if (config.tenant.nakedDomain) {
2798
+ issuerAutoUrl = 'https://' + config.tenant.baseDomain;
2799
+ } else {
2800
+ const tenantName = config.tenant.name || 'default';
2801
+ issuerAutoUrl = 'https://' + tenantName + '.' + config.tenant.baseDomain;
2802
+ }
2803
+ }
2804
+
2805
+ // Build config in AuthrimConfigSchema format
2289
2806
  const configToSave = {
2290
- ...config,
2291
- savedAt: new Date().toISOString(),
2292
- version: '0.1.36',
2807
+ version: '1.0.0',
2808
+ createdAt: now,
2809
+ updatedAt: now,
2810
+ environment: {
2811
+ prefix: env,
2812
+ },
2813
+ urls: {
2814
+ api: {
2815
+ custom: config.apiDomain || null,
2816
+ auto: config.apiDomain ? issuerAutoUrl : 'https://' + workersDomain,
2817
+ },
2818
+ loginUi: {
2819
+ custom: config.loginUiDomain || null,
2820
+ auto: 'https://' + pagesDomain,
2821
+ },
2822
+ adminUi: {
2823
+ custom: config.adminUiDomain || null,
2824
+ auto: 'https://' + pagesDomain + '/admin',
2825
+ },
2826
+ },
2827
+ tenant: {
2828
+ name: config.tenant?.name || 'default',
2829
+ displayName: config.tenant?.displayName || 'Default Tenant',
2830
+ multiTenant: config.tenant?.multiTenant || false,
2831
+ baseDomain: config.tenant?.baseDomain || undefined,
2832
+ },
2833
+ components: config.components || {
2834
+ api: true,
2835
+ loginUi: true,
2836
+ adminUi: true,
2837
+ saml: false,
2838
+ async: false,
2839
+ vc: false,
2840
+ bridge: true,
2841
+ policy: true,
2842
+ },
2843
+ keys: {
2844
+ secretsPath: './.keys/',
2845
+ },
2846
+ database: config.database || {
2847
+ core: { location: 'auto', jurisdiction: 'none' },
2848
+ pii: { location: 'auto', jurisdiction: 'none' },
2849
+ },
2850
+ features: {
2851
+ email: {
2852
+ provider: config.email?.provider || 'none',
2853
+ fromAddress: config.email?.fromAddress || undefined,
2854
+ fromName: config.email?.fromName || undefined,
2855
+ configured: config.email?.provider === 'resend' && config.email?.apiKey ? true : false,
2856
+ },
2857
+ },
2293
2858
  };
2294
2859
 
2860
+ // Remove undefined values for cleaner output
2861
+ if (!configToSave.tenant.baseDomain) {
2862
+ delete configToSave.tenant.baseDomain;
2863
+ }
2864
+ if (!configToSave.features.email.fromAddress) {
2865
+ delete configToSave.features.email.fromAddress;
2866
+ }
2867
+ if (!configToSave.features.email.fromName) {
2868
+ delete configToSave.features.email.fromName;
2869
+ }
2870
+
2295
2871
  const blob = new Blob([JSON.stringify(configToSave, null, 2)], { type: 'application/json' });
2296
2872
  const url = URL.createObjectURL(blob);
2297
2873
  const a = document.createElement('a');