@pramen/auth 0.0.18 → 0.0.19
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/index.d.ts +26 -20
- package/dist/index.js +31 -6
- package/package.json +2 -2
- package/src/index.ts +40 -10
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HandlerContext, Policy } from "@pramen/server";
|
|
1
|
+
import type { AppTaskMap, HandlerContext, HandlerMap, Policy } from "@pramen/server";
|
|
2
2
|
export declare const authSchema: {
|
|
3
3
|
auth_users: import("@pramen/server").EntityDef<{
|
|
4
4
|
username: {
|
|
@@ -86,8 +86,11 @@ export interface MagicLinkOptions {
|
|
|
86
86
|
* On Cloudflare the recommended transport is Cloudflare Email Sending — a
|
|
87
87
|
* `send_email` binding (no API keys), e.g.
|
|
88
88
|
* `await (ctx.env.EMAIL as SendEmail).send({ to, from: { email, name }, subject, text, html })`
|
|
89
|
-
* (see example/app.ts + oblaka.ts).
|
|
90
|
-
*
|
|
89
|
+
* (see example/app.ts + oblaka.ts). Called from the `sendMagicLinkEmail` TASK, not
|
|
90
|
+
* inline in the mutation — a slow SMTP/API call can't hold the mutation's storage
|
|
91
|
+
* transaction open and time the store out. Retries follow the outbox retry policy;
|
|
92
|
+
* a permanent failure dead-letters the task and the token expires unused (users just
|
|
93
|
+
* request a new link). */
|
|
91
94
|
sendEmail: (ctx: HandlerContext, args: {
|
|
92
95
|
email: string;
|
|
93
96
|
token: string;
|
|
@@ -99,24 +102,27 @@ export interface MagicLinkOptions {
|
|
|
99
102
|
/** Roles assigned when a magic-link login first creates the user. Default `["user"]`. */
|
|
100
103
|
defaultRoles?: string[];
|
|
101
104
|
}
|
|
102
|
-
/** Build the `requestMagicLink` / `loginWithMagicLink` handler pair
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
+
/** Build the `requestMagicLink` / `loginWithMagicLink` handler pair, plus the
|
|
106
|
+
* `sendMagicLinkEmail` task handler that actually invokes `opts.sendEmail`.
|
|
107
|
+
*
|
|
108
|
+
* ```
|
|
109
|
+
* const magicLink = createMagicLinkAuth({ sendEmail: ... });
|
|
110
|
+
* const handlers = { ...cmsHandlers, ...authHandlers, ...magicLink.handlers };
|
|
111
|
+
* const tasks = { ...cmsTasks, ...magicLink.tasks };
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* The `requestMagicLink` handler writes the token and ENQUEUES a task (atomic with
|
|
115
|
+
* the write via the transactional outbox); the drainer runs `sendMagicLinkEmail`
|
|
116
|
+
* AFTER commit, outside the mutation's storage transaction. This avoids the class
|
|
117
|
+
* of failure where a slow SMTP/API call holds the storage lock long enough for the
|
|
118
|
+
* store to time out and reset the underlying object.
|
|
119
|
+
*
|
|
120
|
+
* You MUST spread `magicLink.tasks` into your app's task map — without it, tokens
|
|
121
|
+
* get written but the email never sends (the drainer retries then dead-letters).
|
|
122
|
+
* Both handlers are anonymous — gate nothing; the token is the capability. */
|
|
105
123
|
export declare function createMagicLinkAuth(opts: MagicLinkOptions): {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}, {
|
|
109
|
-
ok: boolean;
|
|
110
|
-
}>;
|
|
111
|
-
loginWithMagicLink: import("@pramen/server").Handler<{
|
|
112
|
-
token: string;
|
|
113
|
-
}, {
|
|
114
|
-
token: string;
|
|
115
|
-
user: {
|
|
116
|
-
username: string;
|
|
117
|
-
roles: string[];
|
|
118
|
-
};
|
|
119
|
-
}>;
|
|
124
|
+
handlers: HandlerMap;
|
|
125
|
+
tasks: AppTaskMap;
|
|
120
126
|
};
|
|
121
127
|
/** Build admin + self-service handlers over a users table (default `auth_users`).
|
|
122
128
|
* Pass `table` to operate over your OWN authSchema-shaped table — e.g. one with an
|
package/dist/index.js
CHANGED
|
@@ -227,14 +227,29 @@ function parseLinkToken(raw) {
|
|
|
227
227
|
throw new BadRequest("token is required");
|
|
228
228
|
return { token: o.token };
|
|
229
229
|
}
|
|
230
|
-
/** Build the `requestMagicLink` / `loginWithMagicLink` handler pair
|
|
231
|
-
*
|
|
232
|
-
*
|
|
230
|
+
/** Build the `requestMagicLink` / `loginWithMagicLink` handler pair, plus the
|
|
231
|
+
* `sendMagicLinkEmail` task handler that actually invokes `opts.sendEmail`.
|
|
232
|
+
*
|
|
233
|
+
* ```
|
|
234
|
+
* const magicLink = createMagicLinkAuth({ sendEmail: ... });
|
|
235
|
+
* const handlers = { ...cmsHandlers, ...authHandlers, ...magicLink.handlers };
|
|
236
|
+
* const tasks = { ...cmsTasks, ...magicLink.tasks };
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* The `requestMagicLink` handler writes the token and ENQUEUES a task (atomic with
|
|
240
|
+
* the write via the transactional outbox); the drainer runs `sendMagicLinkEmail`
|
|
241
|
+
* AFTER commit, outside the mutation's storage transaction. This avoids the class
|
|
242
|
+
* of failure where a slow SMTP/API call holds the storage lock long enough for the
|
|
243
|
+
* store to time out and reset the underlying object.
|
|
244
|
+
*
|
|
245
|
+
* You MUST spread `magicLink.tasks` into your app's task map — without it, tokens
|
|
246
|
+
* get written but the email never sends (the drainer retries then dead-letters).
|
|
247
|
+
* Both handlers are anonymous — gate nothing; the token is the capability. */
|
|
233
248
|
export function createMagicLinkAuth(opts) {
|
|
234
249
|
const linkTtlMs = (opts.linkTtlSeconds ?? 900) * 1000;
|
|
235
250
|
const sessionTtl = opts.sessionTtlSeconds ?? TOKEN_TTL_SECONDS;
|
|
236
251
|
const defaultRoles = opts.defaultRoles ?? DEFAULT_ROLES;
|
|
237
|
-
|
|
252
|
+
const handlers = {
|
|
238
253
|
requestMagicLink: mutation(async (ctx, input) => {
|
|
239
254
|
const token = mintToken();
|
|
240
255
|
const tokenHash = await sha256Hex(token);
|
|
@@ -242,8 +257,11 @@ export function createMagicLinkAuth(opts) {
|
|
|
242
257
|
// Invalidate any prior pending links for this email — only the latest works.
|
|
243
258
|
await ctx.db.exec("DELETE FROM auth_magic_links WHERE email = ?", input.email);
|
|
244
259
|
await ctx.db.exec("INSERT INTO auth_magic_links (tokenHash, email, expiresAt, createdAt) VALUES (?, ?, ?, ?)", tokenHash, input.email, now + linkTtlMs, now);
|
|
245
|
-
//
|
|
246
|
-
|
|
260
|
+
// Defer the actual email send. Enqueue is a DB write into the outbox, so it
|
|
261
|
+
// commits atomically with the token insert. If commit fails, the task never
|
|
262
|
+
// runs. If the task fails, retries + eventual dead-letter; the token expires
|
|
263
|
+
// (linkTtl) and the user re-requests.
|
|
264
|
+
await ctx.tasks.enqueue({ kind: "sendMagicLinkEmail", payload: { email: input.email, token } });
|
|
247
265
|
return { ok: true };
|
|
248
266
|
}, { input: parseEmail }),
|
|
249
267
|
loginWithMagicLink: mutation(async (ctx, input) => {
|
|
@@ -278,6 +296,13 @@ export function createMagicLinkAuth(opts) {
|
|
|
278
296
|
return { token, user: { username: email, roles } };
|
|
279
297
|
}, { input: parseLinkToken }),
|
|
280
298
|
};
|
|
299
|
+
const tasks = {
|
|
300
|
+
sendMagicLinkEmail: async (ctx, payload) => {
|
|
301
|
+
const p = payload;
|
|
302
|
+
await opts.sendEmail(ctx, { email: p.email, token: p.token });
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
return { handlers, tasks };
|
|
281
306
|
}
|
|
282
307
|
// --- user management ---------------------------------------------------------
|
|
283
308
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "Optional credential→JWT login for pramen — signup/login/me + PBKDF2 hashing, issuing HS256 tokens the pramen verifier accepts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@pramen/server": "0.0.
|
|
37
|
+
"@pramen/server": "0.0.19"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// the token lifecycle. See createMagicLinkAuth below.
|
|
18
18
|
|
|
19
19
|
import { Entity, mutation, query, defaultTo, unique, hidden, policy, allow, $identity, BadRequest, Unauthorized } from "@pramen/server";
|
|
20
|
-
import type { HandlerContext, Policy } from "@pramen/server";
|
|
20
|
+
import type { AppTaskMap, HandlerContext, HandlerMap, Policy } from "@pramen/server";
|
|
21
21
|
|
|
22
22
|
// --- schema fragment: spread into your defineSchema so the table is migrated ---
|
|
23
23
|
|
|
@@ -277,8 +277,11 @@ export interface MagicLinkOptions {
|
|
|
277
277
|
* On Cloudflare the recommended transport is Cloudflare Email Sending — a
|
|
278
278
|
* `send_email` binding (no API keys), e.g.
|
|
279
279
|
* `await (ctx.env.EMAIL as SendEmail).send({ to, from: { email, name }, subject, text, html })`
|
|
280
|
-
* (see example/app.ts + oblaka.ts).
|
|
281
|
-
*
|
|
280
|
+
* (see example/app.ts + oblaka.ts). Called from the `sendMagicLinkEmail` TASK, not
|
|
281
|
+
* inline in the mutation — a slow SMTP/API call can't hold the mutation's storage
|
|
282
|
+
* transaction open and time the store out. Retries follow the outbox retry policy;
|
|
283
|
+
* a permanent failure dead-letters the task and the token expires unused (users just
|
|
284
|
+
* request a new link). */
|
|
282
285
|
sendEmail: (ctx: HandlerContext, args: { email: string; token: string }) => void | Promise<void>;
|
|
283
286
|
/** How long the emailed link stays valid, in seconds. Default 900 (15 min). */
|
|
284
287
|
linkTtlSeconds?: number;
|
|
@@ -288,15 +291,30 @@ export interface MagicLinkOptions {
|
|
|
288
291
|
defaultRoles?: string[];
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
/** Build the `requestMagicLink` / `loginWithMagicLink` handler pair
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
|
|
294
|
+
/** Build the `requestMagicLink` / `loginWithMagicLink` handler pair, plus the
|
|
295
|
+
* `sendMagicLinkEmail` task handler that actually invokes `opts.sendEmail`.
|
|
296
|
+
*
|
|
297
|
+
* ```
|
|
298
|
+
* const magicLink = createMagicLinkAuth({ sendEmail: ... });
|
|
299
|
+
* const handlers = { ...cmsHandlers, ...authHandlers, ...magicLink.handlers };
|
|
300
|
+
* const tasks = { ...cmsTasks, ...magicLink.tasks };
|
|
301
|
+
* ```
|
|
302
|
+
*
|
|
303
|
+
* The `requestMagicLink` handler writes the token and ENQUEUES a task (atomic with
|
|
304
|
+
* the write via the transactional outbox); the drainer runs `sendMagicLinkEmail`
|
|
305
|
+
* AFTER commit, outside the mutation's storage transaction. This avoids the class
|
|
306
|
+
* of failure where a slow SMTP/API call holds the storage lock long enough for the
|
|
307
|
+
* store to time out and reset the underlying object.
|
|
308
|
+
*
|
|
309
|
+
* You MUST spread `magicLink.tasks` into your app's task map — without it, tokens
|
|
310
|
+
* get written but the email never sends (the drainer retries then dead-letters).
|
|
311
|
+
* Both handlers are anonymous — gate nothing; the token is the capability. */
|
|
312
|
+
export function createMagicLinkAuth(opts: MagicLinkOptions): { handlers: HandlerMap; tasks: AppTaskMap } {
|
|
295
313
|
const linkTtlMs = (opts.linkTtlSeconds ?? 900) * 1000;
|
|
296
314
|
const sessionTtl = opts.sessionTtlSeconds ?? TOKEN_TTL_SECONDS;
|
|
297
315
|
const defaultRoles = opts.defaultRoles ?? DEFAULT_ROLES;
|
|
298
316
|
|
|
299
|
-
|
|
317
|
+
const handlers: HandlerMap = {
|
|
300
318
|
requestMagicLink: mutation(
|
|
301
319
|
async (ctx, input: { email: string }) => {
|
|
302
320
|
const token = mintToken();
|
|
@@ -311,8 +329,11 @@ export function createMagicLinkAuth(opts: MagicLinkOptions) {
|
|
|
311
329
|
now + linkTtlMs,
|
|
312
330
|
now,
|
|
313
331
|
);
|
|
314
|
-
//
|
|
315
|
-
|
|
332
|
+
// Defer the actual email send. Enqueue is a DB write into the outbox, so it
|
|
333
|
+
// commits atomically with the token insert. If commit fails, the task never
|
|
334
|
+
// runs. If the task fails, retries + eventual dead-letter; the token expires
|
|
335
|
+
// (linkTtl) and the user re-requests.
|
|
336
|
+
await ctx.tasks.enqueue({ kind: "sendMagicLinkEmail", payload: { email: input.email, token } });
|
|
316
337
|
return { ok: true };
|
|
317
338
|
},
|
|
318
339
|
{ input: parseEmail },
|
|
@@ -361,6 +382,15 @@ export function createMagicLinkAuth(opts: MagicLinkOptions) {
|
|
|
361
382
|
{ input: parseLinkToken },
|
|
362
383
|
),
|
|
363
384
|
};
|
|
385
|
+
|
|
386
|
+
const tasks: AppTaskMap = {
|
|
387
|
+
sendMagicLinkEmail: async (ctx, payload) => {
|
|
388
|
+
const p = payload as { email: string; token: string };
|
|
389
|
+
await opts.sendEmail(ctx, { email: p.email, token: p.token });
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return { handlers, tasks };
|
|
364
394
|
}
|
|
365
395
|
|
|
366
396
|
// --- user management ---------------------------------------------------------
|