emailengine-app 2.67.3 → 2.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.github/codeql/codeql-config.yml +16 -0
  2. package/.github/workflows/codeql.yml +102 -0
  3. package/.github/workflows/deploy.yml +6 -0
  4. package/.github/workflows/test.yml +6 -0
  5. package/CHANGELOG.md +36 -0
  6. package/SECURITY.md +80 -0
  7. package/SECURITY.txt +27 -0
  8. package/data/google-crawlers.json +7 -1
  9. package/lib/account.js +24 -1
  10. package/lib/api-routes/account-routes.js +12 -2
  11. package/lib/email-client/base-client.js +26 -20
  12. package/lib/email-client/gmail-client.js +14 -12
  13. package/lib/imapproxy/imap-core/lib/imap-command.js +1 -1
  14. package/lib/imapproxy/imap-core/lib/imap-connection.js +7 -0
  15. package/lib/imapproxy/imap-core/lib/imap-server.js +1 -1
  16. package/lib/imapproxy/imap-server.js +92 -29
  17. package/lib/oauth/external-account-config.js +132 -0
  18. package/lib/oauth/external-account-signer.js +256 -0
  19. package/lib/oauth/gmail.js +113 -14
  20. package/lib/oauth/verify-app.js +397 -0
  21. package/lib/oauth2-apps.js +51 -6
  22. package/lib/routes-ui.js +153 -1
  23. package/lib/schemas.js +80 -2
  24. package/lib/settings.js +1 -0
  25. package/lib/tools.js +15 -10
  26. package/package.json +28 -28
  27. package/sbom.json +1 -1
  28. package/server.js +3 -3
  29. package/static/js/ace/ace.js +1 -1
  30. package/static/js/ace/ext-searchbox.js +1 -1
  31. package/static/js/ace/mode-handlebars.js +1 -1
  32. package/static/js/ace/mode-html.js +1 -1
  33. package/static/js/ace/mode-javascript.js +1 -1
  34. package/static/js/ace/mode-markdown.js +1 -1
  35. package/static/js/ace/worker-html.js +1 -1
  36. package/static/js/ace/worker-javascript.js +1 -1
  37. package/static/js/ace/worker-json.js +1 -1
  38. package/static/licenses.html +145 -115
  39. package/translations/messages.pot +49 -49
  40. package/views/config/oauth/app.hbs +224 -0
  41. package/views/config/oauth/edit.hbs +69 -0
  42. package/views/config/oauth/new.hbs +69 -0
  43. package/views/partials/oauth_form.hbs +99 -32
  44. package/workers/api.js +91 -2
@@ -1,10 +1,10 @@
1
1
  msgid ""
2
2
  msgstr ""
3
3
  "Content-Type: text/plain; charset=ascii\n"
4
- "POT-Creation-Date: 2026-04-17 08:51+0000\n"
4
+ "POT-Creation-Date: 2026-06-01 09:16+0000\n"
5
5
 
6
6
  #: views/error.hbs:4
7
- #: workers/api.js:7103
7
+ #: workers/api.js:7192
8
8
  msgid "Something went wrong"
9
9
  msgstr ""
10
10
 
@@ -21,7 +21,7 @@ msgid "Dashboard"
21
21
  msgstr ""
22
22
 
23
23
  #: views/config/license.hbs:45
24
- #: lib/routes-ui.js:2497
24
+ #: lib/routes-ui.js:2509
25
25
  msgid "%d day"
26
26
  msgid_plural "%d days"
27
27
  msgstr[0] ""
@@ -104,14 +104,6 @@ msgstr ""
104
104
  msgid "Enter your email address"
105
105
  msgstr ""
106
106
 
107
- #: views/accounts/register/index.hbs:2
108
- msgid "Choose your email account provider"
109
- msgstr ""
110
-
111
- #: views/accounts/register/index.hbs:15
112
- msgid "Standard IMAP"
113
- msgstr ""
114
-
115
107
  #: views/accounts/register/imap.hbs:11
116
108
  msgid "Your name"
117
109
  msgstr ""
@@ -134,6 +126,14 @@ msgstr ""
134
126
  msgid "Continue"
135
127
  msgstr ""
136
128
 
129
+ #: views/accounts/register/index.hbs:2
130
+ msgid "Choose your email account provider"
131
+ msgstr ""
132
+
133
+ #: views/accounts/register/index.hbs:15
134
+ msgid "Standard IMAP"
135
+ msgstr ""
136
+
137
137
  #: views/accounts/register/imap-server.hbs:19
138
138
  msgid "IMAP"
139
139
  msgstr ""
@@ -249,62 +249,62 @@ msgstr ""
249
249
  msgid "Request failed."
250
250
  msgstr ""
251
251
 
252
- #: lib/routes-ui.js:384
252
+ #: lib/routes-ui.js:385
253
253
  #: lib/ui-routes/account-routes.js:60
254
254
  msgid "Delegated"
255
255
  msgstr ""
256
256
 
257
- #: lib/routes-ui.js:385
257
+ #: lib/routes-ui.js:386
258
258
  #: lib/ui-routes/account-routes.js:61
259
259
  msgid "Using credentials from \"%s\""
260
260
  msgstr ""
261
261
 
262
- #: lib/routes-ui.js:435
262
+ #: lib/routes-ui.js:436
263
263
  #: lib/ui-routes/account-routes.js:111
264
264
  msgid ""
265
265
  "Connection timed out. This usually occurs if you are behind a firewall or "
266
266
  "connecting to the wrong port."
267
267
  msgstr ""
268
268
 
269
- #: lib/routes-ui.js:438
269
+ #: lib/routes-ui.js:439
270
270
  #: lib/ui-routes/account-routes.js:114
271
271
  msgid "The server unexpectedly closed the connection."
272
272
  msgstr ""
273
273
 
274
- #: lib/routes-ui.js:441
274
+ #: lib/routes-ui.js:442
275
275
  #: lib/ui-routes/account-routes.js:117
276
276
  msgid ""
277
277
  "The server unexpectedly closed the connection. This usually happens when "
278
278
  "attempting to connect to a TLS port without TLS enabled."
279
279
  msgstr ""
280
280
 
281
- #: lib/routes-ui.js:446
281
+ #: lib/routes-ui.js:447
282
282
  #: lib/ui-routes/account-routes.js:122
283
283
  msgid ""
284
284
  "The server refused the connection. This typically occurs if the server is "
285
285
  "not running, is overloaded, or you are connecting to the wrong host or port."
286
286
  msgstr ""
287
287
 
288
- #: lib/routes-ui.js:573
288
+ #: lib/routes-ui.js:574
289
289
  msgid "Invalid API key for OpenAI"
290
290
  msgstr ""
291
291
 
292
- #: lib/routes-ui.js:2490
292
+ #: lib/routes-ui.js:2502
293
293
  msgid "Unknown"
294
294
  msgstr ""
295
295
 
296
- #: lib/routes-ui.js:2499
296
+ #: lib/routes-ui.js:2511
297
297
  msgid "Indefinite"
298
298
  msgstr ""
299
299
 
300
- #: lib/routes-ui.js:5133
301
- #: lib/routes-ui.js:5168
302
- #: lib/routes-ui.js:5283
303
- #: lib/routes-ui.js:5330
304
- #: lib/routes-ui.js:5577
305
- #: lib/routes-ui.js:5613
306
- #: workers/api.js:2279
307
- #: workers/api.js:2607
300
+ #: lib/routes-ui.js:5285
301
+ #: lib/routes-ui.js:5320
302
+ #: lib/routes-ui.js:5435
303
+ #: lib/routes-ui.js:5482
304
+ #: lib/routes-ui.js:5729
305
+ #: lib/routes-ui.js:5765
306
+ #: workers/api.js:2280
307
+ #: workers/api.js:2608
308
308
  #: lib/ui-routes/account-routes.js:554
309
309
  #: lib/ui-routes/account-routes.js:590
310
310
  #: lib/ui-routes/account-routes.js:707
@@ -314,44 +314,44 @@ msgstr ""
314
314
  msgid "Email Account Setup"
315
315
  msgstr ""
316
316
 
317
- #: lib/routes-ui.js:5193
318
- #: lib/routes-ui.js:5226
319
- #: lib/routes-ui.js:8186
317
+ #: lib/routes-ui.js:5345
318
+ #: lib/routes-ui.js:5378
319
+ #: lib/routes-ui.js:8338
320
320
  #: lib/ui-routes/account-routes.js:615
321
321
  #: lib/ui-routes/account-routes.js:649
322
322
  msgid "Invalid request. Check your input and try again."
323
323
  msgstr ""
324
324
 
325
- #: lib/routes-ui.js:5386
326
- #: lib/routes-ui.js:5397
325
+ #: lib/routes-ui.js:5538
326
+ #: lib/routes-ui.js:5549
327
327
  #: lib/ui-routes/account-routes.js:811
328
328
  #: lib/ui-routes/account-routes.js:822
329
329
  #: lib/ui-routes/admin-entities-routes.js:2020
330
330
  msgid "Server hostname was not found"
331
331
  msgstr ""
332
332
 
333
- #: lib/routes-ui.js:5389
334
- #: lib/routes-ui.js:5400
333
+ #: lib/routes-ui.js:5541
334
+ #: lib/routes-ui.js:5552
335
335
  #: lib/ui-routes/account-routes.js:814
336
336
  #: lib/ui-routes/account-routes.js:825
337
337
  #: lib/ui-routes/admin-entities-routes.js:2023
338
338
  msgid "Invalid username or password"
339
339
  msgstr ""
340
340
 
341
- #: lib/routes-ui.js:5403
341
+ #: lib/routes-ui.js:5555
342
342
  #: lib/ui-routes/account-routes.js:828
343
343
  #: lib/ui-routes/admin-entities-routes.js:2026
344
344
  msgid "Authentication credentials were not provided"
345
345
  msgstr ""
346
346
 
347
- #: lib/routes-ui.js:5406
347
+ #: lib/routes-ui.js:5558
348
348
  #: lib/ui-routes/account-routes.js:831
349
349
  #: lib/ui-routes/admin-entities-routes.js:2029
350
350
  msgid "OAuth2 authentication failed"
351
351
  msgstr ""
352
352
 
353
- #: lib/routes-ui.js:5409
354
- #: lib/routes-ui.js:5413
353
+ #: lib/routes-ui.js:5561
354
+ #: lib/routes-ui.js:5565
355
355
  #: lib/ui-routes/account-routes.js:834
356
356
  #: lib/ui-routes/account-routes.js:838
357
357
  #: lib/ui-routes/admin-entities-routes.js:2032
@@ -359,38 +359,38 @@ msgstr ""
359
359
  msgid "TLS protocol error"
360
360
  msgstr ""
361
361
 
362
- #: lib/routes-ui.js:5417
362
+ #: lib/routes-ui.js:5569
363
363
  #: lib/ui-routes/account-routes.js:842
364
364
  #: lib/ui-routes/admin-entities-routes.js:2040
365
365
  msgid "Connection timed out"
366
366
  msgstr ""
367
367
 
368
- #: lib/routes-ui.js:5420
368
+ #: lib/routes-ui.js:5572
369
369
  #: lib/ui-routes/account-routes.js:845
370
370
  #: lib/ui-routes/admin-entities-routes.js:2043
371
371
  msgid "Could not connect to server"
372
372
  msgstr ""
373
373
 
374
- #: lib/routes-ui.js:5423
374
+ #: lib/routes-ui.js:5575
375
375
  #: lib/ui-routes/account-routes.js:848
376
376
  #: lib/ui-routes/admin-entities-routes.js:2046
377
377
  msgid "Unexpected server response"
378
378
  msgstr ""
379
379
 
380
- #: lib/routes-ui.js:8149
380
+ #: lib/routes-ui.js:8301
381
381
  #: lib/tools.js:950
382
382
  msgid "Invalid input"
383
383
  msgstr ""
384
384
 
385
- #: lib/routes-ui.js:8159
386
- #: lib/routes-ui.js:8277
387
- #: lib/routes-ui.js:8294
388
- #: lib/routes-ui.js:8330
385
+ #: lib/routes-ui.js:8311
386
+ #: lib/routes-ui.js:8429
387
+ #: lib/routes-ui.js:8446
388
+ #: lib/routes-ui.js:8482
389
389
  msgid "Subscription Management"
390
390
  msgstr ""
391
391
 
392
- #: workers/api.js:7102
393
- #: workers/api.js:7219
392
+ #: workers/api.js:7191
393
+ #: workers/api.js:7308
394
394
  msgid "Requested page not found"
395
395
  msgstr ""
396
396
 
@@ -33,6 +33,11 @@
33
33
  <i class="fas fa-user-edit fa-fw"></i> Edit app
34
34
  </a>
35
35
 
36
+ <button type="button" class="btn btn-light" data-toggle="modal" data-target="#verifySetupModal"
37
+ title="Run live checks against the provider" id="verify-btn" data-placement="top">
38
+ <i class="fas fa-clipboard-check fa-fw"></i> Verify setup
39
+ </button>
40
+
36
41
  <button type="button" class="btn btn-light" data-toggle="modal" data-target="#deleteModal"
37
42
  title="Delete this application" id="delete-btn" data-placement="top">
38
43
  <i class="fas fa-trash-alt fa-fw"></i> Delete app
@@ -40,6 +45,16 @@
40
45
 
41
46
  </div>
42
47
 
48
+ {{#if canAddServiceAccount}}
49
+ <div class="btn-group ml-auto mb-1" role="group" aria-label="Account actions">
50
+ <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addServiceAccountModal"
51
+ title="Register an email account that authenticates through this service application"
52
+ id="add-service-account-btn" data-placement="top">
53
+ <i class="fas fa-user-plus fa-fw"></i> Add account
54
+ </button>
55
+ </div>
56
+ {{/if}}
57
+
43
58
  </div>
44
59
 
45
60
  {{#if app.description}}
@@ -164,6 +179,22 @@
164
179
  <dd class="col-sm-9 text-monospace"><small>*****</small></dd>
165
180
  {{/if}}
166
181
 
182
+ {{#if appShowAuthMethod}}
183
+ <dt class="col-sm-3">Authentication method</dt>
184
+ <dd class="col-sm-9">
185
+ {{#if authMethodIsExternalAccount}}
186
+ Workload Identity Federation
187
+ {{else}}
188
+ Service account key
189
+ {{/if}}
190
+ </dd>
191
+ {{/if}}
192
+
193
+ {{#if app.externalAccount}}
194
+ <dt class="col-sm-3">External account config</dt>
195
+ <dd class="col-sm-9 text-monospace"><small>***** (set)</small></dd>
196
+ {{/if}}
197
+
167
198
  {{#if app.redirectUrl}}
168
199
  <dt class="col-sm-3">Redirect URL</dt>
169
200
  <dd class="col-sm-9 text-monospace"><small>{{app.redirectUrl}}</small></dd>
@@ -337,6 +368,199 @@
337
368
  </div>
338
369
  </div>
339
370
 
371
+ <div class="modal fade" id="verifySetupModal" tabindex="-1" aria-labelledby="verifySetupModalLabel" aria-hidden="true">
372
+ <div class="modal-dialog modal-lg">
373
+ <div class="modal-content">
374
+ <div class="modal-header">
375
+ <h5 class="modal-title" id="verifySetupModalLabel">Verify OAuth2 setup</h5>
376
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
377
+ <span aria-hidden="true">&times;</span>
378
+ </button>
379
+ </div>
380
+ <div class="modal-body">
381
+ <input type="hidden" id="verify-crumb" value="{{crumb}}" />
382
+ <p><small class="text-muted">Runs the provider authentication chain step by step and reports what passes
383
+ or fails.{{#if appShowAuthMethod}} Enter a mailbox address in your Workspace domain to also verify
384
+ domain-wide delegation and live mailbox access.{{/if}}</small></p>
385
+
386
+ <div class="form-group">
387
+ <div class="input-group">
388
+ <input type="email" class="form-control" id="verify-account"
389
+ placeholder="mailbox@example.com (optional)" autocomplete="off" />
390
+ <div class="input-group-append">
391
+ <button type="button" class="btn btn-primary" id="verify-run-btn">
392
+ <i class="fas fa-play fa-fw"></i> Run test
393
+ </button>
394
+ </div>
395
+ </div>
396
+ <small class="form-text text-muted">Leave empty to check configuration and signing only.</small>
397
+ </div>
398
+
399
+ <div id="verify-pending" class="d-none">
400
+ <p><i class="fas fa-spinner fa-spin fa-sm"></i> Running checks, please wait&mldr;</p>
401
+ </div>
402
+ <div id="verify-error" class="d-none">
403
+ <small class="alert alert-danger text-monospace error-message" style="display: block;"></small>
404
+ </div>
405
+ <div id="verify-verdict" class="d-none mb-3"></div>
406
+ <ul id="verify-steps" class="list-group"></ul>
407
+ </div>
408
+ <div class="modal-footer">
409
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
410
+ </div>
411
+ </div>
412
+ </div>
413
+ </div>
414
+
415
+ {{#if canAddServiceAccount}}
416
+ <div class="modal fade" id="addServiceAccountModal" tabindex="-1" aria-labelledby="addServiceAccountLabel"
417
+ aria-hidden="true">
418
+ <div class="modal-dialog">
419
+ <div class="modal-content">
420
+ <div class="modal-header">
421
+ <h5 class="modal-title" id="addServiceAccountLabel">Add an account</h5>
422
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
423
+ <span aria-hidden="true">&times;</span>
424
+ </button>
425
+ </div>
426
+ <form method="post" action="/admin/config/oauth/app/{{app.id}}/add-account">
427
+ <input type="hidden" name="crumb" value="{{crumb}}">
428
+ <div class="modal-body">
429
+ <p><small class="text-muted">This service application authenticates without an interactive consent
430
+ screen, so the account is registered directly. The mailbox must be reachable with the
431
+ application's service credentials.</small></p>
432
+ <div class="form-group">
433
+ <label for="service-account-name" class="col-form-label">Full name:</label>
434
+ <input type="text" class="form-control" id="service-account-name" name="name"
435
+ placeholder="e.g., John Smith">
436
+ </div>
437
+ <div class="form-group">
438
+ <label for="service-account-email" class="col-form-label">Email address:</label>
439
+ <input type="email" class="form-control" id="service-account-email" name="email"
440
+ placeholder="e.g., user@example.com" required>
441
+ </div>
442
+ <div class="form-group">
443
+ <label for="service-account-id" class="col-form-label">Account identifier (optional):</label>
444
+ <input type="text" class="form-control" id="service-account-id" name="account"
445
+ placeholder="e.g., account_123">
446
+ <small class="form-text text-muted">Leave blank to auto-generate. Existing accounts with this ID
447
+ will be updated.</small>
448
+ </div>
449
+ </div>
450
+ <div class="modal-footer">
451
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
452
+ <button type="submit" class="btn btn-primary btn-icon-split">
453
+ <span class="icon text-white-50">
454
+ <i class="fas fa-user-plus"></i>
455
+ </span>
456
+ <span class="text">Add account</span>
457
+ </button>
458
+ </div>
459
+ </form>
460
+ </div>
461
+ </div>
462
+ </div>
463
+
464
+ <script>
465
+ document.addEventListener('DOMContentLoaded', () => {
466
+ $('#addServiceAccountModal').on('shown.bs.modal', () => {
467
+ document.getElementById('service-account-name').focus();
468
+ });
469
+ });
470
+ </script>
471
+ {{/if}}
472
+
473
+ <script>
474
+ document.addEventListener('DOMContentLoaded', () => {
475
+ const appId = "{{app.id}}";
476
+ const stepsElm = document.getElementById('verify-steps');
477
+ const pendingElm = document.getElementById('verify-pending');
478
+ const errorElm = document.getElementById('verify-error');
479
+ const verdictElm = document.getElementById('verify-verdict');
480
+ const runBtn = document.getElementById('verify-run-btn');
481
+ const accountInput = document.getElementById('verify-account');
482
+
483
+ const ICONS = {
484
+ ok: '<i class="fas fa-check-circle text-success fa-fw"></i>',
485
+ fail: '<i class="fas fa-times-circle text-danger fa-fw"></i>',
486
+ skip: '<i class="fas fa-minus-circle text-muted fa-fw"></i>'
487
+ };
488
+
489
+ const esc = str => {
490
+ let div = document.createElement('div');
491
+ div.textContent = str == null ? '' : String(str);
492
+ return div.innerHTML;
493
+ };
494
+
495
+ const renderResult = data => {
496
+ stepsElm.innerHTML = '';
497
+ (data.steps || []).forEach(step => {
498
+ let li = document.createElement('li');
499
+ li.className = 'list-group-item';
500
+ let html = `${ICONS[step.status] || ''} <strong>${esc(step.label)}</strong>`;
501
+ if (step.message) {
502
+ html += `<div class="small text-muted ml-4">${esc(step.message)}</div>`;
503
+ }
504
+ if (step.hint) {
505
+ html += `<div class="small text-info ml-4"><i class="fas fa-lightbulb fa-fw"></i> ${esc(step.hint)}</div>`;
506
+ }
507
+ li.innerHTML = html;
508
+ stepsElm.appendChild(li);
509
+ });
510
+
511
+ let failed = (data.steps || []).some(s => s.status === 'fail');
512
+ verdictElm.className = 'mb-3 alert ' + (failed ? 'alert-danger' : 'alert-success');
513
+ verdictElm.textContent = failed
514
+ ? 'Setup has problems - review the failing step(s) below.'
515
+ : 'Setup verified - all checks passed.';
516
+ verdictElm.classList.remove('d-none');
517
+ };
518
+
519
+ const runVerify = async account => {
520
+ errorElm.classList.add('d-none');
521
+ verdictElm.classList.add('d-none');
522
+ stepsElm.innerHTML = '';
523
+ pendingElm.classList.remove('d-none');
524
+ runBtn.disabled = true;
525
+ try {
526
+ let res = await fetch(`/admin/config/oauth/verify/${encodeURIComponent(appId)}`, {
527
+ method: 'post',
528
+ headers: { 'content-type': 'application/json' },
529
+ body: JSON.stringify({
530
+ crumb: document.getElementById('verify-crumb').value,
531
+ account: account || '',
532
+ testConnection: true
533
+ })
534
+ });
535
+ let data = await res.json();
536
+ if (!res.ok) {
537
+ throw new Error(data.error || `HTTP ${res.status}`);
538
+ }
539
+ renderResult(data);
540
+ } catch (err) {
541
+ errorElm.querySelector('.error-message').textContent = err.message;
542
+ errorElm.classList.remove('d-none');
543
+ } finally {
544
+ pendingElm.classList.add('d-none');
545
+ runBtn.disabled = false;
546
+ }
547
+ };
548
+
549
+ $('#verifySetupModal').on('shown.bs.modal', () => {
550
+ // Auto-run the no-mailbox checks (config + signing chain) on open.
551
+ runVerify(accountInput.value.trim());
552
+ });
553
+
554
+ runBtn.addEventListener('click', () => runVerify(accountInput.value.trim()));
555
+ accountInput.addEventListener('keydown', e => {
556
+ if (e.key === 'Enter') {
557
+ e.preventDefault();
558
+ runVerify(accountInput.value.trim());
559
+ }
560
+ });
561
+ });
562
+ </script>
563
+
340
564
  <script>
341
565
  document.addEventListener('DOMContentLoaded', () => {
342
566
  // not set up by default as this element has a different data-toggle
@@ -89,6 +89,75 @@
89
89
  });
90
90
  }
91
91
 
92
+ let externalAccountFileElm = document.getElementById('externalAccountFile');
93
+ if (externalAccountFileElm) {
94
+ externalAccountFileElm.addEventListener('click', e => {
95
+ e.preventDefault();
96
+ browseFileContents('text').then(jsonStr => {
97
+ if (!jsonStr) {
98
+ return;
99
+ }
100
+
101
+ let data;
102
+ try {
103
+ data = JSON.parse(jsonStr);
104
+ } catch (err) {
105
+ return showToast('Selected file is not JSON formatted', 'alert-triangle');
106
+ }
107
+
108
+ if (data.type !== 'external_account') {
109
+ if (data.type === 'service_account') {
110
+ return showToast('This is a service account key file, not a Workload Identity Federation config. Switch to the "Service account key" tab to load it.', 'alert-triangle');
111
+ }
112
+ return showToast(`Selected file is not an external_account credential config (type: ${data.type || 'missing'})`, 'alert-triangle');
113
+ }
114
+
115
+ let impersonationUrl = data.service_account_impersonation_url || '';
116
+ let emailMatch = impersonationUrl.match(/\/serviceAccounts\/([^/]+):generateAccessToken$/);
117
+ if (emailMatch) {
118
+ let derivedEmail = decodeURIComponent(emailMatch[1]);
119
+ let emailField = document.getElementById('serviceClientEmail');
120
+ if (emailField && !emailField.value) {
121
+ emailField.value = derivedEmail;
122
+ }
123
+ }
124
+
125
+ document.getElementById('externalAccount').value = JSON.stringify(data, null, 2);
126
+ return showToast('Loaded external account configuration from file', 'check-circle');
127
+ }).catch(err => {
128
+ console.error(err);
129
+ return showToast('Failed to load external account file', 'alert-triangle');
130
+ });
131
+ });
132
+ }
133
+
134
+ let authMethodTabs = document.querySelectorAll('.auth-method-tab');
135
+ let authMethodInput = document.getElementById('authMethod');
136
+ if (authMethodTabs.length && authMethodInput) {
137
+ let updateSections = selected => {
138
+ document.querySelectorAll('.auth-method-section').forEach(section => {
139
+ let active = section.classList.contains('auth-method-section-' + selected);
140
+ section.classList.toggle('d-none', !active);
141
+ });
142
+ };
143
+ authMethodTabs.forEach(tab => {
144
+ tab.addEventListener('click', e => {
145
+ e.preventDefault();
146
+ if (tab.classList.contains('disabled') || tab.classList.contains('active')) {
147
+ return;
148
+ }
149
+ let selected = tab.getAttribute('data-auth-method');
150
+ authMethodTabs.forEach(other => {
151
+ let isActive = other === tab;
152
+ other.classList.toggle('active', isActive);
153
+ other.setAttribute('aria-selected', isActive ? 'true' : 'false');
154
+ });
155
+ authMethodInput.value = selected;
156
+ updateSections(selected);
157
+ });
158
+ });
159
+ }
160
+
92
161
  });
93
162
 
94
163
  </script>
@@ -87,6 +87,75 @@
87
87
  });
88
88
  }
89
89
 
90
+ let externalAccountFileElm = document.getElementById('externalAccountFile');
91
+ if (externalAccountFileElm) {
92
+ externalAccountFileElm.addEventListener('click', e => {
93
+ e.preventDefault();
94
+ browseFileContents('text').then(jsonStr => {
95
+ if (!jsonStr) {
96
+ return;
97
+ }
98
+
99
+ let data;
100
+ try {
101
+ data = JSON.parse(jsonStr);
102
+ } catch (err) {
103
+ return showToast('Selected file is not JSON formatted', 'alert-triangle');
104
+ }
105
+
106
+ if (data.type !== 'external_account') {
107
+ if (data.type === 'service_account') {
108
+ return showToast('This is a service account key file, not a Workload Identity Federation config. Switch to the "Service account key" tab to load it.', 'alert-triangle');
109
+ }
110
+ return showToast(`Selected file is not an external_account credential config (type: ${data.type || 'missing'})`, 'alert-triangle');
111
+ }
112
+
113
+ let impersonationUrl = data.service_account_impersonation_url || '';
114
+ let emailMatch = impersonationUrl.match(/\/serviceAccounts\/([^/]+):generateAccessToken$/);
115
+ if (emailMatch) {
116
+ let derivedEmail = decodeURIComponent(emailMatch[1]);
117
+ let emailField = document.getElementById('serviceClientEmail');
118
+ if (emailField && !emailField.value) {
119
+ emailField.value = derivedEmail;
120
+ }
121
+ }
122
+
123
+ document.getElementById('externalAccount').value = JSON.stringify(data, null, 2);
124
+ return showToast('Loaded external account configuration from file', 'check-circle');
125
+ }).catch(err => {
126
+ console.error(err);
127
+ return showToast('Failed to load external account file', 'alert-triangle');
128
+ });
129
+ });
130
+ }
131
+
132
+ let authMethodTabs = document.querySelectorAll('.auth-method-tab');
133
+ let authMethodInput = document.getElementById('authMethod');
134
+ if (authMethodTabs.length && authMethodInput) {
135
+ let updateSections = selected => {
136
+ document.querySelectorAll('.auth-method-section').forEach(section => {
137
+ let active = section.classList.contains('auth-method-section-' + selected);
138
+ section.classList.toggle('d-none', !active);
139
+ });
140
+ };
141
+ authMethodTabs.forEach(tab => {
142
+ tab.addEventListener('click', e => {
143
+ e.preventDefault();
144
+ if (tab.classList.contains('disabled') || tab.classList.contains('active')) {
145
+ return;
146
+ }
147
+ let selected = tab.getAttribute('data-auth-method');
148
+ authMethodTabs.forEach(other => {
149
+ let isActive = other === tab;
150
+ other.classList.toggle('active', isActive);
151
+ other.setAttribute('aria-selected', isActive ? 'true' : 'false');
152
+ });
153
+ authMethodInput.value = selected;
154
+ updateSections(selected);
155
+ });
156
+ });
157
+ }
158
+
90
159
  });
91
160
 
92
161
  </script>