@pramen/auth 0.0.47 → 0.0.49
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 +15 -18
- package/package.json +2 -2
- package/src/index.ts +31 -17
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type { AppTaskMap, HandlerContext, HandlerMap, Policy } from "@pramen/server";
|
|
1
|
+
import type { AppTaskMap, HandlerContext, HandlerMap, JsonObject, Policy, Row } from "@pramen/server";
|
|
2
|
+
/** What an auth factory contributes to an app: RPC handlers plus the task handlers
|
|
3
|
+
* that deliver their emails. */
|
|
4
|
+
export interface AuthModule {
|
|
5
|
+
handlers: HandlerMap;
|
|
6
|
+
tasks: AppTaskMap;
|
|
7
|
+
}
|
|
2
8
|
export declare const authSchema: {
|
|
3
9
|
auth_users: import("@pramen/server").EntityDef<{
|
|
4
10
|
username: {
|
|
@@ -44,7 +50,7 @@ export declare function registerPasswordVerifier(scheme: string, verify: Passwor
|
|
|
44
50
|
* after a successful verify. Also true for an unparseable value, which never verifies. */
|
|
45
51
|
export declare function isForeignHash(stored: string): boolean;
|
|
46
52
|
export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
47
|
-
export declare function signToken(claims:
|
|
53
|
+
export declare function signToken(claims: JsonObject, secret: string, opts?: {
|
|
48
54
|
ttlSeconds?: number;
|
|
49
55
|
}): Promise<string>;
|
|
50
56
|
export interface AuthHandlerOptions {
|
|
@@ -217,10 +223,7 @@ export interface MagicLinkOptions {
|
|
|
217
223
|
* You MUST spread `magicLink.tasks` into your app's task map — without it, tokens
|
|
218
224
|
* get written but the email never sends (the drainer retries then dead-letters).
|
|
219
225
|
* Both handlers are anonymous — gate nothing; the token is the capability. */
|
|
220
|
-
export declare function createMagicLinkAuth(opts: MagicLinkOptions):
|
|
221
|
-
handlers: HandlerMap;
|
|
222
|
-
tasks: AppTaskMap;
|
|
223
|
-
};
|
|
226
|
+
export declare function createMagicLinkAuth(opts: MagicLinkOptions): AuthModule;
|
|
224
227
|
/** Build admin + self-service handlers over a users table (default `auth_users`).
|
|
225
228
|
* Pass `table` to operate over your OWN authSchema-shaped table — e.g. one with an
|
|
226
229
|
* extra `tenants` column — without renaming it: the handlers manage username/roles/
|
|
@@ -244,7 +247,7 @@ export declare function createUserHandlers(opts?: {
|
|
|
244
247
|
setUserRoles: import("@pramen/server").Handler<{
|
|
245
248
|
username: string;
|
|
246
249
|
roles: string[];
|
|
247
|
-
},
|
|
250
|
+
}, Row>;
|
|
248
251
|
/** Admin: activate / deactivate a user. Deactivating blocks future logins AND
|
|
249
252
|
* refreshSession, AND revokes OUTSTANDING tokens immediately via the KV denylist (the
|
|
250
253
|
* Worker fails them closed) — so revocation no longer waits out the token TTL. The
|
|
@@ -253,7 +256,7 @@ export declare function createUserHandlers(opts?: {
|
|
|
253
256
|
setUserActive: import("@pramen/server").Handler<{
|
|
254
257
|
username: string;
|
|
255
258
|
active: boolean;
|
|
256
|
-
},
|
|
259
|
+
}, Row>;
|
|
257
260
|
/** Admin: permanently delete a user. ACL-gated by the admin delete policy; a caller
|
|
258
261
|
* cannot delete their own account. Deactivation (setUserActive) is usually preferable. */
|
|
259
262
|
deleteUser: import("@pramen/server").Handler<{
|
|
@@ -297,7 +300,7 @@ export declare const userHandlers: {
|
|
|
297
300
|
setUserRoles: import("@pramen/server").Handler<{
|
|
298
301
|
username: string;
|
|
299
302
|
roles: string[];
|
|
300
|
-
},
|
|
303
|
+
}, Row>;
|
|
301
304
|
/** Admin: activate / deactivate a user. Deactivating blocks future logins AND
|
|
302
305
|
* refreshSession, AND revokes OUTSTANDING tokens immediately via the KV denylist (the
|
|
303
306
|
* Worker fails them closed) — so revocation no longer waits out the token TTL. The
|
|
@@ -306,7 +309,7 @@ export declare const userHandlers: {
|
|
|
306
309
|
setUserActive: import("@pramen/server").Handler<{
|
|
307
310
|
username: string;
|
|
308
311
|
active: boolean;
|
|
309
|
-
},
|
|
312
|
+
}, Row>;
|
|
310
313
|
/** Admin: permanently delete a user. ACL-gated by the admin delete policy; a caller
|
|
311
314
|
* cannot delete their own account. Deactivation (setUserActive) is usually preferable. */
|
|
312
315
|
deleteUser: import("@pramen/server").Handler<{
|
|
@@ -381,10 +384,7 @@ export interface PasswordResetOptions {
|
|
|
381
384
|
* when an active account matches the email); `resetPassword` redeems the single-use token
|
|
382
385
|
* and sets the new password. Spread `emailTokenSchema` into your schema and `.tasks` into
|
|
383
386
|
* your task map. */
|
|
384
|
-
export declare function createPasswordReset(opts: PasswordResetOptions):
|
|
385
|
-
handlers: HandlerMap;
|
|
386
|
-
tasks: AppTaskMap;
|
|
387
|
-
};
|
|
387
|
+
export declare function createPasswordReset(opts: PasswordResetOptions): AuthModule;
|
|
388
388
|
export interface EmailVerificationOptions {
|
|
389
389
|
/** Deliver the verification link. Receives the ctx + `{ email, token, username }` — build
|
|
390
390
|
* the URL your app routes to, e.g. `${ctx.env.APP_URL}/verify?token=${token}`. Called from
|
|
@@ -406,7 +406,4 @@ export interface EmailVerificationOptions {
|
|
|
406
406
|
* `auth_users.emailVerified`. A token is bound to the address current at request time, so a
|
|
407
407
|
* later `changeEmail` invalidates it (verifyEmail rejects a token whose address no longer
|
|
408
408
|
* matches). Spread `emailTokenSchema` into your schema and `.tasks` into your task map. */
|
|
409
|
-
export declare function createEmailVerification(opts: EmailVerificationOptions):
|
|
410
|
-
handlers: HandlerMap;
|
|
411
|
-
tasks: AppTaskMap;
|
|
412
|
-
};
|
|
409
|
+
export declare function createEmailVerification(opts: EmailVerificationOptions): AuthModule;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49",
|
|
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.49"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,21 @@
|
|
|
17
17
|
// the token lifecycle. See createMagicLinkAuth below.
|
|
18
18
|
|
|
19
19
|
import { Entity, mutation, query, defaultTo, unique, hidden, policy, allow, $identity, BadRequest, Unauthorized, denySession, allowSession } from "@pramen/server";
|
|
20
|
-
import type { AppTaskMap, HandlerContext, HandlerMap, Policy } from "@pramen/server";
|
|
20
|
+
import type { AppTaskMap, CellValue, HandlerContext, HandlerMap, JsonObject, JsonValue, Policy, Row } from "@pramen/server";
|
|
21
|
+
|
|
22
|
+
/** What an auth factory contributes to an app: RPC handlers plus the task handlers
|
|
23
|
+
* that deliver their emails. */
|
|
24
|
+
export interface AuthModule {
|
|
25
|
+
handlers: HandlerMap;
|
|
26
|
+
tasks: AppTaskMap;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Validated signup / login credentials, parsed from the request input. */
|
|
30
|
+
interface Credentials {
|
|
31
|
+
username: string;
|
|
32
|
+
password: string;
|
|
33
|
+
email?: string;
|
|
34
|
+
}
|
|
21
35
|
|
|
22
36
|
// --- schema fragment: spread into your defineSchema so the table is migrated ---
|
|
23
37
|
|
|
@@ -209,7 +223,7 @@ function dummyPasswordHash(): Promise<string> {
|
|
|
209
223
|
// --- HS256 token signing (matches the verifier in @pramen/server auth.ts) ---
|
|
210
224
|
|
|
211
225
|
export async function signToken(
|
|
212
|
-
claims:
|
|
226
|
+
claims: JsonObject,
|
|
213
227
|
secret: string,
|
|
214
228
|
opts: { ttlSeconds?: number } = {},
|
|
215
229
|
): Promise<string> {
|
|
@@ -244,8 +258,8 @@ function sessionTtlOf(ctx: HandlerContext): number {
|
|
|
244
258
|
* truth for `parseEmail` and the optional email at signup. */
|
|
245
259
|
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
|
246
260
|
|
|
247
|
-
function parseCreds(raw:
|
|
248
|
-
const o = (raw ?? {}) as
|
|
261
|
+
function parseCreds(raw: JsonValue): Credentials {
|
|
262
|
+
const o = (raw ?? {}) as JsonObject;
|
|
249
263
|
if (typeof o.username !== "string" || o.username.length === 0) throw new Error("username is required");
|
|
250
264
|
if (typeof o.password !== "string" || o.password.length < 8) throw new Error("password must be at least 8 characters");
|
|
251
265
|
// Optional contact email at signup — validated + normalized when present, so password
|
|
@@ -313,7 +327,7 @@ export function createAuthHandlers(opts: AuthHandlerOptions = {}) {
|
|
|
313
327
|
|
|
314
328
|
/** Resolve the submitted identifier to a row, per `loginBy`. Returns undefined when
|
|
315
329
|
* nothing matches; the caller still runs a dummy verify so the timing is flat. */
|
|
316
|
-
async function findLoginRow(ctx: HandlerContext, identifier: string): Promise<
|
|
330
|
+
async function findLoginRow(ctx: HandlerContext, identifier: string): Promise<Row | undefined> {
|
|
317
331
|
const cols = "SELECT username, passwordHash, roles, active FROM auth_users";
|
|
318
332
|
if (loginBy !== "email") {
|
|
319
333
|
const byName = await ctx.db.exec(`${cols} WHERE username = ? LIMIT 1`, identifier);
|
|
@@ -479,15 +493,15 @@ function mintToken(): string {
|
|
|
479
493
|
return b64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
480
494
|
}
|
|
481
495
|
|
|
482
|
-
function parseEmail(raw:
|
|
483
|
-
const o = (raw ?? {}) as
|
|
496
|
+
function parseEmail(raw: JsonValue): { email: string } {
|
|
497
|
+
const o = (raw ?? {}) as JsonObject;
|
|
484
498
|
const email = typeof o.email === "string" ? o.email.trim().toLowerCase() : "";
|
|
485
499
|
if (!EMAIL_RE.test(email)) throw new BadRequest("a valid email is required");
|
|
486
500
|
return { email };
|
|
487
501
|
}
|
|
488
502
|
|
|
489
|
-
function parseLinkToken(raw:
|
|
490
|
-
const o = (raw ?? {}) as
|
|
503
|
+
function parseLinkToken(raw: JsonValue): { token: string } {
|
|
504
|
+
const o = (raw ?? {}) as JsonObject;
|
|
491
505
|
if (typeof o.token !== "string" || o.token.length === 0) throw new BadRequest("token is required");
|
|
492
506
|
return { token: o.token };
|
|
493
507
|
}
|
|
@@ -530,7 +544,7 @@ export interface MagicLinkOptions {
|
|
|
530
544
|
* You MUST spread `magicLink.tasks` into your app's task map — without it, tokens
|
|
531
545
|
* get written but the email never sends (the drainer retries then dead-letters).
|
|
532
546
|
* Both handlers are anonymous — gate nothing; the token is the capability. */
|
|
533
|
-
export function createMagicLinkAuth(opts: MagicLinkOptions):
|
|
547
|
+
export function createMagicLinkAuth(opts: MagicLinkOptions): AuthModule {
|
|
534
548
|
const linkTtlMs = (opts.linkTtlSeconds ?? 900) * 1000;
|
|
535
549
|
const sessionTtl = opts.sessionTtlSeconds ?? TOKEN_TTL_SECONDS;
|
|
536
550
|
const defaultRoles = opts.defaultRoles ?? DEFAULT_ROLES;
|
|
@@ -688,7 +702,7 @@ export function createMagicLinkAuth(opts: MagicLinkOptions): { handlers: Handler
|
|
|
688
702
|
// @pramen/server so an app can revoke on its own compromise signals too.
|
|
689
703
|
|
|
690
704
|
/** SQLite has no bool: `active` is stored 0/1 (NULL on a pre-column row = active). */
|
|
691
|
-
function isActive(v:
|
|
705
|
+
function isActive(v: CellValue): boolean {
|
|
692
706
|
return v == null || Number(v) !== 0;
|
|
693
707
|
}
|
|
694
708
|
|
|
@@ -702,10 +716,10 @@ function requireUserId(ctx: HandlerContext): string {
|
|
|
702
716
|
// can't import — so address the users table through a minimal structural view of the
|
|
703
717
|
// ACL'd Db. This is the same ctx.db at runtime: row-scope + field projection still apply.
|
|
704
718
|
interface UsersDb {
|
|
705
|
-
update(table: string, id: string, patch:
|
|
719
|
+
update(table: string, id: string, patch: Row): Promise<Row | undefined>;
|
|
706
720
|
delete(table: string, id: string): Promise<boolean>;
|
|
707
721
|
}
|
|
708
|
-
const usersDb = (ctx: HandlerContext): UsersDb => ctx.db as
|
|
722
|
+
const usersDb = (ctx: HandlerContext): UsersDb => ctx.db as UsersDb;
|
|
709
723
|
|
|
710
724
|
// The users table must be a valid SQL identifier (it's interpolated into the raw exec
|
|
711
725
|
// strings below). It's app config, never request input, but guard it anyway.
|
|
@@ -917,8 +931,8 @@ async function redeemEmailToken(ctx: HandlerContext, purpose: string, token: str
|
|
|
917
931
|
}
|
|
918
932
|
|
|
919
933
|
/** Parse `{ token, newPassword }` for `resetPassword`. */
|
|
920
|
-
function parseResetInput(raw:
|
|
921
|
-
const o = (raw ?? {}) as
|
|
934
|
+
function parseResetInput(raw: JsonValue): { token: string; newPassword: string } {
|
|
935
|
+
const o = (raw ?? {}) as JsonObject;
|
|
922
936
|
if (typeof o.token !== "string" || o.token.length === 0) throw new BadRequest("token is required");
|
|
923
937
|
if (typeof o.newPassword !== "string" || o.newPassword.length < 8) throw new BadRequest("newPassword must be at least 8 characters");
|
|
924
938
|
return { token: o.token, newPassword: o.newPassword };
|
|
@@ -942,7 +956,7 @@ export interface PasswordResetOptions {
|
|
|
942
956
|
* when an active account matches the email); `resetPassword` redeems the single-use token
|
|
943
957
|
* and sets the new password. Spread `emailTokenSchema` into your schema and `.tasks` into
|
|
944
958
|
* your task map. */
|
|
945
|
-
export function createPasswordReset(opts: PasswordResetOptions):
|
|
959
|
+
export function createPasswordReset(opts: PasswordResetOptions): AuthModule {
|
|
946
960
|
const table = assertIdentifier(opts.table ?? "auth_users");
|
|
947
961
|
const linkTtlMs = (opts.linkTtlSeconds ?? 3600) * 1000;
|
|
948
962
|
|
|
@@ -1008,7 +1022,7 @@ export interface EmailVerificationOptions {
|
|
|
1008
1022
|
* `auth_users.emailVerified`. A token is bound to the address current at request time, so a
|
|
1009
1023
|
* later `changeEmail` invalidates it (verifyEmail rejects a token whose address no longer
|
|
1010
1024
|
* matches). Spread `emailTokenSchema` into your schema and `.tasks` into your task map. */
|
|
1011
|
-
export function createEmailVerification(opts: EmailVerificationOptions):
|
|
1025
|
+
export function createEmailVerification(opts: EmailVerificationOptions): AuthModule {
|
|
1012
1026
|
const table = assertIdentifier(opts.table ?? "auth_users");
|
|
1013
1027
|
const linkTtlMs = (opts.linkTtlSeconds ?? 86_400) * 1000;
|
|
1014
1028
|
|