@camstack/addon-auth 1.2.130 → 1.2.132
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 → dist-DbaYwR-N.mjs} +2664 -525
- package/dist/{hub-browser-origin-C-u2l9VC.js → dist-DjCeMP9o.js} +2665 -526
- package/dist/hub-browser-origin-DohQUFPz.js +16 -0
- package/dist/hub-browser-origin-LBBO5r05.mjs +11 -0
- package/dist/magic-link/auth-magic-link.addon.js +132 -33
- package/dist/magic-link/auth-magic-link.addon.mjs +124 -25
- package/dist/oidc/auth-oidc.addon.js +213 -44
- package/dist/oidc/auth-oidc.addon.mjs +205 -36
- 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-Cy2WCjhX.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-DBsbNmPW.mjs +26 -0
- package/dist/webauthn/auth-webauthn.addon.js +13 -12
- package/dist/webauthn/auth-webauthn.addon.mjs +2 -1
- package/dist/webauthn/{hostInit-DEFeGgRf.mjs → hostInit-3W0iPRJ_.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
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
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 };
|
|
@@ -3,7 +3,42 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../chunk-Cek0wNdY.js");
|
|
6
|
-
const
|
|
6
|
+
const require_dist = require("../dist-DjCeMP9o.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
|
|
7
42
|
//#region src/magic-link/auth-magic-link.addon.ts
|
|
8
43
|
/**
|
|
9
44
|
* Magic-link authentication addon.
|
|
@@ -18,10 +53,10 @@ const require_hub_browser_origin = require("../hub-browser-origin-C-u2l9VC.js");
|
|
|
18
53
|
* 3. Email click hits `/login?bridge=…` → 302 to `/api/auth/sso/finish?bridge=…`
|
|
19
54
|
* → hub verifies the bridge token and mints the real session.
|
|
20
55
|
*
|
|
21
|
-
* Delivery is pluggable. This addon ships a "log-only" mode by default
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the SMTP relay via global settings.
|
|
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.
|
|
25
60
|
*
|
|
26
61
|
* Open follow-ups:
|
|
27
62
|
* - Wire `nodemailer` for real SMTP delivery (peer dep; the framework
|
|
@@ -29,16 +64,30 @@ const require_hub_browser_origin = require("../hub-browser-origin-C-u2l9VC.js");
|
|
|
29
64
|
* - Throttle requests per-email (rate-limit window).
|
|
30
65
|
* - Auto-provision unknown emails (currently rejected for safety).
|
|
31
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
|
+
];
|
|
32
79
|
var DEFAULT_CONFIG = {
|
|
33
|
-
enabled: false,
|
|
34
80
|
displayName: "Email magic link",
|
|
35
|
-
icon: "
|
|
81
|
+
icon: "mailbox",
|
|
36
82
|
publicOrigin: "",
|
|
37
83
|
deliveryMode: "log-only",
|
|
38
84
|
defaultRole: "viewer",
|
|
39
85
|
ttlSec: 600
|
|
40
86
|
};
|
|
41
|
-
var AuthMagicLinkAddon = class extends
|
|
87
|
+
var AuthMagicLinkAddon = class extends require_dist.BaseAddon {
|
|
88
|
+
enabledFlagRetired = false;
|
|
89
|
+
retirementTimer = null;
|
|
90
|
+
shuttingDown = false;
|
|
42
91
|
constructor() {
|
|
43
92
|
super({ ...DEFAULT_CONFIG });
|
|
44
93
|
}
|
|
@@ -53,7 +102,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
53
102
|
throw new Error("Magic link does not use callback — the email link redirects to /api/auth/sso/finish directly");
|
|
54
103
|
}
|
|
55
104
|
};
|
|
56
|
-
const routeProvider =
|
|
105
|
+
const routeProvider = require_dist.buildAddonRouteProvider("auth-magic-link", [
|
|
57
106
|
{
|
|
58
107
|
method: "GET",
|
|
59
108
|
path: "/start",
|
|
@@ -78,32 +127,88 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
78
127
|
]);
|
|
79
128
|
const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
|
|
80
129
|
this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
|
|
130
|
+
this.shuttingDown = false;
|
|
131
|
+
this.attemptEnabledFlagRetirement(0);
|
|
81
132
|
return [
|
|
82
133
|
{
|
|
83
|
-
capability:
|
|
134
|
+
capability: require_dist.authProviderCapability,
|
|
84
135
|
provider: authProvider
|
|
85
136
|
},
|
|
86
137
|
{
|
|
87
|
-
capability:
|
|
138
|
+
capability: require_dist.addonRoutesCapability,
|
|
88
139
|
provider: routeProvider
|
|
89
140
|
},
|
|
90
141
|
{
|
|
91
|
-
capability:
|
|
142
|
+
capability: require_dist.loginMethodCapability,
|
|
92
143
|
provider: loginMethodProvider
|
|
93
144
|
}
|
|
94
145
|
];
|
|
95
146
|
}
|
|
96
147
|
buildLoginMethods() {
|
|
97
|
-
if (!this.
|
|
148
|
+
if (!this.enabledFlagRetired) return [];
|
|
98
149
|
return [{
|
|
99
150
|
kind: "redirect",
|
|
100
151
|
id: "auth-magic-link",
|
|
101
152
|
label: this.config.displayName || DEFAULT_CONFIG.displayName,
|
|
102
|
-
icon: this.config.icon
|
|
153
|
+
icon: require_dist.isDisplayIconKey(this.config.icon) ? this.config.icon : DEFAULT_CONFIG.icon,
|
|
103
154
|
startUrl: "/addon/auth-magic-link/start",
|
|
104
155
|
stage: "primary"
|
|
105
156
|
}];
|
|
106
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
|
+
}
|
|
107
212
|
globalSettingsSchema() {
|
|
108
213
|
return this.schema({ sections: [{
|
|
109
214
|
id: "magic-link",
|
|
@@ -111,13 +216,6 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
111
216
|
description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
|
|
112
217
|
columns: 1,
|
|
113
218
|
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
219
|
this.field({
|
|
122
220
|
type: "text",
|
|
123
221
|
key: "displayName",
|
|
@@ -125,9 +223,10 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
125
223
|
default: DEFAULT_CONFIG.displayName
|
|
126
224
|
}),
|
|
127
225
|
this.field({
|
|
128
|
-
type: "
|
|
226
|
+
type: "icon",
|
|
129
227
|
key: "icon",
|
|
130
228
|
label: "Icon",
|
|
229
|
+
description: "Shown on the login button.",
|
|
131
230
|
default: DEFAULT_CONFIG.icon
|
|
132
231
|
}),
|
|
133
232
|
this.field({
|
|
@@ -141,7 +240,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
141
240
|
type: "select",
|
|
142
241
|
key: "deliveryMode",
|
|
143
242
|
label: "Delivery mode",
|
|
144
|
-
description: "log-only
|
|
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.",
|
|
145
244
|
default: DEFAULT_CONFIG.deliveryMode,
|
|
146
245
|
options: [{
|
|
147
246
|
value: "log-only",
|
|
@@ -184,7 +283,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
184
283
|
try {
|
|
185
284
|
users = await this.ctx.api.userManagement.listUsers.query();
|
|
186
285
|
} catch (err) {
|
|
187
|
-
this.ctx.logger.warn("Magic-link user lookup failed (user-management cap unavailable)", { meta: { message:
|
|
286
|
+
this.ctx.logger.warn("Magic-link user lookup failed (user-management cap unavailable)", { meta: { message: require_dist.errMsg(err) } });
|
|
188
287
|
return null;
|
|
189
288
|
}
|
|
190
289
|
const needle = email.trim().toLowerCase();
|
|
@@ -228,7 +327,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
228
327
|
<input id="email" name="email" type="email" autocomplete="email" required autofocus placeholder="you@example.com">
|
|
229
328
|
<button id="b" type="submit">Send login link</button>
|
|
230
329
|
<div class="msg" id="m" hidden></div>
|
|
231
|
-
<p style="margin-top:1rem"><a href="/
|
|
330
|
+
<p style="margin-top:1rem"><a href="/login">Back to sign in</a></p>
|
|
232
331
|
</form>
|
|
233
332
|
<script>
|
|
234
333
|
var f=document.getElementById('f'),b=document.getElementById('b'),m=document.getElementById('m');
|
|
@@ -273,20 +372,20 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
273
372
|
ttlSec: this.config.ttlSec || DEFAULT_CONFIG.ttlSec
|
|
274
373
|
}));
|
|
275
374
|
} catch (err) {
|
|
276
|
-
this.ctx.logger.error("Magic-link bridge-token minting failed (sso-bridge unavailable)", { meta: { message:
|
|
375
|
+
this.ctx.logger.error("Magic-link bridge-token minting failed (sso-bridge unavailable)", { meta: { message: require_dist.errMsg(err) } });
|
|
277
376
|
reply.code(503);
|
|
278
377
|
reply.send({ error: "sso-bridge capability unavailable" });
|
|
279
378
|
return;
|
|
280
379
|
}
|
|
281
|
-
|
|
282
|
-
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated (log-only mode
|
|
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: {
|
|
283
382
|
email,
|
|
284
|
-
|
|
285
|
-
|
|
383
|
+
ttlSec: this.config.ttlSec,
|
|
384
|
+
delivered: false
|
|
286
385
|
} });
|
|
287
|
-
else this.ctx.logger.warn("Magic link
|
|
386
|
+
else this.ctx.logger.warn("Magic link not delivered: SMTP mode requested but no smtp-provider cap registered", { meta: {
|
|
288
387
|
email,
|
|
289
|
-
|
|
388
|
+
delivered: false
|
|
290
389
|
} });
|
|
291
390
|
reply.code(200);
|
|
292
391
|
reply.send(ok);
|
|
@@ -294,7 +393,7 @@ var AuthMagicLinkAddon = class extends require_hub_browser_origin.BaseAddon {
|
|
|
294
393
|
reply.code(500);
|
|
295
394
|
reply.send({
|
|
296
395
|
error: "Magic-link request failed",
|
|
297
|
-
message:
|
|
396
|
+
message: require_dist.errMsg(err)
|
|
298
397
|
});
|
|
299
398
|
}
|
|
300
399
|
}
|
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as isDisplayIconKey, c as errMsg, i as buildAddonRouteProvider, l as BaseAddon, o as loginMethodCapability, r as authProviderCapability, t as addonRoutesCapability } from "../dist-DbaYwR-N.mjs";
|
|
2
|
+
import { t as hubBrowserOrigin } from "../hub-browser-origin-LBBO5r05.mjs";
|
|
3
|
+
//#region src/magic-link/retire-enabled-flag.ts
|
|
4
|
+
/** The retired settings key. */
|
|
5
|
+
var RETIRED_ENABLED_KEY = "enabled";
|
|
6
|
+
/** Store key marking the retirement as done. Not a schema field. */
|
|
7
|
+
var RETIREMENT_MARKER_KEY = "enabledFlagRetired";
|
|
8
|
+
function deferred(reason) {
|
|
9
|
+
return {
|
|
10
|
+
kind: "deferred",
|
|
11
|
+
reason
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function isMarked(read) {
|
|
15
|
+
return read.kind === "present" && read.value === true;
|
|
16
|
+
}
|
|
17
|
+
async function retireEnabledFlag(deps) {
|
|
18
|
+
const marker = await deps.readStoredKey(RETIREMENT_MARKER_KEY);
|
|
19
|
+
if (marker.kind === "unreadable") return deferred("settings store unreadable (marker)");
|
|
20
|
+
if (isMarked(marker)) return { kind: "already-retired" };
|
|
21
|
+
const flag = await deps.readStoredKey(RETIRED_ENABLED_KEY);
|
|
22
|
+
if (flag.kind === "unreadable") return deferred("settings store unreadable (enabled)");
|
|
23
|
+
const wasOn = flag.kind === "present" && flag.value === true;
|
|
24
|
+
if (!wasOn) try {
|
|
25
|
+
await deps.disableAtCapability();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return deferred(`capability toggle refused: ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
}
|
|
29
|
+
await deps.commit();
|
|
30
|
+
if (!isMarked(await deps.readStoredKey("enabledFlagRetired"))) return deferred("retirement write did not land");
|
|
31
|
+
return {
|
|
32
|
+
kind: "retired",
|
|
33
|
+
capDisabled: !wasOn
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
2
37
|
//#region src/magic-link/auth-magic-link.addon.ts
|
|
3
38
|
/**
|
|
4
39
|
* Magic-link authentication addon.
|
|
@@ -13,10 +48,10 @@ import { a as buildAddonRouteProvider, c as errMsg, i as authProviderCapability,
|
|
|
13
48
|
* 3. Email click hits `/login?bridge=…` → 302 to `/api/auth/sso/finish?bridge=…`
|
|
14
49
|
* → hub verifies the bridge token and mints the real session.
|
|
15
50
|
*
|
|
16
|
-
* Delivery is pluggable. This addon ships a "log-only" mode by default
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the SMTP relay via global settings.
|
|
51
|
+
* Delivery is pluggable. This addon ships a "log-only" mode by default,
|
|
52
|
+
* which records that a link was requested but never writes the link
|
|
53
|
+
* itself (it is a login credential — D783). Production deployments
|
|
54
|
+
* configure the SMTP relay via global settings.
|
|
20
55
|
*
|
|
21
56
|
* Open follow-ups:
|
|
22
57
|
* - Wire `nodemailer` for real SMTP delivery (peer dep; the framework
|
|
@@ -24,16 +59,30 @@ import { a as buildAddonRouteProvider, c as errMsg, i as authProviderCapability,
|
|
|
24
59
|
* - Throttle requests per-email (rate-limit window).
|
|
25
60
|
* - Auto-provision unknown emails (currently rejected for safety).
|
|
26
61
|
*/
|
|
62
|
+
/** Capability whose per-provider toggle is magic-link's ONE enable (Identity → Authentication). */
|
|
63
|
+
var GOVERNING_CAPABILITY = "auth-provider";
|
|
64
|
+
/** Backoff between retirement attempts — the first ones are refused until the
|
|
65
|
+
* provider registration reaches the hub. After the last, the next boot retries. */
|
|
66
|
+
var RETIREMENT_RETRY_DELAYS_MS = [
|
|
67
|
+
1e3,
|
|
68
|
+
2e3,
|
|
69
|
+
5e3,
|
|
70
|
+
1e4,
|
|
71
|
+
3e4,
|
|
72
|
+
6e4
|
|
73
|
+
];
|
|
27
74
|
var DEFAULT_CONFIG = {
|
|
28
|
-
enabled: false,
|
|
29
75
|
displayName: "Email magic link",
|
|
30
|
-
icon: "
|
|
76
|
+
icon: "mailbox",
|
|
31
77
|
publicOrigin: "",
|
|
32
78
|
deliveryMode: "log-only",
|
|
33
79
|
defaultRole: "viewer",
|
|
34
80
|
ttlSec: 600
|
|
35
81
|
};
|
|
36
82
|
var AuthMagicLinkAddon = class extends BaseAddon {
|
|
83
|
+
enabledFlagRetired = false;
|
|
84
|
+
retirementTimer = null;
|
|
85
|
+
shuttingDown = false;
|
|
37
86
|
constructor() {
|
|
38
87
|
super({ ...DEFAULT_CONFIG });
|
|
39
88
|
}
|
|
@@ -73,6 +122,8 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
73
122
|
]);
|
|
74
123
|
const loginMethodProvider = { getLoginMethods: async () => this.buildLoginMethods() };
|
|
75
124
|
this.ctx.logger.info("Magic-link auth provider initialized", { meta: { deliveryMode: this.config.deliveryMode } });
|
|
125
|
+
this.shuttingDown = false;
|
|
126
|
+
this.attemptEnabledFlagRetirement(0);
|
|
76
127
|
return [
|
|
77
128
|
{
|
|
78
129
|
capability: authProviderCapability,
|
|
@@ -89,16 +140,70 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
89
140
|
];
|
|
90
141
|
}
|
|
91
142
|
buildLoginMethods() {
|
|
92
|
-
if (!this.
|
|
143
|
+
if (!this.enabledFlagRetired) return [];
|
|
93
144
|
return [{
|
|
94
145
|
kind: "redirect",
|
|
95
146
|
id: "auth-magic-link",
|
|
96
147
|
label: this.config.displayName || DEFAULT_CONFIG.displayName,
|
|
97
|
-
icon: this.config.icon
|
|
148
|
+
icon: isDisplayIconKey(this.config.icon) ? this.config.icon : DEFAULT_CONFIG.icon,
|
|
98
149
|
startUrl: "/addon/auth-magic-link/start",
|
|
99
150
|
stage: "primary"
|
|
100
151
|
}];
|
|
101
152
|
}
|
|
153
|
+
async onShutdown() {
|
|
154
|
+
this.shuttingDown = true;
|
|
155
|
+
if (this.retirementTimer !== null) clearTimeout(this.retirementTimer);
|
|
156
|
+
this.retirementTimer = null;
|
|
157
|
+
}
|
|
158
|
+
/** One attempt of the D777 carry-over; reschedules itself while deferred. */
|
|
159
|
+
async attemptEnabledFlagRetirement(attempt) {
|
|
160
|
+
if (this.shuttingDown) return;
|
|
161
|
+
let outcome;
|
|
162
|
+
try {
|
|
163
|
+
outcome = await retireEnabledFlag({
|
|
164
|
+
readStoredKey: (key) => this.readStoredGlobalKey(key),
|
|
165
|
+
disableAtCapability: async () => {
|
|
166
|
+
await this.ctx.api.addons.setCapabilityProviderEnabled.mutate({
|
|
167
|
+
capName: GOVERNING_CAPABILITY,
|
|
168
|
+
addonId: this.ctx.id,
|
|
169
|
+
enabled: false
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
commit: async () => {
|
|
173
|
+
const patch = {
|
|
174
|
+
[RETIRED_ENABLED_KEY]: void 0,
|
|
175
|
+
[RETIREMENT_MARKER_KEY]: true
|
|
176
|
+
};
|
|
177
|
+
await this.updateGlobalSettings(patch);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
outcome = {
|
|
182
|
+
kind: "deferred",
|
|
183
|
+
reason: errMsg(err)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (outcome.kind !== "deferred") {
|
|
187
|
+
this.enabledFlagRetired = true;
|
|
188
|
+
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.");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const delay = RETIREMENT_RETRY_DELAYS_MS[attempt];
|
|
192
|
+
if (delay === void 0) {
|
|
193
|
+
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 } });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.ctx.logger.debug("Magic-link: enabled-flag retirement deferred", { meta: {
|
|
197
|
+
reason: outcome.reason,
|
|
198
|
+
attempt,
|
|
199
|
+
retryInMs: delay
|
|
200
|
+
} });
|
|
201
|
+
this.retirementTimer = setTimeout(() => {
|
|
202
|
+
this.retirementTimer = null;
|
|
203
|
+
this.attemptEnabledFlagRetirement(attempt + 1);
|
|
204
|
+
}, delay);
|
|
205
|
+
this.retirementTimer.unref?.();
|
|
206
|
+
}
|
|
102
207
|
globalSettingsSchema() {
|
|
103
208
|
return this.schema({ sections: [{
|
|
104
209
|
id: "magic-link",
|
|
@@ -106,13 +211,6 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
106
211
|
description: "Passwordless email login. The user types an email; the hub mails them a one-time link valid for the configured TTL.",
|
|
107
212
|
columns: 1,
|
|
108
213
|
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
214
|
this.field({
|
|
117
215
|
type: "text",
|
|
118
216
|
key: "displayName",
|
|
@@ -120,9 +218,10 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
120
218
|
default: DEFAULT_CONFIG.displayName
|
|
121
219
|
}),
|
|
122
220
|
this.field({
|
|
123
|
-
type: "
|
|
221
|
+
type: "icon",
|
|
124
222
|
key: "icon",
|
|
125
223
|
label: "Icon",
|
|
224
|
+
description: "Shown on the login button.",
|
|
126
225
|
default: DEFAULT_CONFIG.icon
|
|
127
226
|
}),
|
|
128
227
|
this.field({
|
|
@@ -136,7 +235,7 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
136
235
|
type: "select",
|
|
137
236
|
key: "deliveryMode",
|
|
138
237
|
label: "Delivery mode",
|
|
139
|
-
description: "log-only
|
|
238
|
+
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
239
|
default: DEFAULT_CONFIG.deliveryMode,
|
|
141
240
|
options: [{
|
|
142
241
|
value: "log-only",
|
|
@@ -223,7 +322,7 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
223
322
|
<input id="email" name="email" type="email" autocomplete="email" required autofocus placeholder="you@example.com">
|
|
224
323
|
<button id="b" type="submit">Send login link</button>
|
|
225
324
|
<div class="msg" id="m" hidden></div>
|
|
226
|
-
<p style="margin-top:1rem"><a href="/
|
|
325
|
+
<p style="margin-top:1rem"><a href="/login">Back to sign in</a></p>
|
|
227
326
|
</form>
|
|
228
327
|
<script>
|
|
229
328
|
var f=document.getElementById('f'),b=document.getElementById('b'),m=document.getElementById('m');
|
|
@@ -273,15 +372,15 @@ var AuthMagicLinkAddon = class extends BaseAddon {
|
|
|
273
372
|
reply.send({ error: "sso-bridge capability unavailable" });
|
|
274
373
|
return;
|
|
275
374
|
}
|
|
276
|
-
|
|
277
|
-
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated (log-only mode
|
|
375
|
+
`${await this.buildPublicOrigin()}${encodeURIComponent(token)}`;
|
|
376
|
+
if (this.config.deliveryMode === "log-only") this.ctx.logger.warn("Magic link generated — not delivered (log-only mode, link withheld)", { meta: {
|
|
278
377
|
email,
|
|
279
|
-
|
|
280
|
-
|
|
378
|
+
ttlSec: this.config.ttlSec,
|
|
379
|
+
delivered: false
|
|
281
380
|
} });
|
|
282
|
-
else this.ctx.logger.warn("Magic link
|
|
381
|
+
else this.ctx.logger.warn("Magic link not delivered: SMTP mode requested but no smtp-provider cap registered", { meta: {
|
|
283
382
|
email,
|
|
284
|
-
|
|
383
|
+
delivered: false
|
|
285
384
|
} });
|
|
286
385
|
reply.code(200);
|
|
287
386
|
reply.send(ok);
|