@camstack/addon-auth 1.2.134 → 1.2.136

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.
@@ -1,5 +1,4 @@
1
- import { d as boolean, f as number, l as BaseAddon, m as string, n as addonWidgetsSourceCapability, o as loginMethodCapability, p as object, s as userPasskeysCapability, u as array } from "../dist-aH7jUuvS.mjs";
2
- import { t as hubBrowserOrigin } from "../hub-browser-origin-LBBO5r05.mjs";
1
+ import { c as userPasskeysCapability, d as array, f as boolean, h as string, m as object, p as number, r as addonWidgetsSourceCapability, s as loginMethodCapability, u as BaseAddon } from "../dist-DMFGQOff.mjs";
3
2
  import { createRequire } from "node:module";
4
3
  import Stream from "stream";
5
4
  import http from "http";
@@ -12134,6 +12133,16 @@ async function verifyAuthenticationResponse(options) {
12134
12133
  * @module @simplewebauthn/server
12135
12134
  */
12136
12135
  //#endregion
12136
+ //#region src/hub-browser-origin.ts
12137
+ async function hubBrowserOrigin(localNetwork, logger) {
12138
+ try {
12139
+ return (await localNetwork.getAddresses.query()).roles.publicEndpoint.resolved;
12140
+ } catch (err) {
12141
+ logger.warn("hub address unavailable from local-network — using the localhost default", { meta: { error: err instanceof Error ? err.message : String(err) } });
12142
+ return null;
12143
+ }
12144
+ }
12145
+ //#endregion
12137
12146
  //#region src/webauthn/passkey-store.ts
12138
12147
  /**
12139
12148
  * Persistence layer for passkeys (credentials). One row per
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.332",
39
+ version: "1.2.336",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.127",
48
+ version: "1.2.128",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.253",
84
+ version: "1.2.257",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-C_bpdIIi.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-BSmYiv_-.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@camstack/addon-auth",
3
- "version": "1.2.134",
4
- "description": "Authentication bundle — magic-link, OIDC, and WebAuthn/passkey. Multi-entry npm package shipping 3 addons under a single bundle; each addon keeps its own id, capabilities, and runner.",
3
+ "version": "1.2.136",
4
+ "description": "Authentication bundle — OIDC and WebAuthn/passkey. Multi-entry npm package shipping 2 addons under a single bundle; each addon keeps its own id, capabilities, and runner.",
5
5
  "keywords": [
6
6
  "camstack",
7
7
  "addon",
8
8
  "camstack-addon",
9
9
  "auth",
10
- "magic-link",
11
10
  "oidc",
12
11
  "sso",
13
12
  "webauthn",
@@ -18,12 +17,8 @@
18
17
  "type": "git",
19
18
  "url": "https://github.com/camstack/server"
20
19
  },
21
- "main": "./dist/magic-link/auth-magic-link.addon.js",
20
+ "main": "./dist/oidc/auth-oidc.addon.js",
22
21
  "exports": {
23
- "./magic-link": {
24
- "import": "./dist/magic-link/auth-magic-link.addon.mjs",
25
- "require": "./dist/magic-link/auth-magic-link.addon.js"
26
- },
27
22
  "./oidc": {
28
23
  "import": "./dist/oidc/auth-oidc.addon.mjs",
29
24
  "require": "./dist/oidc/auth-oidc.addon.js"
@@ -42,32 +37,9 @@
42
37
  "displayName": "Authentication",
43
38
  "bundle": {
44
39
  "displayName": "Authentication",
45
- "description": "Magic-link, OIDC SSO, and WebAuthn/passkey login providers."
40
+ "description": "OIDC SSO and WebAuthn/passkey login providers."
46
41
  },
47
42
  "addons": [
48
- {
49
- "id": "auth-magic-link",
50
- "execution": {
51
- "group": "auth"
52
- },
53
- "category": "system",
54
- "name": "Magic Link",
55
- "icon": "assets/magic-link/icon.svg",
56
- "description": "Passwordless email login. Sends a one-time link to the user's email; clicking it logs them in. SMTP delivery is configured per-deployment.",
57
- "entry": "./dist/magic-link/auth-magic-link.addon.js",
58
- "capabilities": [
59
- {
60
- "name": "auth-provider"
61
- },
62
- {
63
- "name": "addon-routes"
64
- },
65
- {
66
- "name": "login-method"
67
- }
68
- ],
69
- "protected": true
70
- },
71
43
  {
72
44
  "id": "auth-oidc",
73
45
  "execution": {
@@ -1,5 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#e11d48" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
- <path d="M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8 19 13M17.8 6.2 19 5M12.2 6.2 11 5"/>
3
- <path d="m3 21 9-9"/>
4
- <path d="M15 9h.01"/>
5
- </svg>
@@ -1,64 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
9
- var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
- var __exportAll = (all, no_symbols) => {
11
- let target = {};
12
- for (var name in all) __defProp(target, name, {
13
- get: all[name],
14
- enumerable: true
15
- });
16
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
17
- return target;
18
- };
19
- var __copyProps = (to, from, except, desc) => {
20
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
21
- key = keys[i];
22
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
23
- get: ((k) => from[k]).bind(null, key),
24
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
25
- });
26
- }
27
- return to;
28
- };
29
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
30
- value: mod,
31
- enumerable: true
32
- }) : target, mod));
33
- var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
- //#endregion
35
- Object.defineProperty(exports, "__commonJSMin", {
36
- enumerable: true,
37
- get: function() {
38
- return __commonJSMin;
39
- }
40
- });
41
- Object.defineProperty(exports, "__esmMin", {
42
- enumerable: true,
43
- get: function() {
44
- return __esmMin;
45
- }
46
- });
47
- Object.defineProperty(exports, "__exportAll", {
48
- enumerable: true,
49
- get: function() {
50
- return __exportAll;
51
- }
52
- });
53
- Object.defineProperty(exports, "__toCommonJS", {
54
- enumerable: true,
55
- get: function() {
56
- return __toCommonJS;
57
- }
58
- });
59
- Object.defineProperty(exports, "__toESM", {
60
- enumerable: true,
61
- get: function() {
62
- return __toESM;
63
- }
64
- });
@@ -1,16 +0,0 @@
1
- //#region src/hub-browser-origin.ts
2
- async function hubBrowserOrigin(localNetwork, logger) {
3
- try {
4
- return (await localNetwork.getAddresses.query()).roles.publicEndpoint.resolved;
5
- } catch (err) {
6
- logger.warn("hub address unavailable from local-network — using the localhost default", { meta: { error: err instanceof Error ? err.message : String(err) } });
7
- return null;
8
- }
9
- }
10
- //#endregion
11
- Object.defineProperty(exports, "hubBrowserOrigin", {
12
- enumerable: true,
13
- get: function() {
14
- return hubBrowserOrigin;
15
- }
16
- });
@@ -1,11 +0,0 @@
1
- //#region src/hub-browser-origin.ts
2
- async function hubBrowserOrigin(localNetwork, logger) {
3
- try {
4
- return (await localNetwork.getAddresses.query()).roles.publicEndpoint.resolved;
5
- } catch (err) {
6
- logger.warn("hub address unavailable from local-network — using the localhost default", { meta: { error: err instanceof Error ? err.message : String(err) } });
7
- return null;
8
- }
9
- }
10
- //#endregion
11
- export { hubBrowserOrigin as t };
@@ -1,418 +0,0 @@
1
- Object.defineProperties(exports, {
2
- __esModule: { value: true },
3
- [Symbol.toStringTag]: { value: "Module" }
4
- });
5
- require("../chunk-Cek0wNdY.js");
6
- const require_dist = require("../dist-D5CZYR3c.js");
7
- const require_hub_browser_origin = require("../hub-browser-origin-DohQUFPz.js");
8
- //#region src/magic-link/retire-enabled-flag.ts
9
- /** The retired settings key. */
10
- var RETIRED_ENABLED_KEY = "enabled";
11
- /** Store key marking the retirement as done. Not a schema field. */
12
- var RETIREMENT_MARKER_KEY = "enabledFlagRetired";
13
- function deferred(reason) {
14
- return {
15
- kind: "deferred",
16
- reason
17
- };
18
- }
19
- function isMarked(read) {
20
- return read.kind === "present" && read.value === true;
21
- }
22
- async function retireEnabledFlag(deps) {
23
- const marker = await deps.readStoredKey(RETIREMENT_MARKER_KEY);
24
- if (marker.kind === "unreadable") return deferred("settings store unreadable (marker)");
25
- if (isMarked(marker)) return { kind: "already-retired" };
26
- const flag = await deps.readStoredKey(RETIRED_ENABLED_KEY);
27
- if (flag.kind === "unreadable") return deferred("settings store unreadable (enabled)");
28
- const wasOn = flag.kind === "present" && flag.value === true;
29
- if (!wasOn) try {
30
- await deps.disableAtCapability();
31
- } catch (err) {
32
- return deferred(`capability toggle refused: ${err instanceof Error ? err.message : String(err)}`);
33
- }
34
- await deps.commit();
35
- if (!isMarked(await deps.readStoredKey("enabledFlagRetired"))) return deferred("retirement write did not land");
36
- return {
37
- kind: "retired",
38
- capDisabled: !wasOn
39
- };
40
- }
41
- //#endregion
42
- //#region src/magic-link/auth-magic-link.addon.ts
43
- /**
44
- * Magic-link authentication addon.
45
- *
46
- * Passwordless flow:
47
- * 1. Operator types their email on the login page → clicks "Send link".
48
- * 2. The addon's `/request` route accepts the email, looks up the user
49
- * via `user-management.listUsers()`, mints a short-lived HMAC bridge
50
- * token (via `sso-bridge` cap), and delivers a URL of the form
51
- * `https://<hub>/addon/auth-magic-link/login?bridge=<token>` to the
52
- * user's email.
53
- * 3. Email click hits `/login?bridge=…` → 302 to `/api/auth/sso/finish?bridge=…`
54
- * → hub verifies the bridge token and mints the real session.
55
- *
56
- * Delivery is pluggable. This addon ships a "log-only" mode by default,
57
- * which records that a link was requested but never writes the link
58
- * itself (it is a login credential — D783). Production deployments
59
- * configure the SMTP relay via global settings.
60
- *
61
- * Open follow-ups:
62
- * - Wire `nodemailer` for real SMTP delivery (peer dep; the framework
63
- * declares `smtpProvider` cap if/when we ship an SMTP addon).
64
- * - Throttle requests per-email (rate-limit window).
65
- * - Auto-provision unknown emails (currently rejected for safety).
66
- */
67
- /** Capability whose per-provider toggle is magic-link's ONE enable (Identity → Authentication). */
68
- var GOVERNING_CAPABILITY = "auth-provider";
69
- /** Backoff between retirement attempts — the first ones are refused until the
70
- * provider registration reaches the hub. After the last, the next boot retries. */
71
- var RETIREMENT_RETRY_DELAYS_MS = [
72
- 1e3,
73
- 2e3,
74
- 5e3,
75
- 1e4,
76
- 3e4,
77
- 6e4
78
- ];
79
- var DEFAULT_CONFIG = {
80
- displayName: "Email magic link",
81
- icon: "mailbox",
82
- publicOrigin: "",
83
- deliveryMode: "log-only",
84
- defaultRole: "viewer",
85
- ttlSec: 600
86
- };
87
- var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
88
- enabledFlagRetired = false;
89
- retirementTimer = null;
90
- shuttingDown = false;
91
- constructor() {
92
- super({ ...DEFAULT_CONFIG });
93
- }
94
- async onInitialize() {
95
- const authProvider = {
96
- validateCredentials: async () => null,
97
- validateToken: async () => null,
98
- getLoginUrl: async () => {
99
- throw new Error("Magic link does not support direct getLoginUrl — POST email to /addon/auth-magic-link/request");
100
- },
101
- handleCallback: async () => {
102
- throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
103
- }
104
- };
105
- const routeProvider = require_dist.buildAddonRouteProvider("auth-magic-link", [
106
- {
107
- method: "GET",
108
- path: "/start",
109
- access: "public",
110
- description: "Login-page landing: email-entry form that POSTs to /request",
111
- handler: async (req, reply) => this.handleStart(req, reply)
112
- },
113
- {
114
- method: "POST",
115
- path: "/request",
116
- access: "public",
117
- description: "Request a magic link be delivered to the given email",
118
- handler: async (req, reply) => this.handleRequest(req, reply)
119
- },
120
- {
121
- method: "GET",
122
- path: "/login",
123
- access: "public",
124
- description: "Click target embedded in the magic-link email",
125
- handler: async (req, reply) => this.handleLogin(req, reply)
126
- }
127
- ]);
128
- const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
129
- this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
130
- this.shuttingDown = false;
131
- this.attemptEnabledFlagRetirement(0);
132
- return [
133
- {
134
- capability: require_dist.authProviderCapability,
135
- provider: authProvider
136
- },
137
- {
138
- capability: require_dist.addonRoutesCapability,
139
- provider: routeProvider
140
- },
141
- {
142
- capability: require_dist.loginMethodCapability,
143
- provider: loginMethodProvider
144
- }
145
- ];
146
- }
147
- buildLoginMethods() {
148
- if (!this.enabledFlagRetired) return [];
149
- return [{
150
- kind: "redirect",
151
- id: "auth-magic-link",
152
- label: this.config.displayName || DEFAULT_CONFIG.displayName,
153
- icon: require_dist.isDisplayIconKey(this.config.icon) ? this.config.icon : DEFAULT_CONFIG.icon,
154
- startUrl: "/addon/auth-magic-link/start",
155
- stage: "primary"
156
- }];
157
- }
158
- async onShutdown() {
159
- this.shuttingDown = true;
160
- if (this.retirementTimer !== null) clearTimeout(this.retirementTimer);
161
- this.retirementTimer = null;
162
- }
163
- /** One attempt of the D777 carry-over; reschedules itself while deferred. */
164
- async attemptEnabledFlagRetirement(attempt) {
165
- if (this.shuttingDown) return;
166
- let outcome;
167
- try {
168
- outcome = await retireEnabledFlag({
169
- readStoredKey: (key) => this.readStoredGlobalKey(key),
170
- disableAtCapability: async () => {
171
- await this.ctx.api.addons.setCapabilityProviderEnabled.mutate({
172
- capName: GOVERNING_CAPABILITY,
173
- addonId: this.ctx.id,
174
- enabled: false
175
- });
176
- },
177
- commit: async () => {
178
- const patch = {
179
- [RETIRED_ENABLED_KEY]: void 0,
180
- [RETIREMENT_MARKER_KEY]: true
181
- };
182
- await this.updateGlobalSettings(patch);
183
- }
184
- });
185
- } catch (err) {
186
- outcome = {
187
- kind: "deferred",
188
- reason: require_dist.errMsg(err)
189
- };
190
- }
191
- if (outcome.kind !== "deferred") {
192
- this.enabledFlagRetired = true;
193
- if (outcome.kind === "retired") this.ctx.logger.info(outcome.capDisabled ? "Magic-link: the retired `enabled` setting was off — the provider is now DISABLED on Identity → Authentication; enable it there to offer magic-link login." : "Magic-link: the retired `enabled` setting was on — the provider stays enabled on Identity → Authentication.");
194
- return;
195
- }
196
- const delay = RETIREMENT_RETRY_DELAYS_MS[attempt];
197
- if (delay === void 0) {
198
- this.ctx.logger.warn("Magic-link: carrying the retired `enabled` setting over to the capability toggle failed — login button withheld until the next boot", { meta: { reason: outcome.reason } });
199
- return;
200
- }
201
- this.ctx.logger.debug("Magic-link: enabled-flag retirement deferred", { meta: {
202
- reason: outcome.reason,
203
- attempt,
204
- retryInMs: delay
205
- } });
206
- this.retirementTimer = setTimeout(() => {
207
- this.retirementTimer = null;
208
- this.attemptEnabledFlagRetirement(attempt + 1);
209
- }, delay);
210
- this.retirementTimer.unref?.();
211
- }
212
- globalSettingsSchema() {
213
- return this.schema({ sections: [{
214
- id: "magic-link",
215
- title: "Magic Link Settings",
216
- description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
217
- columns: 1,
218
- fields: [
219
- this.field({
220
- type: "text",
221
- key: "displayName",
222
- label: "Display name",
223
- default: DEFAULT_CONFIG.displayName
224
- }),
225
- this.field({
226
- type: "icon",
227
- key: "icon",
228
- label: "Icon",
229
- description: "Shown on the login button.",
230
- default: DEFAULT_CONFIG.icon
231
- }),
232
- this.field({
233
- type: "text",
234
- key: "publicOrigin",
235
- label: "Public origin",
236
- description: "Where the email link points. Empty = the hub address from Network (public tunnel, then mesh, then LAN).",
237
- default: ""
238
- }),
239
- this.field({
240
- type: "select",
241
- key: "deliveryMode",
242
- label: "Delivery mode",
243
- description: "log-only records the request in the addon log but never the link (it is a login credential). smtp uses the configured SMTP relay.",
244
- default: DEFAULT_CONFIG.deliveryMode,
245
- options: [{
246
- value: "log-only",
247
- label: "Log-only (no email sent)"
248
- }, {
249
- value: "smtp",
250
- label: "SMTP (when configured)"
251
- }]
252
- }),
253
- this.field({
254
- type: "select",
255
- key: "defaultRole",
256
- label: "Default role",
257
- default: DEFAULT_CONFIG.defaultRole,
258
- options: [{
259
- value: "viewer",
260
- label: "Viewer"
261
- }, {
262
- value: "admin",
263
- label: "Admin"
264
- }]
265
- }),
266
- this.field({
267
- type: "number",
268
- key: "ttlSec",
269
- label: "Link TTL (seconds)",
270
- description: "How long the magic link stays valid. Default 600 (10 min).",
271
- default: DEFAULT_CONFIG.ttlSec
272
- })
273
- ]
274
- }] });
275
- }
276
- async buildPublicOrigin() {
277
- const configured = this.config.publicOrigin?.trim();
278
- if (configured) return configured.replace(/\/+$/, "");
279
- return await require_hub_browser_origin.hubBrowserOrigin(this.ctx.api.localNetwork, this.ctx.logger) ?? "https://localhost:4443";
280
- }
281
- async findUserByEmail(email) {
282
- let users;
283
- try {
284
- users = await this.ctx.api.userManagement.listUsers.query();
285
- } catch (err) {
286
- this.ctx.logger.warn("Magic-link user lookup failed (user-management cap unavailable)", { meta: { message: require_dist.errMsg(err) } });
287
- return null;
288
- }
289
- const needle = email.trim().toLowerCase();
290
- for (const u of users) if (u.username.toLowerCase() === needle) return u;
291
- return null;
292
- }
293
- /**
294
- * Login-page landing for the magic-link `redirect` login method. Serves
295
- * a self-contained email-entry page that POSTs to `/request`; the tiny
296
- * inline script lives in the ADDON's served HTML (not the admin shell),
297
- * keeping the login page free of any magic-link-specific JS. Always
298
- * responds 200 (account-enumeration-safe, mirroring `/request`).
299
- */
300
- async handleStart(_req, reply) {
301
- const title = this.config.displayName || DEFAULT_CONFIG.displayName;
302
- const html = `<!doctype html>
303
- <html lang="en"><head><meta charset="utf-8">
304
- <meta name="viewport" content="width=device-width, initial-scale=1">
305
- <title>${escapeHtml(title)}</title>
306
- <style>
307
- :root { color-scheme: dark; }
308
- body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
309
- background:#0b0d12; color:#e5e7eb; font:14px/1.5 system-ui,-apple-system,sans-serif; }
310
- .card { width:100%; max-width:22rem; padding:1.5rem; border:1px solid #232733; border-radius:0.75rem;
311
- background:#12151c; box-shadow:0 10px 30px rgba(0,0,0,.35); }
312
- h1 { font-size:1rem; margin:0 0 1rem; }
313
- label { font-size:.75rem; color:#9ca3af; display:block; margin-bottom:.35rem; }
314
- input { width:100%; box-sizing:border-box; padding:.6rem .7rem; border-radius:.5rem;
315
- border:1px solid #232733; background:#0b0d12; color:#e5e7eb; outline:none; }
316
- input:focus { border-color:#6366f1; }
317
- button { margin-top:1rem; width:100%; padding:.6rem; border:0; border-radius:.5rem; cursor:pointer;
318
- background:#6366f1; color:#fff; font-weight:600; }
319
- button:disabled { opacity:.5; cursor:default; }
320
- .msg { margin-top:1rem; font-size:.8rem; color:#34d399; }
321
- .err { color:#f87171; }
322
- a { color:#9ca3af; font-size:.75rem; }
323
- </style></head>
324
- <body><form class="card" id="f">
325
- <h1>${escapeHtml(title)}</h1>
326
- <label for="email">Email address</label>
327
- <input id="email" name="email" type="email" autocomplete="email" required autofocus placeholder="you@example.com">
328
- <button id="b" type="submit">Send login link</button>
329
- <div class="msg" id="m" hidden></div>
330
- <p style="margin-top:1rem"><a href="/login">Back to sign in</a></p>
331
- </form>
332
- <script>
333
- var f=document.getElementById('f'),b=document.getElementById('b'),m=document.getElementById('m');
334
- f.addEventListener('submit',function(e){e.preventDefault();b.disabled=true;m.hidden=true;m.className='msg';
335
- fetch('/addon/auth-magic-link/request',{method:'POST',headers:{'content-type':'application/json'},
336
- body:JSON.stringify({email:document.getElementById('email').value})})
337
- .then(function(){m.textContent='If that email matches an account, a login link is on its way.';m.hidden=false;})
338
- .catch(function(){m.className='msg err';m.textContent='Something went wrong. Please try again.';m.hidden=false;})
339
- .finally(function(){b.disabled=false;});});
340
- <\/script>
341
- </body></html>`;
342
- reply.code(200);
343
- reply.header("Content-Type", "text/html; charset=utf-8");
344
- reply.send(html);
345
- }
346
- async handleRequest(req, reply) {
347
- try {
348
- const body = req.body ?? {};
349
- const email = typeof body.email === "string" ? body.email.trim() : "";
350
- if (!email || !email.includes("@")) {
351
- reply.code(400);
352
- reply.send({ error: "A valid email address is required" });
353
- return;
354
- }
355
- const ok = { sent: true };
356
- const user = await this.findUserByEmail(email);
357
- if (!user) {
358
- reply.code(200);
359
- reply.send(ok);
360
- return;
361
- }
362
- let token;
363
- try {
364
- ({token} = await this.ctx.api.ssoBridge.signBridgeToken.query({
365
- claims: {
366
- userId: user.id,
367
- username: user.username,
368
- isAdmin: user.isAdmin,
369
- provider: "auth-magic-link",
370
- email
371
- },
372
- ttlSec: this.config.ttlSec || DEFAULT_CONFIG.ttlSec
373
- }));
374
- } catch (err) {
375
- this.ctx.logger.error("Magic-link bridge-token minting failed (sso-bridge unavailable)", { meta: { message: require_dist.errMsg(err) } });
376
- reply.code(503);
377
- reply.send({ error: "sso-bridge capability unavailable" });
378
- return;
379
- }
380
- `${await this.buildPublicOrigin()}${encodeURIComponent(token)}`;
381
- if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated — not delivered (log-only mode, link withheld)", { meta: {
382
- email,
383
- ttlSec: this.config.ttlSec,
384
- delivered: false
385
- } });
386
- else this.ctx.logger.warn("Magic link not delivered: SMTP mode requested but no smtp-provider cap registered", { meta: {
387
- email,
388
- delivered: false
389
- } });
390
- reply.code(200);
391
- reply.send(ok);
392
- } catch (err) {
393
- reply.code(500);
394
- reply.send({
395
- error: "Magic-link request failed",
396
- message: require_dist.errMsg(err)
397
- });
398
- }
399
- }
400
- async handleLogin(req, reply) {
401
- const bridge = req.query["bridge"];
402
- if (!bridge) {
403
- reply.code(400);
404
- reply.send({ error: "Missing bridge token" });
405
- return;
406
- }
407
- reply.code(302);
408
- reply.header("Location", `/api/auth/sso/finish?bridge=${encodeURIComponent(bridge)}`);
409
- reply.send("");
410
- }
411
- };
412
- /** Minimal HTML-attribute/text escaper for the interpolated page title. */
413
- function escapeHtml(value) {
414
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
415
- }
416
- //#endregion
417
- exports.AuthMagicLinkAddon = AuthMagicLinkAddon;
418
- exports.default = AuthMagicLinkAddon;