@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.
- package/dist/{dist-aH7jUuvS.mjs → dist-CaBcilKz.js} +1799 -132
- package/dist/{dist-D5CZYR3c.js → dist-DMFGQOff.mjs} +1646 -216
- package/dist/oidc/auth-oidc.addon.js +67 -17
- package/dist/oidc/auth-oidc.addon.mjs +66 -15
- package/dist/webauthn/_stub.js +55 -7
- package/dist/webauthn/{_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-C_bpdIIi.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-BSmYiv_-.mjs} +3 -3
- package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DdVWgcLV.mjs +26 -0
- package/dist/webauthn/auth-webauthn.addon.js +36 -28
- package/dist/webauthn/auth-webauthn.addon.mjs +11 -2
- package/dist/webauthn/{hostInit-BqtiJT2W.mjs → hostInit-MuJOFBLm.mjs} +3 -3
- package/dist/webauthn/remoteEntry.js +1 -1
- package/package.json +4 -32
- package/assets/magic-link/icon.svg +0 -5
- package/dist/chunk-Cek0wNdY.js +0 -64
- package/dist/hub-browser-origin-DohQUFPz.js +0 -16
- package/dist/hub-browser-origin-LBBO5r05.mjs +0 -11
- package/dist/magic-link/auth-magic-link.addon.js +0 -418
- package/dist/magic-link/auth-magic-link.addon.mjs +0 -412
- package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-ClmCVIsL.mjs +0 -26
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
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.
|
|
4
|
-
"description": "Authentication bundle —
|
|
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/
|
|
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": "
|
|
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>
|
package/dist/chunk-Cek0wNdY.js
DELETED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
415
|
-
}
|
|
416
|
-
//#endregion
|
|
417
|
-
exports.AuthMagicLinkAddon = AuthMagicLinkAddon;
|
|
418
|
-
exports.default = AuthMagicLinkAddon;
|