@camstack/addon-auth 1.1.2 → 1.1.4

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 (21) hide show
  1. package/dist/{dist-JmuFs-U5.mjs → dist-BLBS0d1z.js} +458 -8
  2. package/dist/{dist-CXCR7sEk.js → dist-JUeAGU3c.mjs} +393 -37
  3. package/dist/magic-link/auth-magic-link.addon.js +113 -34
  4. package/dist/magic-link/auth-magic-link.addon.mjs +113 -34
  5. package/dist/oidc/auth-oidc.addon.js +38 -14
  6. package/dist/oidc/auth-oidc.addon.mjs +38 -14
  7. package/dist/webauthn/_stub.js +568 -0
  8. package/dist/webauthn/_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-Bg_T1-iY.mjs +156 -0
  9. package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-EnxrfqY5.mjs +26 -0
  10. package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-CQ-aEQ9b.mjs +26 -0
  11. package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare__react__loadShare__.js-BIIa6vDX.mjs +26 -0
  12. package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-BL2etuqg.mjs +26 -0
  13. package/dist/webauthn/auth-webauthn.addon.js +141 -5
  14. package/dist/webauthn/auth-webauthn.addon.mjs +136 -37
  15. package/dist/webauthn/dist-CYZr2fwk.mjs +2726 -0
  16. package/dist/webauthn/hostInit-Boe91Eeg.mjs +129 -0
  17. package/dist/webauthn/remoteEntry.js +134 -0
  18. package/dist/webauthn/remoteEntry.ssr.js +33 -0
  19. package/dist/webauthn/virtualExposes-Ckj56FAf.mjs +27 -0
  20. package/dist/webauthn/virtual_mf-exposes-ssr___mfe_internal__addon_auth_webauthn_widgets__remoteEntry_js-C5AzEzK5.mjs +10 -0
  21. package/package.json +30 -5
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../chunk-Cek0wNdY.js");
6
- const require_dist = require("../dist-CXCR7sEk.js");
6
+ const require_dist = require("../dist-BLBS0d1z.js");
7
7
  //#region src/magic-link/auth-magic-link.addon.ts
8
8
  /**
9
9
  * Magic-link authentication addon.
@@ -30,6 +30,7 @@ const require_dist = require("../dist-CXCR7sEk.js");
30
30
  * - Auto-provision unknown emails (currently rejected for safety).
31
31
  */
32
32
  var DEFAULT_CONFIG = {
33
+ enabled: false,
33
34
  displayName: "Email magic link",
34
35
  icon: "mail",
35
36
  publicOrigin: "",
@@ -38,21 +39,10 @@ var DEFAULT_CONFIG = {
38
39
  ttlSec: 600
39
40
  };
40
41
  var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
41
- kind = "magic-link";
42
- hasRedirectFlow = false;
43
- hasCredentialFlow = true;
44
- displayName;
45
- icon;
46
42
  constructor() {
47
43
  super({ ...DEFAULT_CONFIG });
48
- this.displayName = DEFAULT_CONFIG.displayName;
49
- this.icon = DEFAULT_CONFIG.icon;
50
44
  }
51
45
  async onInitialize() {
52
- Object.assign(this, {
53
- displayName: this.config.displayName || DEFAULT_CONFIG.displayName,
54
- icon: this.config.icon || DEFAULT_CONFIG.icon
55
- });
56
46
  const authProvider = {
57
47
  validateCredentials: async () => null,
58
48
  validateToken: async () => null,
@@ -63,30 +53,55 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
63
53
  throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
64
54
  }
65
55
  };
66
- const routes = [{
67
- method: "POST",
68
- path: "/request",
69
- access: "public",
70
- description: "Request a magic link be delivered to the given email",
71
- handler: async (req, reply) => this.handleRequest(req, reply)
72
- }, {
73
- method: "GET",
74
- path: "/login",
75
- access: "public",
76
- description: "Click target embedded in the magic-link email",
77
- handler: async (req, reply) => this.handleLogin(req, reply)
78
- }];
79
- const routeProvider = {
80
- id: "auth-magic-link",
81
- getRoutes: () => routes
82
- };
56
+ const routeProvider = require_dist.buildAddonRouteProvider("auth-magic-link", [
57
+ {
58
+ method: "GET",
59
+ path: "/start",
60
+ access: "public",
61
+ description: "Login-page landing: email-entry form that POSTs to /request",
62
+ handler: async (req, reply) => this.handleStart(req, reply)
63
+ },
64
+ {
65
+ method: "POST",
66
+ path: "/request",
67
+ access: "public",
68
+ description: "Request a magic link be delivered to the given email",
69
+ handler: async (req, reply) => this.handleRequest(req, reply)
70
+ },
71
+ {
72
+ method: "GET",
73
+ path: "/login",
74
+ access: "public",
75
+ description: "Click target embedded in the magic-link email",
76
+ handler: async (req, reply) => this.handleLogin(req, reply)
77
+ }
78
+ ]);
79
+ const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
83
80
  this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
81
+ return [
82
+ {
83
+ capability: require_dist.authProviderCapability,
84
+ provider: authProvider
85
+ },
86
+ {
87
+ capability: require_dist.addonRoutesCapability,
88
+ provider: routeProvider
89
+ },
90
+ {
91
+ capability: require_dist.loginMethodCapability,
92
+ provider: loginMethodProvider
93
+ }
94
+ ];
95
+ }
96
+ buildLoginMethods() {
97
+ if (!this.config.enabled) return [];
84
98
  return [{
85
- capability: require_dist.authProviderCapability,
86
- provider: authProvider
87
- }, {
88
- capability: require_dist.addonRoutesCapability,
89
- provider: routeProvider
99
+ kind: "redirect",
100
+ id: "auth-magic-link",
101
+ label: this.config.displayName || DEFAULT_CONFIG.displayName,
102
+ icon: this.config.icon || DEFAULT_CONFIG.icon,
103
+ startUrl: "/addon/auth-magic-link/start",
104
+ stage: "primary"
90
105
  }];
91
106
  }
92
107
  globalSettingsSchema() {
@@ -96,6 +111,13 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
96
111
  description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
97
112
  columns: 1,
98
113
  fields: [
114
+ this.field({
115
+ type: "boolean",
116
+ key: "enabled",
117
+ label: "Enabled",
118
+ description: "Offer the magic-link button on the login page. Leave off until email delivery is configured.",
119
+ default: DEFAULT_CONFIG.enabled
120
+ }),
99
121
  this.field({
100
122
  type: "text",
101
123
  key: "displayName",
@@ -169,6 +191,59 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
169
191
  for (const u of users) if (u.username.toLowerCase() === needle) return u;
170
192
  return null;
171
193
  }
194
+ /**
195
+ * Login-page landing for the magic-link `redirect` login method. Serves
196
+ * a self-contained email-entry page that POSTs to `/request`; the tiny
197
+ * inline script lives in the ADDON's served HTML (not the admin shell),
198
+ * keeping the login page free of any magic-link-specific JS. Always
199
+ * responds 200 (account-enumeration-safe, mirroring `/request`).
200
+ */
201
+ async handleStart(_req, reply) {
202
+ const title = this.config.displayName || DEFAULT_CONFIG.displayName;
203
+ const html = `<!doctype html>
204
+ <html lang="en"><head><meta charset="utf-8">
205
+ <meta name="viewport" content="width=device-width, initial-scale=1">
206
+ <title>${escapeHtml(title)}</title>
207
+ <style>
208
+ :root { color-scheme: dark; }
209
+ body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
210
+ background:#0b0d12; color:#e5e7eb; font:14px/1.5 system-ui,-apple-system,sans-serif; }
211
+ .card { width:100%; max-width:22rem; padding:1.5rem; border:1px solid #232733; border-radius:0.75rem;
212
+ background:#12151c; box-shadow:0 10px 30px rgba(0,0,0,.35); }
213
+ h1 { font-size:1rem; margin:0 0 1rem; }
214
+ label { font-size:.75rem; color:#9ca3af; display:block; margin-bottom:.35rem; }
215
+ input { width:100%; box-sizing:border-box; padding:.6rem .7rem; border-radius:.5rem;
216
+ border:1px solid #232733; background:#0b0d12; color:#e5e7eb; outline:none; }
217
+ input:focus { border-color:#6366f1; }
218
+ button { margin-top:1rem; width:100%; padding:.6rem; border:0; border-radius:.5rem; cursor:pointer;
219
+ background:#6366f1; color:#fff; font-weight:600; }
220
+ button:disabled { opacity:.5; cursor:default; }
221
+ .msg { margin-top:1rem; font-size:.8rem; color:#34d399; }
222
+ .err { color:#f87171; }
223
+ a { color:#9ca3af; font-size:.75rem; }
224
+ </style></head>
225
+ <body><form class="card" id="f">
226
+ <h1>${escapeHtml(title)}</h1>
227
+ <label for="email">Email address</label>
228
+ <input id="email" name="email" type="email" autocomplete="email" required autofocus placeholder="you@example.com">
229
+ <button id="b" type="submit">Send login link</button>
230
+ <div class="msg" id="m" hidden></div>
231
+ <p style="margin-top:1rem"><a href="/admin/login">Back to sign in</a></p>
232
+ </form>
233
+ <script>
234
+ var f=document.getElementById('f'),b=document.getElementById('b'),m=document.getElementById('m');
235
+ f.addEventListener('submit',function(e){e.preventDefault();b.disabled=true;m.hidden=true;m.className='msg';
236
+ fetch('/addon/auth-magic-link/request',{method:'POST',headers:{'content-type':'application/json'},
237
+ body:JSON.stringify({email:document.getElementById('email').value})})
238
+ .then(function(){m.textContent='If that email matches an account, a login link is on its way.';m.hidden=false;})
239
+ .catch(function(){m.className='msg err';m.textContent='Something went wrong. Please try again.';m.hidden=false;})
240
+ .finally(function(){b.disabled=false;});});
241
+ <\/script>
242
+ </body></html>`;
243
+ reply.code(200);
244
+ reply.header("Content-Type", "text/html; charset=utf-8");
245
+ reply.send(html);
246
+ }
172
247
  async handleRequest(req, reply) {
173
248
  try {
174
249
  const body = req.body ?? {};
@@ -235,6 +310,10 @@ var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
235
310
  reply.send("");
236
311
  }
237
312
  };
313
+ /** Minimal HTML-attribute/text escaper for the interpolated page title. */
314
+ function escapeHtml(value) {
315
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
316
+ }
238
317
  //#endregion
239
318
  exports.AuthMagicLinkAddon = AuthMagicLinkAddon;
240
319
  exports.default = AuthMagicLinkAddon;
@@ -1,4 +1,4 @@
1
- import { a as BaseAddon, i as errMsg, n as authProviderCapability, t as addonRoutesCapability } from "../dist-JmuFs-U5.mjs";
1
+ import { a as loginMethodCapability, c as BaseAddon, i as buildAddonRouteProvider, r as authProviderCapability, s as errMsg, t as addonRoutesCapability } from "../dist-JUeAGU3c.mjs";
2
2
  //#region src/magic-link/auth-magic-link.addon.ts
3
3
  /**
4
4
  * Magic-link authentication addon.
@@ -25,6 +25,7 @@ import { a as BaseAddon, i as errMsg, n as authProviderCapability, t as addonRou
25
25
  * - Auto-provision unknown emails (currently rejected for safety).
26
26
  */
27
27
  var DEFAULT_CONFIG = {
28
+ enabled: false,
28
29
  displayName: "Email magic link",
29
30
  icon: "mail",
30
31
  publicOrigin: "",
@@ -33,21 +34,10 @@ var DEFAULT_CONFIG = {
33
34
  ttlSec: 600
34
35
  };
35
36
  var AuthMagicLinkAddon = class extends BaseAddon {
36
- kind = "magic-link";
37
- hasRedirectFlow = false;
38
- hasCredentialFlow = true;
39
- displayName;
40
- icon;
41
37
  constructor() {
42
38
  super({ ...DEFAULT_CONFIG });
43
- this.displayName = DEFAULT_CONFIG.displayName;
44
- this.icon = DEFAULT_CONFIG.icon;
45
39
  }
46
40
  async onInitialize() {
47
- Object.assign(this, {
48
- displayName: this.config.displayName || DEFAULT_CONFIG.displayName,
49
- icon: this.config.icon || DEFAULT_CONFIG.icon
50
- });
51
41
  const authProvider = {
52
42
  validateCredentials: async () => null,
53
43
  validateToken: async () => null,
@@ -58,30 +48,55 @@ var AuthMagicLinkAddon = class extends BaseAddon {
58
48
  throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
59
49
  }
60
50
  };
61
- const routes = [{
62
- method: "POST",
63
- path: "/request",
64
- access: "public",
65
- description: "Request a magic link be delivered to the given email",
66
- handler: async (req, reply) => this.handleRequest(req, reply)
67
- }, {
68
- method: "GET",
69
- path: "/login",
70
- access: "public",
71
- description: "Click target embedded in the magic-link email",
72
- handler: async (req, reply) => this.handleLogin(req, reply)
73
- }];
74
- const routeProvider = {
75
- id: "auth-magic-link",
76
- getRoutes: () => routes
77
- };
51
+ const routeProvider = buildAddonRouteProvider("auth-magic-link", [
52
+ {
53
+ method: "GET",
54
+ path: "/start",
55
+ access: "public",
56
+ description: "Login-page landing: email-entry form that POSTs to /request",
57
+ handler: async (req, reply) => this.handleStart(req, reply)
58
+ },
59
+ {
60
+ method: "POST",
61
+ path: "/request",
62
+ access: "public",
63
+ description: "Request a magic link be delivered to the given email",
64
+ handler: async (req, reply) => this.handleRequest(req, reply)
65
+ },
66
+ {
67
+ method: "GET",
68
+ path: "/login",
69
+ access: "public",
70
+ description: "Click target embedded in the magic-link email",
71
+ handler: async (req, reply) => this.handleLogin(req, reply)
72
+ }
73
+ ]);
74
+ const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
78
75
  this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
76
+ return [
77
+ {
78
+ capability: authProviderCapability,
79
+ provider: authProvider
80
+ },
81
+ {
82
+ capability: addonRoutesCapability,
83
+ provider: routeProvider
84
+ },
85
+ {
86
+ capability: loginMethodCapability,
87
+ provider: loginMethodProvider
88
+ }
89
+ ];
90
+ }
91
+ buildLoginMethods() {
92
+ if (!this.config.enabled) return [];
79
93
  return [{
80
- capability: authProviderCapability,
81
- provider: authProvider
82
- }, {
83
- capability: addonRoutesCapability,
84
- provider: routeProvider
94
+ kind: "redirect",
95
+ id: "auth-magic-link",
96
+ label: this.config.displayName || DEFAULT_CONFIG.displayName,
97
+ icon: this.config.icon || DEFAULT_CONFIG.icon,
98
+ startUrl: "/addon/auth-magic-link/start",
99
+ stage: "primary"
85
100
  }];
86
101
  }
87
102
  globalSettingsSchema() {
@@ -91,6 +106,13 @@ var AuthMagicLinkAddon = class extends BaseAddon {
91
106
  description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
92
107
  columns: 1,
93
108
  fields: [
109
+ this.field({
110
+ type: "boolean",
111
+ key: "enabled",
112
+ label: "Enabled",
113
+ description: "Offer the magic-link button on the login page. Leave off until email delivery is configured.",
114
+ default: DEFAULT_CONFIG.enabled
115
+ }),
94
116
  this.field({
95
117
  type: "text",
96
118
  key: "displayName",
@@ -164,6 +186,59 @@ var AuthMagicLinkAddon = class extends BaseAddon {
164
186
  for (const u of users) if (u.username.toLowerCase() === needle) return u;
165
187
  return null;
166
188
  }
189
+ /**
190
+ * Login-page landing for the magic-link `redirect` login method. Serves
191
+ * a self-contained email-entry page that POSTs to `/request`; the tiny
192
+ * inline script lives in the ADDON's served HTML (not the admin shell),
193
+ * keeping the login page free of any magic-link-specific JS. Always
194
+ * responds 200 (account-enumeration-safe, mirroring `/request`).
195
+ */
196
+ async handleStart(_req, reply) {
197
+ const title = this.config.displayName || DEFAULT_CONFIG.displayName;
198
+ const html = `<!doctype html>
199
+ <html lang="en"><head><meta charset="utf-8">
200
+ <meta name="viewport" content="width=device-width, initial-scale=1">
201
+ <title>${escapeHtml(title)}</title>
202
+ <style>
203
+ :root { color-scheme: dark; }
204
+ body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
205
+ background:#0b0d12; color:#e5e7eb; font:14px/1.5 system-ui,-apple-system,sans-serif; }
206
+ .card { width:100%; max-width:22rem; padding:1.5rem; border:1px solid #232733; border-radius:0.75rem;
207
+ background:#12151c; box-shadow:0 10px 30px rgba(0,0,0,.35); }
208
+ h1 { font-size:1rem; margin:0 0 1rem; }
209
+ label { font-size:.75rem; color:#9ca3af; display:block; margin-bottom:.35rem; }
210
+ input { width:100%; box-sizing:border-box; padding:.6rem .7rem; border-radius:.5rem;
211
+ border:1px solid #232733; background:#0b0d12; color:#e5e7eb; outline:none; }
212
+ input:focus { border-color:#6366f1; }
213
+ button { margin-top:1rem; width:100%; padding:.6rem; border:0; border-radius:.5rem; cursor:pointer;
214
+ background:#6366f1; color:#fff; font-weight:600; }
215
+ button:disabled { opacity:.5; cursor:default; }
216
+ .msg { margin-top:1rem; font-size:.8rem; color:#34d399; }
217
+ .err { color:#f87171; }
218
+ a { color:#9ca3af; font-size:.75rem; }
219
+ </style></head>
220
+ <body><form class="card" id="f">
221
+ <h1>${escapeHtml(title)}</h1>
222
+ <label for="email">Email address</label>
223
+ <input id="email" name="email" type="email" autocomplete="email" required autofocus placeholder="you@example.com">
224
+ <button id="b" type="submit">Send login link</button>
225
+ <div class="msg" id="m" hidden></div>
226
+ <p style="margin-top:1rem"><a href="/admin/login">Back to sign in</a></p>
227
+ </form>
228
+ <script>
229
+ var f=document.getElementById('f'),b=document.getElementById('b'),m=document.getElementById('m');
230
+ f.addEventListener('submit',function(e){e.preventDefault();b.disabled=true;m.hidden=true;m.className='msg';
231
+ fetch('/addon/auth-magic-link/request',{method:'POST',headers:{'content-type':'application/json'},
232
+ body:JSON.stringify({email:document.getElementById('email').value})})
233
+ .then(function(){m.textContent='If that email matches an account, a login link is on its way.';m.hidden=false;})
234
+ .catch(function(){m.className='msg err';m.textContent='Something went wrong. Please try again.';m.hidden=false;})
235
+ .finally(function(){b.disabled=false;});});
236
+ <\/script>
237
+ </body></html>`;
238
+ reply.code(200);
239
+ reply.header("Content-Type", "text/html; charset=utf-8");
240
+ reply.send(html);
241
+ }
167
242
  async handleRequest(req, reply) {
168
243
  try {
169
244
  const body = req.body ?? {};
@@ -230,5 +305,9 @@ var AuthMagicLinkAddon = class extends BaseAddon {
230
305
  reply.send("");
231
306
  }
232
307
  };
308
+ /** Minimal HTML-attribute/text escaper for the interpolated page title. */
309
+ function escapeHtml(value) {
310
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
311
+ }
233
312
  //#endregion
234
313
  export { AuthMagicLinkAddon, AuthMagicLinkAddon as default };
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-Cek0wNdY.js");
6
- const require_dist = require("../dist-CXCR7sEk.js");
6
+ const require_dist = require("../dist-BLBS0d1z.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  //#region node_modules/jose/dist/webapi/lib/buffer_utils.js
@@ -1237,7 +1237,7 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1237
1237
  return this.handleCallback(code, state);
1238
1238
  }
1239
1239
  };
1240
- const routes = [{
1240
+ const routeProvider = require_dist.buildAddonRouteProvider("auth-oidc", [{
1241
1241
  method: "GET",
1242
1242
  path: "/:providerId/start",
1243
1243
  access: "public",
@@ -1249,19 +1249,43 @@ var AuthOidcAddon = class extends require_dist.BaseAddon {
1249
1249
  access: "public",
1250
1250
  description: "OIDC redirect callback for a configured provider",
1251
1251
  handler: async (req, reply) => this.handleCallbackRoute(req, reply)
1252
- }];
1253
- const routeProvider = {
1254
- id: "auth-oidc",
1255
- getRoutes: () => routes
1256
- };
1252
+ }]);
1253
+ const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
1257
1254
  this.ctx.logger.info("OIDC auth provider initialized", { meta: { providers: [...this.instances.keys()] } });
1258
- return [{
1259
- capability: require_dist.authProviderCapability,
1260
- provider: authProvider
1261
- }, {
1262
- capability: require_dist.addonRoutesCapability,
1263
- provider: routeProvider
1264
- }];
1255
+ return [
1256
+ {
1257
+ capability: require_dist.authProviderCapability,
1258
+ provider: authProvider
1259
+ },
1260
+ {
1261
+ capability: require_dist.addonRoutesCapability,
1262
+ provider: routeProvider
1263
+ },
1264
+ {
1265
+ capability: require_dist.loginMethodCapability,
1266
+ provider: loginMethodProvider
1267
+ }
1268
+ ];
1269
+ }
1270
+ /**
1271
+ * Build one `redirect` login-method per configured provider instance.
1272
+ * `startUrl` is the addon's public `/start` route for that instance;
1273
+ * the login page renders a generic button navigating to it.
1274
+ */
1275
+ buildLoginMethods() {
1276
+ const out = [];
1277
+ for (const inst of this.instances.values()) {
1278
+ const id = inst.config.id;
1279
+ out.push({
1280
+ kind: "redirect",
1281
+ id: `auth-oidc/${id}`,
1282
+ label: inst.config.displayName || DEFAULT_PROVIDER.displayName,
1283
+ icon: inst.config.icon || DEFAULT_PROVIDER.icon,
1284
+ startUrl: `/addon/auth-oidc/${id}/start`,
1285
+ stage: "primary"
1286
+ });
1287
+ }
1288
+ return out;
1265
1289
  }
1266
1290
  globalSettingsSchema() {
1267
1291
  return this.schema({ sections: [{
@@ -1,4 +1,4 @@
1
- import { a as BaseAddon, i as errMsg, n as authProviderCapability, t as addonRoutesCapability } from "../dist-JmuFs-U5.mjs";
1
+ import { a as loginMethodCapability, c as BaseAddon, i as buildAddonRouteProvider, r as authProviderCapability, s as errMsg, t as addonRoutesCapability } from "../dist-JUeAGU3c.mjs";
2
2
  import * as crypto$1 from "node:crypto";
3
3
  //#region node_modules/jose/dist/webapi/lib/buffer_utils.js
4
4
  var encoder = new TextEncoder();
@@ -1231,7 +1231,7 @@ var AuthOidcAddon = class extends BaseAddon {
1231
1231
  return this.handleCallback(code, state);
1232
1232
  }
1233
1233
  };
1234
- const routes = [{
1234
+ const routeProvider = buildAddonRouteProvider("auth-oidc", [{
1235
1235
  method: "GET",
1236
1236
  path: "/:providerId/start",
1237
1237
  access: "public",
@@ -1243,19 +1243,43 @@ var AuthOidcAddon = class extends BaseAddon {
1243
1243
  access: "public",
1244
1244
  description: "OIDC redirect callback for a configured provider",
1245
1245
  handler: async (req, reply) => this.handleCallbackRoute(req, reply)
1246
- }];
1247
- const routeProvider = {
1248
- id: "auth-oidc",
1249
- getRoutes: () => routes
1250
- };
1246
+ }]);
1247
+ const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
1251
1248
  this.ctx.logger.info("OIDC auth provider initialized", { meta: { providers: [...this.instances.keys()] } });
1252
- return [{
1253
- capability: authProviderCapability,
1254
- provider: authProvider
1255
- }, {
1256
- capability: addonRoutesCapability,
1257
- provider: routeProvider
1258
- }];
1249
+ return [
1250
+ {
1251
+ capability: authProviderCapability,
1252
+ provider: authProvider
1253
+ },
1254
+ {
1255
+ capability: addonRoutesCapability,
1256
+ provider: routeProvider
1257
+ },
1258
+ {
1259
+ capability: loginMethodCapability,
1260
+ provider: loginMethodProvider
1261
+ }
1262
+ ];
1263
+ }
1264
+ /**
1265
+ * Build one `redirect` login-method per configured provider instance.
1266
+ * `startUrl` is the addon's public `/start` route for that instance;
1267
+ * the login page renders a generic button navigating to it.
1268
+ */
1269
+ buildLoginMethods() {
1270
+ const out = [];
1271
+ for (const inst of this.instances.values()) {
1272
+ const id = inst.config.id;
1273
+ out.push({
1274
+ kind: "redirect",
1275
+ id: `auth-oidc/${id}`,
1276
+ label: inst.config.displayName || DEFAULT_PROVIDER.displayName,
1277
+ icon: inst.config.icon || DEFAULT_PROVIDER.icon,
1278
+ startUrl: `/addon/auth-oidc/${id}/start`,
1279
+ stage: "primary"
1280
+ });
1281
+ }
1282
+ return out;
1259
1283
  }
1260
1284
  globalSettingsSchema() {
1261
1285
  return this.schema({ sections: [{