@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/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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":""}