@camstack/addon-auth 1.2.130 → 1.2.131
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/{hub-browser-origin-B1o5vmQX.mjs → hub-browser-origin-BRU-HVqq.mjs} +906 -482
- package/dist/{hub-browser-origin-C-u2l9VC.js → hub-browser-origin-CCWSbHYb.js} +911 -481
- package/dist/magic-link/auth-magic-link.addon.js +122 -24
- package/dist/magic-link/auth-magic-link.addon.mjs +122 -24
- package/dist/oidc/auth-oidc.addon.js +60 -30
- package/dist/oidc/auth-oidc.addon.mjs +60 -30
- package/dist/webauthn/_stub.js +1 -1
- package/dist/webauthn/{_virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-v7lvfZM3.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_auth_webauthn_widgets-DjJfFQA5.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-CMFCCqdv.mjs +26 -0
- package/dist/webauthn/auth-webauthn.addon.js +1 -1
- package/dist/webauthn/auth-webauthn.addon.mjs +1 -1
- package/dist/webauthn/{hostInit-DEFeGgRf.mjs → hostInit-BCMxziLA.mjs} +3 -3
- package/dist/webauthn/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/webauthn/_virtual_mf___mfe_internal__addon_auth_webauthn_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bifweq9M.mjs +0 -26
|
@@ -3,7 +3,41 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_hub_browser_origin = require("../hub-browser-origin-
|
|
6
|
+
const require_hub_browser_origin = require("../hub-browser-origin-CCWSbHYb.js");
|
|
7
|
+
//#region src/magic-link/retire-enabled-flag.ts
|
|
8
|
+
/** The retired settings key. */
|
|
9
|
+
var RETIRED_ENABLED_KEY = "enabled";
|
|
10
|
+
/** Store key marking the retirement as done. Not a schema field. */
|
|
11
|
+
var RETIREMENT_MARKER_KEY = "enabledFlagRetired";
|
|
12
|
+
function deferred(reason) {
|
|
13
|
+
return {
|
|
14
|
+
kind: "deferred",
|
|
15
|
+
reason
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function isMarked(read) {
|
|
19
|
+
return read.kind === "present" && read.value === true;
|
|
20
|
+
}
|
|
21
|
+
async function retireEnabledFlag(deps) {
|
|
22
|
+
const marker = await deps.readStoredKey(RETIREMENT_MARKER_KEY);
|
|
23
|
+
if (marker.kind === "unreadable") return deferred("settings store unreadable (marker)");
|
|
24
|
+
if (isMarked(marker)) return { kind: "already-retired" };
|
|
25
|
+
const flag = await deps.readStoredKey(RETIRED_ENABLED_KEY);
|
|
26
|
+
if (flag.kind === "unreadable") return deferred("settings store unreadable (enabled)");
|
|
27
|
+
const wasOn = flag.kind === "present" && flag.value === true;
|
|
28
|
+
if (!wasOn) try {
|
|
29
|
+
await deps.disableAtCapability();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return deferred(`capability toggle refused: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
}
|
|
33
|
+
await deps.commit();
|
|
34
|
+
if (!isMarked(await deps.readStoredKey("enabledFlagRetired"))) return deferred("retirement write did not land");
|
|
35
|
+
return {
|
|
36
|
+
kind: "retired",
|
|
37
|
+
capDisabled: !wasOn
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
7
41
|
//#region src/magic-link/auth-magic-link.addon.ts
|
|
8
42
|
/**
|
|
9
43
|
* Magic-link authentication addon.
|
|
@@ -18,10 +52,10 @@ const require_hub_browser_origin = require("../hub-browser-origin-C-u2l9VC.js");
|
|
|
18
52
|
* 3. Email click hits `/login?bridge=…` → 302 to `/api/auth/sso/finish?bridge=…`
|
|
19
53
|
* → hub verifies the bridge token and mints the real session.
|
|
20
54
|
*
|
|
21
|
-
* Delivery is pluggable. This addon ships a "log-only" mode by default
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the SMTP relay via global settings.
|
|
55
|
+
* Delivery is pluggable. This addon ships a "log-only" mode by default,
|
|
56
|
+
* which records that a link was requested but never writes the link
|
|
57
|
+
* itself (it is a login credential — D783). Production deployments
|
|
58
|
+
* configure the SMTP relay via global settings.
|
|
25
59
|
*
|
|
26
60
|
* Open follow-ups:
|
|
27
61
|
* - Wire `nodemailer` for real SMTP delivery (peer dep; the framework
|
|
@@ -29,16 +63,30 @@ const require_hub_browser_origin = require("../hub-browser-origin-C-u2l9VC.js");
|
|
|
29
63
|
* - Throttle requests per-email (rate-limit window).
|
|
30
64
|
* - Auto-provision unknown emails (currently rejected for safety).
|
|
31
65
|
*/
|
|
66
|
+
/** Capability whose per-provider toggle is magic-link's ONE enable (Identity → Authentication). */
|
|
67
|
+
var GOVERNING_CAPABILITY = "auth-provider";
|
|
68
|
+
/** Backoff between retirement attempts — the first ones are refused until the
|
|
69
|
+
* provider registration reaches the hub. After the last, the next boot retries. */
|
|
70
|
+
var RETIREMENT_RETRY_DELAYS_MS = [
|
|
71
|
+
1e3,
|
|
72
|
+
2e3,
|
|
73
|
+
5e3,
|
|
74
|
+
1e4,
|
|
75
|
+
3e4,
|
|
76
|
+
6e4
|
|
77
|
+
];
|
|
32
78
|
var DEFAULT_CONFIG = {
|
|
33
|
-
enabled: false,
|
|
34
79
|
displayName: "Email magic link",
|
|
35
|
-
icon: "
|
|
80
|
+
icon: "mailbox",
|
|
36
81
|
publicOrigin: "",
|
|
37
82
|
deliveryMode: "log-only",
|
|
38
83
|
defaultRole: "viewer",
|
|
39
84
|
ttlSec: 600
|
|
40
85
|
};
|
|
41
86
|
var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
87
|
+
enabledFlagRetired = false;
|
|
88
|
+
retirementTimer = null;
|
|
89
|
+
shuttingDown = false;
|
|
42
90
|
constructor() {
|
|
43
91
|
super({ ...DEFAULT_CONFIG });
|
|
44
92
|
}
|
|
@@ -78,6 +126,8 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
78
126
|
]);
|
|
79
127
|
const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
|
|
80
128
|
this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
|
|
129
|
+
this.shuttingDown = false;
|
|
130
|
+
this.attemptEnabledFlagRetirement(0);
|
|
81
131
|
return [
|
|
82
132
|
{
|
|
83
133
|
capability: require_hub_browser_origin.authProviderCapability,
|
|
@@ -94,16 +144,70 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
94
144
|
];
|
|
95
145
|
}
|
|
96
146
|
buildLoginMethods() {
|
|
97
|
-
if (!this.
|
|
147
|
+
if (!this.enabledFlagRetired) return [];
|
|
98
148
|
return [{
|
|
99
149
|
kind: "redirect",
|
|
100
150
|
id: "auth-magic-link",
|
|
101
151
|
label: this.config.displayName || DEFAULT_CONFIG.displayName,
|
|
102
|
-
icon: this.config.icon
|
|
152
|
+
icon: require_hub_browser_origin.isDisplayIconKey(this.config.icon) ? this.config.icon : DEFAULT_CONFIG.icon,
|
|
103
153
|
startUrl: "/addon/auth-magic-link/start",
|
|
104
154
|
stage: "primary"
|
|
105
155
|
}];
|
|
106
156
|
}
|
|
157
|
+
async onShutdown() {
|
|
158
|
+
this.shuttingDown = true;
|
|
159
|
+
if (this.retirementTimer !== null) clearTimeout(this.retirementTimer);
|
|
160
|
+
this.retirementTimer = null;
|
|
161
|
+
}
|
|
162
|
+
/** One attempt of the D777 carry-over; reschedules itself while deferred. */
|
|
163
|
+
async attemptEnabledFlagRetirement(attempt) {
|
|
164
|
+
if (this.shuttingDown) return;
|
|
165
|
+
let outcome;
|
|
166
|
+
try {
|
|
167
|
+
outcome = await retireEnabledFlag({
|
|
168
|
+
readStoredKey: (key) => this.readStoredGlobalKey(key),
|
|
169
|
+
disableAtCapability: async () => {
|
|
170
|
+
await this.ctx.api.addons.setCapabilityProviderEnabled.mutate({
|
|
171
|
+
capName: GOVERNING_CAPABILITY,
|
|
172
|
+
addonId: this.ctx.id,
|
|
173
|
+
enabled: false
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
commit: async () => {
|
|
177
|
+
const patch = {
|
|
178
|
+
[RETIRED_ENABLED_KEY]: void 0,
|
|
179
|
+
[RETIREMENT_MARKER_KEY]: true
|
|
180
|
+
};
|
|
181
|
+
await this.updateGlobalSettings(patch);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
} catch (err) {
|
|
185
|
+
outcome = {
|
|
186
|
+
kind: "deferred",
|
|
187
|
+
reason: require_hub_browser_origin.errMsg(err)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (outcome.kind !== "deferred") {
|
|
191
|
+
this.enabledFlagRetired = true;
|
|
192
|
+
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.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const delay = RETIREMENT_RETRY_DELAYS_MS[attempt];
|
|
196
|
+
if (delay === void 0) {
|
|
197
|
+
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 } });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.ctx.logger.debug("Magic-link: enabled-flag retirement deferred", { meta: {
|
|
201
|
+
reason: outcome.reason,
|
|
202
|
+
attempt,
|
|
203
|
+
retryInMs: delay
|
|
204
|
+
} });
|
|
205
|
+
this.retirementTimer = setTimeout(() => {
|
|
206
|
+
this.retirementTimer = null;
|
|
207
|
+
this.attemptEnabledFlagRetirement(attempt + 1);
|
|
208
|
+
}, delay);
|
|
209
|
+
this.retirementTimer.unref?.();
|
|
210
|
+
}
|
|
107
211
|
globalSettingsSchema() {
|
|
108
212
|
return this.schema({ sections: [{
|
|
109
213
|
id: "magic-link",
|
|
@@ -111,13 +215,6 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
111
215
|
description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
|
|
112
216
|
columns: 1,
|
|
113
217
|
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
|
-
}),
|
|
121
218
|
this.field({
|
|
122
219
|
type: "text",
|
|
123
220
|
key: "displayName",
|
|
@@ -125,9 +222,10 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
125
222
|
default: DEFAULT_CONFIG.displayName
|
|
126
223
|
}),
|
|
127
224
|
this.field({
|
|
128
|
-
type: "
|
|
225
|
+
type: "icon",
|
|
129
226
|
key: "icon",
|
|
130
227
|
label: "Icon",
|
|
228
|
+
description: "Shown on the login button.",
|
|
131
229
|
default: DEFAULT_CONFIG.icon
|
|
132
230
|
}),
|
|
133
231
|
this.field({
|
|
@@ -141,7 +239,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
141
239
|
type: "select",
|
|
142
240
|
key: "deliveryMode",
|
|
143
241
|
label: "Delivery mode",
|
|
144
|
-
description: "log-only
|
|
242
|
+
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.",
|
|
145
243
|
default: DEFAULT_CONFIG.deliveryMode,
|
|
146
244
|
options: [{
|
|
147
245
|
value: "log-only",
|
|
@@ -278,15 +376,15 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
278
376
|
reply.send({ error: "sso-bridge capability unavailable" });
|
|
279
377
|
return;
|
|
280
378
|
}
|
|
281
|
-
|
|
282
|
-
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated (log-only mode
|
|
379
|
+
`${await this.buildPublicOrigin()}${encodeURIComponent(token)}`;
|
|
380
|
+
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated — not delivered (log-only mode, link withheld)", { meta: {
|
|
283
381
|
email,
|
|
284
|
-
|
|
285
|
-
|
|
382
|
+
ttlSec: this.config.ttlSec,
|
|
383
|
+
delivered: false
|
|
286
384
|
} });
|
|
287
|
-
else this.ctx.logger.warn("Magic link
|
|
385
|
+
else this.ctx.logger.warn("Magic link not delivered: SMTP mode requested but no smtp-provider cap registered", { meta: {
|
|
288
386
|
email,
|
|
289
|
-
|
|
387
|
+
delivered: false
|
|
290
388
|
} });
|
|
291
389
|
reply.code(200);
|
|
292
390
|
reply.send(ok);
|
|
@@ -1,4 +1,38 @@
|
|
|
1
|
-
import { a as buildAddonRouteProvider,
|
|
1
|
+
import { a as buildAddonRouteProvider, i as authProviderCapability, l as errMsg, n as addonRoutesCapability, o as isDisplayIconKey, s as loginMethodCapability, t as hubBrowserOrigin, u as BaseAddon } from "../hub-browser-origin-BRU-HVqq.mjs";
|
|
2
|
+
//#region src/magic-link/retire-enabled-flag.ts
|
|
3
|
+
/** The retired settings key. */
|
|
4
|
+
var RETIRED_ENABLED_KEY = "enabled";
|
|
5
|
+
/** Store key marking the retirement as done. Not a schema field. */
|
|
6
|
+
var RETIREMENT_MARKER_KEY = "enabledFlagRetired";
|
|
7
|
+
function deferred(reason) {
|
|
8
|
+
return {
|
|
9
|
+
kind: "deferred",
|
|
10
|
+
reason
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function isMarked(read) {
|
|
14
|
+
return read.kind === "present" && read.value === true;
|
|
15
|
+
}
|
|
16
|
+
async function retireEnabledFlag(deps) {
|
|
17
|
+
const marker = await deps.readStoredKey(RETIREMENT_MARKER_KEY);
|
|
18
|
+
if (marker.kind === "unreadable") return deferred("settings store unreadable (marker)");
|
|
19
|
+
if (isMarked(marker)) return { kind: "already-retired" };
|
|
20
|
+
const flag = await deps.readStoredKey(RETIRED_ENABLED_KEY);
|
|
21
|
+
if (flag.kind === "unreadable") return deferred("settings store unreadable (enabled)");
|
|
22
|
+
const wasOn = flag.kind === "present" && flag.value === true;
|
|
23
|
+
if (!wasOn) try {
|
|
24
|
+
await deps.disableAtCapability();
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return deferred(`capability toggle refused: ${err instanceof Error ? err.message : String(err)}`);
|
|
27
|
+
}
|
|
28
|
+
await deps.commit();
|
|
29
|
+
if (!isMarked(await deps.readStoredKey("enabledFlagRetired"))) return deferred("retirement write did not land");
|
|
30
|
+
return {
|
|
31
|
+
kind: "retired",
|
|
32
|
+
capDisabled: !wasOn
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
2
36
|
//#region src/magic-link/auth-magic-link.addon.ts
|
|
3
37
|
/**
|
|
4
38
|
* Magic-link authentication addon.
|
|
@@ -13,10 +47,10 @@ import { a as buildAddonRouteProvider, c as errMsg, i as authProviderCapability,
|
|
|
13
47
|
* 3. Email click hits `/login?bridge=…` → 302 to `/api/auth/sso/finish?bridge=…`
|
|
14
48
|
* → hub verifies the bridge token and mints the real session.
|
|
15
49
|
*
|
|
16
|
-
* Delivery is pluggable. This addon ships a "log-only" mode by default
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the SMTP relay via global settings.
|
|
50
|
+
* Delivery is pluggable. This addon ships a "log-only" mode by default,
|
|
51
|
+
* which records that a link was requested but never writes the link
|
|
52
|
+
* itself (it is a login credential — D783). Production deployments
|
|
53
|
+
* configure the SMTP relay via global settings.
|
|
20
54
|
*
|
|
21
55
|
* Open follow-ups:
|
|
22
56
|
* - Wire `nodemailer` for real SMTP delivery (peer dep; the framework
|
|
@@ -24,16 +58,30 @@ import { a as buildAddonRouteProvider, c as errMsg, i as authProviderCapability,
|
|
|
24
58
|
* - Throttle requests per-email (rate-limit window).
|
|
25
59
|
* - Auto-provision unknown emails (currently rejected for safety).
|
|
26
60
|
*/
|
|
61
|
+
/** Capability whose per-provider toggle is magic-link's ONE enable (Identity → Authentication). */
|
|
62
|
+
var GOVERNING_CAPABILITY = "auth-provider";
|
|
63
|
+
/** Backoff between retirement attempts — the first ones are refused until the
|
|
64
|
+
* provider registration reaches the hub. After the last, the next boot retries. */
|
|
65
|
+
var RETIREMENT_RETRY_DELAYS_MS = [
|
|
66
|
+
1e3,
|
|
67
|
+
2e3,
|
|
68
|
+
5e3,
|
|
69
|
+
1e4,
|
|
70
|
+
3e4,
|
|
71
|
+
6e4
|
|
72
|
+
];
|
|
27
73
|
var DEFAULT_CONFIG = {
|
|
28
|
-
enabled: false,
|
|
29
74
|
displayName: "Email magic link",
|
|
30
|
-
icon: "
|
|
75
|
+
icon: "mailbox",
|
|
31
76
|
publicOrigin: "",
|
|
32
77
|
deliveryMode: "log-only",
|
|
33
78
|
defaultRole: "viewer",
|
|
34
79
|
ttlSec: 600
|
|
35
80
|
};
|
|
36
81
|
var AuthMagicLinkAddon = class extends BaseAddon {
|
|
82
|
+
enabledFlagRetired = false;
|
|
83
|
+
retirementTimer = null;
|
|
84
|
+
shuttingDown = false;
|
|
37
85
|
constructor() {
|
|
38
86
|
super({ ...DEFAULT_CONFIG });
|
|
39
87
|
}
|
|
@@ -73,6 +121,8 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
73
121
|
]);
|
|
74
122
|
const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
|
|
75
123
|
this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
|
|
124
|
+
this.shuttingDown = false;
|
|
125
|
+
this.attemptEnabledFlagRetirement(0);
|
|
76
126
|
return [
|
|
77
127
|
{
|
|
78
128
|
capability: authProviderCapability,
|
|
@@ -89,16 +139,70 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
89
139
|
];
|
|
90
140
|
}
|
|
91
141
|
buildLoginMethods() {
|
|
92
|
-
if (!this.
|
|
142
|
+
if (!this.enabledFlagRetired) return [];
|
|
93
143
|
return [{
|
|
94
144
|
kind: "redirect",
|
|
95
145
|
id: "auth-magic-link",
|
|
96
146
|
label: this.config.displayName || DEFAULT_CONFIG.displayName,
|
|
97
|
-
icon: this.config.icon
|
|
147
|
+
icon: isDisplayIconKey(this.config.icon) ? this.config.icon : DEFAULT_CONFIG.icon,
|
|
98
148
|
startUrl: "/addon/auth-magic-link/start",
|
|
99
149
|
stage: "primary"
|
|
100
150
|
}];
|
|
101
151
|
}
|
|
152
|
+
async onShutdown() {
|
|
153
|
+
this.shuttingDown = true;
|
|
154
|
+
if (this.retirementTimer !== null) clearTimeout(this.retirementTimer);
|
|
155
|
+
this.retirementTimer = null;
|
|
156
|
+
}
|
|
157
|
+
/** One attempt of the D777 carry-over; reschedules itself while deferred. */
|
|
158
|
+
async attemptEnabledFlagRetirement(attempt) {
|
|
159
|
+
if (this.shuttingDown) return;
|
|
160
|
+
let outcome;
|
|
161
|
+
try {
|
|
162
|
+
outcome = await retireEnabledFlag({
|
|
163
|
+
readStoredKey: (key) => this.readStoredGlobalKey(key),
|
|
164
|
+
disableAtCapability: async () => {
|
|
165
|
+
await this.ctx.api.addons.setCapabilityProviderEnabled.mutate({
|
|
166
|
+
capName: GOVERNING_CAPABILITY,
|
|
167
|
+
addonId: this.ctx.id,
|
|
168
|
+
enabled: false
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
commit: async () => {
|
|
172
|
+
const patch = {
|
|
173
|
+
[RETIRED_ENABLED_KEY]: void 0,
|
|
174
|
+
[RETIREMENT_MARKER_KEY]: true
|
|
175
|
+
};
|
|
176
|
+
await this.updateGlobalSettings(patch);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
} catch (err) {
|
|
180
|
+
outcome = {
|
|
181
|
+
kind: "deferred",
|
|
182
|
+
reason: errMsg(err)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (outcome.kind !== "deferred") {
|
|
186
|
+
this.enabledFlagRetired = true;
|
|
187
|
+
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.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const delay = RETIREMENT_RETRY_DELAYS_MS[attempt];
|
|
191
|
+
if (delay === void 0) {
|
|
192
|
+
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 } });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.ctx.logger.debug("Magic-link: enabled-flag retirement deferred", { meta: {
|
|
196
|
+
reason: outcome.reason,
|
|
197
|
+
attempt,
|
|
198
|
+
retryInMs: delay
|
|
199
|
+
} });
|
|
200
|
+
this.retirementTimer = setTimeout(() => {
|
|
201
|
+
this.retirementTimer = null;
|
|
202
|
+
this.attemptEnabledFlagRetirement(attempt + 1);
|
|
203
|
+
}, delay);
|
|
204
|
+
this.retirementTimer.unref?.();
|
|
205
|
+
}
|
|
102
206
|
globalSettingsSchema() {
|
|
103
207
|
return this.schema({ sections: [{
|
|
104
208
|
id: "magic-link",
|
|
@@ -106,13 +210,6 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
106
210
|
description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
|
|
107
211
|
columns: 1,
|
|
108
212
|
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
|
-
}),
|
|
116
213
|
this.field({
|
|
117
214
|
type: "text",
|
|
118
215
|
key: "displayName",
|
|
@@ -120,9 +217,10 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
120
217
|
default: DEFAULT_CONFIG.displayName
|
|
121
218
|
}),
|
|
122
219
|
this.field({
|
|
123
|
-
type: "
|
|
220
|
+
type: "icon",
|
|
124
221
|
key: "icon",
|
|
125
222
|
label: "Icon",
|
|
223
|
+
description: "Shown on the login button.",
|
|
126
224
|
default: DEFAULT_CONFIG.icon
|
|
127
225
|
}),
|
|
128
226
|
this.field({
|
|
@@ -136,7 +234,7 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
136
234
|
type: "select",
|
|
137
235
|
key: "deliveryMode",
|
|
138
236
|
label: "Delivery mode",
|
|
139
|
-
description: "log-only
|
|
237
|
+
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.",
|
|
140
238
|
default: DEFAULT_CONFIG.deliveryMode,
|
|
141
239
|
options: [{
|
|
142
240
|
value: "log-only",
|
|
@@ -273,15 +371,15 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
273
371
|
reply.send({ error: "sso-bridge capability unavailable" });
|
|
274
372
|
return;
|
|
275
373
|
}
|
|
276
|
-
|
|
277
|
-
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated (log-only mode
|
|
374
|
+
`${await this.buildPublicOrigin()}${encodeURIComponent(token)}`;
|
|
375
|
+
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated — not delivered (log-only mode, link withheld)", { meta: {
|
|
278
376
|
email,
|
|
279
|
-
|
|
280
|
-
|
|
377
|
+
ttlSec: this.config.ttlSec,
|
|
378
|
+
delivered: false
|
|
281
379
|
} });
|
|
282
|
-
else this.ctx.logger.warn("Magic link
|
|
380
|
+
else this.ctx.logger.warn("Magic link not delivered: SMTP mode requested but no smtp-provider cap registered", { meta: {
|
|
283
381
|
email,
|
|
284
|
-
|
|
382
|
+
delivered: false
|
|
285
383
|
} });
|
|
286
384
|
reply.code(200);
|
|
287
385
|
reply.send(ok);
|
|
@@ -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_hub_browser_origin = require("../hub-browser-origin-
|
|
6
|
+
const require_hub_browser_origin = require("../hub-browser-origin-CCWSbHYb.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
|
|
@@ -733,9 +733,14 @@ function createRemoteJWKSet(url, options) {
|
|
|
733
733
|
* — the hub verifies it with the same `jwtSecret` and only then mints
|
|
734
734
|
* the user session JWT.
|
|
735
735
|
*
|
|
736
|
+
* Identity mapping is EXPLICIT LINKING only (D782): the bridge names the
|
|
737
|
+
* identity `oidc:<instance>:<sub>`; the hub signs in the user that identity
|
|
738
|
+
* was linked to, and refuses an unlinked one. A flow started by a signed-in
|
|
739
|
+
* user's "Link" button carries the hub's opaque `?link=` handle through the
|
|
740
|
+
* pending state into the bridge (`linkIntent`); the hub resolves it.
|
|
741
|
+
*
|
|
736
742
|
* TODO (next session):
|
|
737
743
|
* - RP-initiated logout (`end_session_endpoint` 302 with id_token_hint).
|
|
738
|
-
* - Groups → scope mapping via id_token `groups` claim.
|
|
739
744
|
* - Refresh token rotation (offline_access).
|
|
740
745
|
* - Persist state + nonce + PKCE across restart (multi-replica).
|
|
741
746
|
*/
|
|
@@ -748,11 +753,24 @@ var DEFAULT_PROVIDER = {
|
|
|
748
753
|
clientSecret: "",
|
|
749
754
|
redirectUri: "",
|
|
750
755
|
scopes: "openid profile email",
|
|
751
|
-
usernameClaim: "preferred_username"
|
|
752
|
-
defaultRole: "viewer"
|
|
756
|
+
usernameClaim: "preferred_username"
|
|
753
757
|
};
|
|
754
758
|
var DEFAULT_CONFIG = { providers: [] };
|
|
755
759
|
var STATE_TTL_MS = 5 * 6e4;
|
|
760
|
+
/**
|
|
761
|
+
* Shape of a hub link-intent handle (`?link=` on /start). Opaque to this
|
|
762
|
+
* addon — the hub resolves it, single-use, at the SSO finish. Validated only
|
|
763
|
+
* so a junk value is refused here rather than carried into a signed bridge.
|
|
764
|
+
*/
|
|
765
|
+
var LINK_INTENT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
766
|
+
/**
|
|
767
|
+
* An instance id appears in URLs and in the identity `oidc:<id>:<sub>`; a `:`
|
|
768
|
+
* would make two identities read the same, so ids are slugs. A configured row
|
|
769
|
+
* whose id is not one is skipped.
|
|
770
|
+
*/
|
|
771
|
+
var INSTANCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
772
|
+
/** Bound on in-flight logins: `/start` is public, so a flood must not grow the map. */
|
|
773
|
+
var MAX_PENDING_STATES = 1e3;
|
|
756
774
|
var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
757
775
|
/** Per-provider runtime state, keyed by `provider.id`. */
|
|
758
776
|
instances = /* @__PURE__ */ new Map();
|
|
@@ -822,9 +840,10 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
822
840
|
kind: "redirect",
|
|
823
841
|
id: `auth-oidc/${id}`,
|
|
824
842
|
label: inst.config.displayName || DEFAULT_PROVIDER.displayName,
|
|
825
|
-
icon: inst.config.icon
|
|
843
|
+
icon: require_hub_browser_origin.isDisplayIconKey(inst.config.icon) ? inst.config.icon : DEFAULT_PROVIDER.icon,
|
|
826
844
|
startUrl: `/addon/auth-oidc/${id}/start`,
|
|
827
|
-
stage: "primary"
|
|
845
|
+
stage: "primary",
|
|
846
|
+
linkable: true
|
|
828
847
|
});
|
|
829
848
|
}
|
|
830
849
|
return out;
|
|
@@ -857,11 +876,11 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
857
876
|
default: "OpenID Connect"
|
|
858
877
|
},
|
|
859
878
|
{
|
|
860
|
-
type: "
|
|
879
|
+
type: "icon",
|
|
861
880
|
key: "icon",
|
|
862
881
|
label: "Icon",
|
|
863
|
-
description: "
|
|
864
|
-
default:
|
|
882
|
+
description: "Shown on the login button.",
|
|
883
|
+
default: DEFAULT_PROVIDER.icon
|
|
865
884
|
},
|
|
866
885
|
{
|
|
867
886
|
type: "text",
|
|
@@ -877,7 +896,7 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
877
896
|
default: ""
|
|
878
897
|
},
|
|
879
898
|
{
|
|
880
|
-
type: "
|
|
899
|
+
type: "password",
|
|
881
900
|
key: "clientSecret",
|
|
882
901
|
label: "Client Secret",
|
|
883
902
|
default: ""
|
|
@@ -915,20 +934,6 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
915
934
|
label: "sub (user id)"
|
|
916
935
|
}
|
|
917
936
|
]
|
|
918
|
-
},
|
|
919
|
-
{
|
|
920
|
-
type: "select",
|
|
921
|
-
key: "defaultRole",
|
|
922
|
-
label: "Default role",
|
|
923
|
-
description: "Role assigned to first-time SSO users.",
|
|
924
|
-
default: "viewer",
|
|
925
|
-
options: [{
|
|
926
|
-
value: "viewer",
|
|
927
|
-
label: "Viewer"
|
|
928
|
-
}, {
|
|
929
|
-
value: "admin",
|
|
930
|
-
label: "Admin"
|
|
931
|
-
}]
|
|
932
937
|
}
|
|
933
938
|
]
|
|
934
939
|
})]
|
|
@@ -942,7 +947,10 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
942
947
|
...DEFAULT_PROVIDER,
|
|
943
948
|
...raw
|
|
944
949
|
};
|
|
945
|
-
if (!merged.id || !merged.id
|
|
950
|
+
if (!merged.id || !INSTANCE_ID_PATTERN.test(merged.id)) {
|
|
951
|
+
if (merged.id) this.ctx.logger.warn("OIDC: provider instance skipped — id must be a lowercase slug", { meta: { id: merged.id } });
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
946
954
|
this.instances.set(merged.id, {
|
|
947
955
|
config: merged,
|
|
948
956
|
discovery: null,
|
|
@@ -982,7 +990,7 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
982
990
|
if (configured) return configured;
|
|
983
991
|
return `${await require_hub_browser_origin.hubBrowserOrigin(this.ctx.api.localNetwork, this.ctx.logger) ?? "https://localhost:4443"}/addon/auth-oidc/${providerId}/callback`;
|
|
984
992
|
}
|
|
985
|
-
async buildLoginUrl(providerId, state) {
|
|
993
|
+
async buildLoginUrl(providerId, state, linkIntent) {
|
|
986
994
|
const inst = this.instances.get(providerId);
|
|
987
995
|
if (!inst) throw new Error(`OIDC: no provider configured with id "${providerId}"`);
|
|
988
996
|
const doc = await this.getDiscovery(providerId);
|
|
@@ -991,12 +999,18 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
991
999
|
const nonce = node_crypto.randomBytes(16).toString("base64url");
|
|
992
1000
|
const redirectUri = await this.buildRedirectUri(providerId);
|
|
993
1001
|
this.pruneExpiredStates();
|
|
1002
|
+
while (this.pendingStates.size >= MAX_PENDING_STATES) {
|
|
1003
|
+
const oldest = this.pendingStates.keys().next();
|
|
1004
|
+
if (oldest.done) break;
|
|
1005
|
+
this.pendingStates.delete(oldest.value);
|
|
1006
|
+
}
|
|
994
1007
|
this.pendingStates.set(state, {
|
|
995
1008
|
codeVerifier,
|
|
996
1009
|
nonce,
|
|
997
1010
|
providerId,
|
|
998
1011
|
redirectUri,
|
|
999
|
-
createdAt: Date.now()
|
|
1012
|
+
createdAt: Date.now(),
|
|
1013
|
+
...linkIntent !== void 0 ? { linkIntent } : {}
|
|
1000
1014
|
});
|
|
1001
1015
|
const params = new URLSearchParams({
|
|
1002
1016
|
response_type: "code",
|
|
@@ -1066,7 +1080,10 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1066
1080
|
username: String(username),
|
|
1067
1081
|
...emailClaim ? { email: emailClaim } : {},
|
|
1068
1082
|
...nameClaim ? { displayName: nameClaim } : {},
|
|
1069
|
-
isAdmin:
|
|
1083
|
+
isAdmin: false,
|
|
1084
|
+
...pending.linkIntent !== void 0 ? { linkIntent: pending.linkIntent } : {},
|
|
1085
|
+
issuer: doc.issuer,
|
|
1086
|
+
providerId
|
|
1070
1087
|
};
|
|
1071
1088
|
}
|
|
1072
1089
|
validateLocalSessionToken(_token) {
|
|
@@ -1079,9 +1096,15 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1079
1096
|
reply.send({ error: `Unknown OIDC provider: ${providerId}` });
|
|
1080
1097
|
return;
|
|
1081
1098
|
}
|
|
1099
|
+
const linkParam = req.query["link"];
|
|
1100
|
+
if (linkParam !== void 0 && !LINK_INTENT_PATTERN.test(linkParam)) {
|
|
1101
|
+
reply.code(400);
|
|
1102
|
+
reply.send({ error: "Malformed link request" });
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1082
1105
|
const state = node_crypto.randomBytes(16).toString("base64url");
|
|
1083
1106
|
try {
|
|
1084
|
-
const url = await this.buildLoginUrl(providerId, state);
|
|
1107
|
+
const url = await this.buildLoginUrl(providerId, state, linkParam);
|
|
1085
1108
|
reply.code(302);
|
|
1086
1109
|
reply.header("Location", url);
|
|
1087
1110
|
reply.send("");
|
|
@@ -1120,13 +1143,20 @@ var AuthOidcAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
1120
1143
|
}
|
|
1121
1144
|
try {
|
|
1122
1145
|
const result = await this.handleCallback(code, state);
|
|
1146
|
+
if (result.providerId !== providerId) {
|
|
1147
|
+
reply.code(400);
|
|
1148
|
+
reply.send({ error: "OIDC callback does not match the provider the login started at" });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1123
1151
|
const { token: bridgeToken } = await this.ctx.api.ssoBridge.signBridgeToken.query({ claims: {
|
|
1124
1152
|
userId: result.userId,
|
|
1125
1153
|
username: result.username,
|
|
1126
1154
|
isAdmin: result.isAdmin,
|
|
1127
1155
|
provider: `auth-oidc/${providerId}`,
|
|
1128
1156
|
...result.email ? { email: result.email } : {},
|
|
1129
|
-
...result.displayName ? { displayName: result.displayName } : {}
|
|
1157
|
+
...result.displayName ? { displayName: result.displayName } : {},
|
|
1158
|
+
...result.linkIntent !== void 0 ? { linkIntent: result.linkIntent } : {},
|
|
1159
|
+
issuer: result.issuer
|
|
1130
1160
|
} });
|
|
1131
1161
|
reply.code(302);
|
|
1132
1162
|
reply.header("Location", `/api/auth/sso/finish?bridge=${encodeURIComponent(bridgeToken)}`);
|