@open-neko/plugin-magic-link 0.3.0
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/LICENSE +202 -0
- package/README.md +65 -0
- package/dist/email.d.ts +34 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +190 -0
- package/dist/email.js.map +1 -0
- package/dist/plugin.d.ts +35 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +131 -0
- package/dist/plugin.js.map +1 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +15675 -0
- package/dist/run.js.map +1 -0
- package/dist/token.d.ts +45 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +89 -0
- package/dist/token.js.map +1 -0
- package/package.json +87 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @open-neko/plugin-magic-link — passwordless email sign-in.
|
|
3
|
+
*
|
|
4
|
+
* Implements OpenNeko's auth contract with `provisioning: "manual"`:
|
|
5
|
+
* the core only starts this flow for emails an admin pre-provisioned,
|
|
6
|
+
* and never creates users from it. This plugin's job is narrow —
|
|
7
|
+
* mint a signed token, email the link, verify the token on callback:
|
|
8
|
+
*
|
|
9
|
+
* begin_auth → HMAC-sign {email, state, expiry}, email the link
|
|
10
|
+
* `${redirectUri}?code=<token>&state=<state>`, and
|
|
11
|
+
* point the browser at the "check your email" notice.
|
|
12
|
+
* complete_auth → verify signature + expiry + state binding, return
|
|
13
|
+
* the identity (groups always empty — role and
|
|
14
|
+
* access assignment is the core's job, and nothing
|
|
15
|
+
* self-asserted should influence it).
|
|
16
|
+
*
|
|
17
|
+
* Single-use and same-browser properties come from the core's
|
|
18
|
+
* read-and-clear state cookie; see token.ts for the exact argument.
|
|
19
|
+
*/
|
|
20
|
+
import { definePlugin, } from "@open-neko/plugin-types";
|
|
21
|
+
import { buildSignInEmail, resolveProvider } from "./email.js";
|
|
22
|
+
import { clampTtl, mintToken, MIN_SECRET_LENGTH, verifyToken, } from "./token.js";
|
|
23
|
+
export class MagicLinkPluginError extends Error {
|
|
24
|
+
cause;
|
|
25
|
+
constructor(message, cause) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.cause = cause;
|
|
28
|
+
this.name = "MagicLinkPluginError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function resolveEnv() {
|
|
32
|
+
const secret = process.env.MAGIC_LINK_SIGNING_SECRET ?? "";
|
|
33
|
+
if (secret.length < MIN_SECRET_LENGTH) {
|
|
34
|
+
throw new MagicLinkPluginError(`MAGIC_LINK_SIGNING_SECRET must be set to at least ${MIN_SECRET_LENGTH} characters`);
|
|
35
|
+
}
|
|
36
|
+
const from = process.env.MAGIC_LINK_FROM?.trim() ?? "";
|
|
37
|
+
if (!from) {
|
|
38
|
+
throw new MagicLinkPluginError("MAGIC_LINK_FROM must be set");
|
|
39
|
+
}
|
|
40
|
+
const rawTtl = process.env.MAGIC_LINK_TTL_SECONDS;
|
|
41
|
+
const ttlSeconds = clampTtl(rawTtl ? Number(rawTtl) : undefined);
|
|
42
|
+
return { secret, from, ttlSeconds };
|
|
43
|
+
}
|
|
44
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
45
|
+
export async function runBeginAuth(params, options = {}) {
|
|
46
|
+
if (!params.redirectUri) {
|
|
47
|
+
throw new MagicLinkPluginError("params.redirectUri is required");
|
|
48
|
+
}
|
|
49
|
+
if (!params.state) {
|
|
50
|
+
throw new MagicLinkPluginError("params.state is required");
|
|
51
|
+
}
|
|
52
|
+
const email = params.loginHint?.trim().toLowerCase() ?? "";
|
|
53
|
+
if (!EMAIL_RE.test(email)) {
|
|
54
|
+
// The core's sign-in page requires the email field
|
|
55
|
+
// (loginHintRequired), so reaching here means a hand-built request.
|
|
56
|
+
throw new MagicLinkPluginError("magic link sign-in requires the email address as loginHint");
|
|
57
|
+
}
|
|
58
|
+
let callback;
|
|
59
|
+
try {
|
|
60
|
+
callback = new URL(params.redirectUri);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new MagicLinkPluginError("params.redirectUri must be an absolute URL");
|
|
64
|
+
}
|
|
65
|
+
const env = resolveEnv();
|
|
66
|
+
const token = mintToken({
|
|
67
|
+
email,
|
|
68
|
+
state: params.state,
|
|
69
|
+
secret: env.secret,
|
|
70
|
+
ttlSeconds: env.ttlSeconds,
|
|
71
|
+
nowMs: options.nowMs,
|
|
72
|
+
});
|
|
73
|
+
const link = new URL(callback.toString());
|
|
74
|
+
link.searchParams.set("code", token);
|
|
75
|
+
link.searchParams.set("state", params.state);
|
|
76
|
+
const send = options.send ?? resolveProvider({
|
|
77
|
+
resendApiKey: process.env.RESEND_API_KEY,
|
|
78
|
+
postmarkServerToken: process.env.POSTMARK_SERVER_TOKEN,
|
|
79
|
+
sendgridApiKey: process.env.SENDGRID_API_KEY,
|
|
80
|
+
}).send;
|
|
81
|
+
await send(buildSignInEmail({
|
|
82
|
+
from: env.from,
|
|
83
|
+
to: email,
|
|
84
|
+
link: link.toString(),
|
|
85
|
+
ttlMinutes: Math.max(1, Math.round(env.ttlSeconds / 60)),
|
|
86
|
+
}));
|
|
87
|
+
// No IdP to redirect to — bounce the browser to the sign-in page's
|
|
88
|
+
// "check your email" notice. The core shows the identical notice when
|
|
89
|
+
// it drops an unprovisioned email before ever calling us.
|
|
90
|
+
return {
|
|
91
|
+
authorizationUrl: `${callback.origin}/signin?notice=link-sent`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function runCompleteAuth(params, options = {}) {
|
|
95
|
+
if (!params.code) {
|
|
96
|
+
throw new MagicLinkPluginError("params.code is required");
|
|
97
|
+
}
|
|
98
|
+
if (!params.state) {
|
|
99
|
+
throw new MagicLinkPluginError("params.state is required");
|
|
100
|
+
}
|
|
101
|
+
const env = resolveEnv();
|
|
102
|
+
const claims = verifyToken({
|
|
103
|
+
token: params.code,
|
|
104
|
+
state: params.state,
|
|
105
|
+
secret: env.secret,
|
|
106
|
+
nowMs: options.nowMs,
|
|
107
|
+
});
|
|
108
|
+
const identity = {
|
|
109
|
+
// Deterministic per mailbox so repeat sign-ins hit the same
|
|
110
|
+
// app_user row via the sub lookup.
|
|
111
|
+
sub: `magic-link:${claims.email}`,
|
|
112
|
+
email: claims.email,
|
|
113
|
+
name: null,
|
|
114
|
+
orgId: null,
|
|
115
|
+
// Self-asserted flows must never influence role mapping.
|
|
116
|
+
groups: [],
|
|
117
|
+
};
|
|
118
|
+
return { identity };
|
|
119
|
+
}
|
|
120
|
+
export default definePlugin({
|
|
121
|
+
name: "@open-neko/plugin-magic-link",
|
|
122
|
+
version: "0.3.0", // x-release-please-version
|
|
123
|
+
capabilities: {
|
|
124
|
+
auth: {
|
|
125
|
+
providerLabel: "Email link",
|
|
126
|
+
begin: (params) => runBeginAuth(params),
|
|
127
|
+
complete: (params) => runCompleteAuth(params),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":""}
|