@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 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: Record<string, unknown>, secret: string, opts?: {
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
- }, Record<string, unknown>>;
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
- }, Record<string, unknown>>;
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
- }, Record<string, unknown>>;
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
- }, Record<string, unknown>>;
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.47",
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.47"
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: Record<string, unknown>,
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: unknown): { username: string; password: string; email?: string } {
248
- const o = (raw ?? {}) as Record<string, unknown>;
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<Record<string, unknown> | undefined> {
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: unknown): { email: string } {
483
- const o = (raw ?? {}) as Record<string, unknown>;
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: unknown): { token: string } {
490
- const o = (raw ?? {}) as Record<string, unknown>;
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): { handlers: HandlerMap; tasks: AppTaskMap } {
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: unknown): boolean {
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: Record<string, unknown>): Promise<Record<string, unknown> | undefined>;
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 unknown as UsersDb;
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: unknown): { token: string; newPassword: string } {
921
- const o = (raw ?? {}) as Record<string, unknown>;
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): { handlers: HandlerMap; tasks: AppTaskMap } {
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): { handlers: HandlerMap; tasks: AppTaskMap } {
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