@nifrajs/auth 2.2.0 → 2.4.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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @nifrajs/auth
2
2
 
3
- Session primitives for nifra — signed-cookie + server-store sessions, CSRF, and route guards. Bring your own identity (Better Auth / Lucia / OAuth); nifra owns the session.
3
+ Session primitives for nifra - signed-cookie + server-store sessions, CSRF, and route guards. Bring your own identity (Better Auth / Lucia / OAuth); nifra owns the session.
4
4
 
5
- Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework — one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
5
+ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
6
6
 
7
7
  ## Install
8
8
 
@@ -19,7 +19,7 @@ MIT
19
19
 
20
20
  ## For AI agents
21
21
 
22
- Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
22
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
23
23
  one cheap read instead of the whole corpus. For the wider framework: the repo's
24
24
  [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
25
25
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
package/dist/csrf.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CSRF protection via an **Origin / Referer check** on state-changing requests — OWASP's recommended
2
+ * CSRF protection via an **Origin / Referer check** on state-changing requests - OWASP's recommended
3
3
  * defense for cookie-authenticated apps. A browser always attaches `Origin` (or at least `Referer`) to
4
4
  * a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
5
5
  * methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
@@ -7,7 +7,7 @@
7
7
  import type { Middleware } from "@nifrajs/core/server";
8
8
  export interface CsrfOptions {
9
9
  /**
10
- * Allowed origins (e.g. `["https://example.com"]`). **Set this in production behind a proxy** — when
10
+ * Allowed origins (e.g. `["https://example.com"]`). **Set this in production behind a proxy** - when
11
11
  * omitted, the check derives same-origin from the request URL, which is correct in dev / when the
12
12
  * proxy preserves `Host` but not when the public origin differs from the worker's.
13
13
  */
package/dist/csrf.js CHANGED
@@ -11,7 +11,7 @@ export function csrf(options = {}) {
11
11
  const origin = req.headers.get("origin");
12
12
  if (origin !== null)
13
13
  return allowed.has(origin) ? undefined : forbidden();
14
- // Some same-origin requests omit `Origin` — fall back to the `Referer`'s origin.
14
+ // Some same-origin requests omit `Origin` - fall back to the `Referer`'s origin.
15
15
  const referer = req.headers.get("referer");
16
16
  if (referer !== null) {
17
17
  let refererOrigin;
package/dist/guards.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Route guards — run at the top of a protected loader/action/handler. On a missing session they
2
+ * Route guards - run at the top of a protected loader/action/handler. On a missing session they
3
3
  * **throw a `Response`** (a 302 to your login path, or a 401); nifra returns a thrown Response as-is, so
4
4
  * the guard short-circuits the rest of the handler. Pairs with `@nifrajs/auth` sessions but only needs the
5
5
  * `Session` shape, so it's framework-agnostic.
6
6
  */
7
7
  import type { Session } from "./session.js";
8
- /** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or — omitted — a
8
+ /** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or - omitted - a
9
9
  * 401 JSON (`{ ok: false, error: "unauthorized" }`). */
10
10
  export interface GuardOptions {
11
11
  readonly redirectTo?: string;
package/dist/guards.js CHANGED
@@ -4,7 +4,7 @@ const rejection = (options) => {
4
4
  return Response.json({ ok: false, error: "unauthorized" }, { status: 401 });
5
5
  }
6
6
  // Same-origin guard (mirrors @nifrajs/web `redirect`): a single leading "/", never "//host" or an
7
- // absolute URL. `redirectTo` is dev-authored, so a bad value is a config bug — fail loud here.
7
+ // absolute URL. `redirectTo` is dev-authored, so a bad value is a config bug - fail loud here.
8
8
  if (!to.startsWith("/") || to.startsWith("//")) {
9
9
  throw new Error(`[nifra/auth] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`);
10
10
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/auth` — session primitives for nifra. Signed-cookie + server-store sessions, with the same
2
+ * `@nifrajs/auth` - session primitives for nifra. Signed-cookie + server-store sessions, with the same
3
3
  * pluggable-store + prod-guard discipline as the ISR cache. **Bring your own identity** (Better Auth,
4
4
  * Lucia, your own OAuth/password logic); nifra owns the session cookie, store, CSRF, and route guards.
5
5
  */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/auth` — session primitives for nifra. Signed-cookie + server-store sessions, with the same
2
+ * `@nifrajs/auth` - session primitives for nifra. Signed-cookie + server-store sessions, with the same
3
3
  * pluggable-store + prod-guard discipline as the ISR cache. **Bring your own identity** (Better Auth,
4
4
  * Lucia, your own OAuth/password logic); nifra owns the session cookie, store, CSRF, and route guards.
5
5
  */
package/dist/session.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  /**
2
- * The session manager — `createSessions<Data>()` returns get/commit/destroy/regenerate bound to a
2
+ * The session manager - `createSessions<Data>()` returns get/commit/destroy/regenerate bound to a
3
3
  * signing secret + cookie config. Two modes:
4
4
  * • **store mode** (a {@link SessionStore} is given): a random session id rides a signed cookie; the
5
5
  * data lives in the store. Big/sensitive data, server-side revocation.
6
6
  * • **cookie mode** (no store): the data itself is signed into the cookie (stateless, edge-friendly,
7
7
  * ≤4 KB). No server state.
8
8
  * The cookie value is **always HMAC-signed** (tamper-evident; verified constant-time) and the session
9
- * cookie is always `HttpOnly`. Reads **fail closed** — a tampered/missing/expired cookie yields a fresh
9
+ * cookie is always `HttpOnly`. Reads **fail closed** - a tampered/missing/expired cookie yields a fresh
10
10
  * anonymous session, never an error.
11
11
  */
12
12
  import { type CookieOptions } from "@nifrajs/core/cookies";
13
13
  import type { SessionStore } from "./store.js";
14
- /** The cookie + response surface the manager needs — a structural subset of nifra's `Context`, so any
14
+ /** The cookie + response surface the manager needs - a structural subset of nifra's `Context`, so any
15
15
  * `c` satisfies it and it's testable with a stub. */
16
16
  export interface SessionContext {
17
17
  readonly cookies: Readonly<Record<string, string>>;
@@ -23,7 +23,7 @@ export interface SessionContext {
23
23
  }): void;
24
24
  };
25
25
  }
26
- /** A typed session handle. Every key is optional — a fresh session is empty. */
26
+ /** A typed session handle. Every key is optional - a fresh session is empty. */
27
27
  export interface Session<Data extends Record<string, unknown> = Record<string, unknown>> {
28
28
  get<K extends keyof Data>(key: K): Data[K] | undefined;
29
29
  set<K extends keyof Data>(key: K, value: Data[K]): void;
@@ -35,7 +35,7 @@ export interface Session<Data extends Record<string, unknown> = Record<string, u
35
35
  readonly data: Readonly<Partial<Data>>;
36
36
  readonly isEmpty: boolean;
37
37
  }
38
- /** Cookie attributes a session may tune. `httpOnly` is **not** offered — a session cookie is always
38
+ /** Cookie attributes a session may tune. `httpOnly` is **not** offered - a session cookie is always
39
39
  * HttpOnly; `maxAge`/`expires` are derived from the session's lifetime. */
40
40
  export type SessionCookieOptions = Pick<CookieOptions, "secure" | "sameSite" | "path" | "domain">;
41
41
  export interface SessionOptions {
@@ -52,20 +52,20 @@ export interface SessionOptions {
52
52
  /** Extra cookie attributes (the session cookie is always HttpOnly; `Secure`/`SameSite=Lax`/`Path=/`
53
53
  * default via `c.set.cookie`). Pass `{ secure: false }` for local http dev. */
54
54
  readonly cookie?: SessionCookieOptions;
55
- /** Clock (ms) — injected for testability; production passes `() => Date.now()` (the default). */
55
+ /** Clock (ms) - injected for testability; production passes `() => Date.now()` (the default). */
56
56
  readonly now?: () => number;
57
57
  }
58
58
  export interface SessionManager<Data extends Record<string, unknown> = Record<string, unknown>> {
59
59
  /** Load the session from the request cookie (verified + un-expired), or a fresh anonymous one. */
60
60
  get(c: SessionContext): Promise<Session<Data>>;
61
- /** Read-only load from a raw `Request` — for `@nifrajs/web` loaders (which can read the request but not
61
+ /** Read-only load from a raw `Request` - for `@nifrajs/web` loaders (which can read the request but not
62
62
  * write cookies). Identical verify/expiry to {@link get}; commit/destroy in a route or action. */
63
63
  read(request: Request): Promise<Session<Data>>;
64
64
  /** Persist the session: write the store (store mode) + the signed cookie. */
65
65
  commit(c: SessionContext, session: Session<Data>): Promise<void>;
66
66
  /** End the session: drop the store record (store mode) + clear the cookie. */
67
67
  destroy(c: SessionContext, session?: Session<Data>): Promise<void>;
68
- /** Rotate the session id on the next commit (call on privilege change — login — to defend against
68
+ /** Rotate the session id on the next commit (call on privilege change - login - to defend against
69
69
  * session fixation). Data is preserved; the old store record is dropped. */
70
70
  regenerate(session: Session<Data>): void;
71
71
  }
package/dist/session.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * The session manager — `createSessions<Data>()` returns get/commit/destroy/regenerate bound to a
2
+ * The session manager - `createSessions<Data>()` returns get/commit/destroy/regenerate bound to a
3
3
  * signing secret + cookie config. Two modes:
4
4
  * • **store mode** (a {@link SessionStore} is given): a random session id rides a signed cookie; the
5
5
  * data lives in the store. Big/sensitive data, server-side revocation.
6
6
  * • **cookie mode** (no store): the data itself is signed into the cookie (stateless, edge-friendly,
7
7
  * ≤4 KB). No server state.
8
8
  * The cookie value is **always HMAC-signed** (tamper-evident; verified constant-time) and the session
9
- * cookie is always `HttpOnly`. Reads **fail closed** — a tampered/missing/expired cookie yields a fresh
9
+ * cookie is always `HttpOnly`. Reads **fail closed** - a tampered/missing/expired cookie yields a fresh
10
10
  * anonymous session, never an error.
11
11
  */
12
12
  import { parseCookies, signValue, unsignValue } from "@nifrajs/core/cookies";
@@ -22,9 +22,9 @@ const encodeBase64Url = (bytes) => {
22
22
  bin += String.fromCharCode(b);
23
23
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
24
24
  };
25
- /** A 256-bit random session id (base64url). High-entropy + unguessable — the cookie also signs it. */
25
+ /** A 256-bit random session id (base64url). High-entropy + unguessable - the cookie also signs it. */
26
26
  const randomId = () => encodeBase64Url(crypto.getRandomValues(new Uint8Array(32)));
27
- /** Validate a cookie-mode payload at the trust boundary — malformed JSON / shape → `null` (no session). */
27
+ /** Validate a cookie-mode payload at the trust boundary - malformed JSON / shape → `null` (no session). */
28
28
  const parsePayload = (json) => {
29
29
  let value;
30
30
  try {
@@ -51,7 +51,7 @@ export function createSessions(options) {
51
51
  const maxAge = options.maxAge ?? 7 * DAY_SECONDS;
52
52
  const rolling = options.rolling ?? true;
53
53
  const now = options.now ?? (() => Date.now());
54
- // Annotated (not just `?? {}`) so `cookieOpts.path`/`.domain` stay typed — `?? {}` alone widens the
54
+ // Annotated (not just `?? {}`) so `cookieOpts.path`/`.domain` stay typed - `?? {}` alone widens the
55
55
  // type to include the empty-object literal, which the package's build tsconfig rejects on access.
56
56
  const cookieOpts = options.cookie ?? {};
57
57
  // Per-manager so the WeakMap is typed to this `Data` (no `any`); a session can only be committed by
@@ -105,7 +105,7 @@ export function createSessions(options) {
105
105
  if (record === undefined)
106
106
  return make({});
107
107
  if (record.expiresAt <= now()) {
108
- await store.delete(unsigned); // expired — evict + treat as no session
108
+ await store.delete(unsigned); // expired - evict + treat as no session
109
109
  return make({});
110
110
  }
111
111
  return make({ id: unsigned, data: record.data, expiresAt: record.expiresAt });
@@ -116,7 +116,7 @@ export function createSessions(options) {
116
116
  return make({ data: payload.data, expiresAt: payload.expiresAt });
117
117
  };
118
118
  const get = (c) => loadFromRaw(c.cookies[cookieName]);
119
- // Read-only load from a raw Request — for @nifrajs/web loaders, which have the request but can't write
119
+ // Read-only load from a raw Request - for @nifrajs/web loaders, which have the request but can't write
120
120
  // cookies (commit/destroy belong in a route/action with the full Context). Same verify + expiry.
121
121
  const read = (request) => loadFromRaw(parseCookies(request.headers.get("cookie"))[cookieName]);
122
122
  const writeCookie = (c, value, expiresAt) => {
package/dist/store.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Session stores (the **store mode** backend) — a pluggable `SessionStore` plus an in-memory
2
+ * Session stores (the **store mode** backend) - a pluggable `SessionStore` plus an in-memory
3
3
  * implementation (dev / single-instance, prod-guarded) and a Cloudflare Workers KV implementation
4
4
  * (the durable, shared production path). The store is dumb persistence: the session *manager* owns the
5
5
  * clock and authoritatively enforces expiry, so the store stays clock-free and trivially testable.
6
6
  */
7
- /** A persisted session — its data plus an absolute expiry (ms epoch). */
7
+ /** A persisted session - its data plus an absolute expiry (ms epoch). */
8
8
  export interface SessionRecord {
9
9
  readonly data: Record<string, unknown>;
10
10
  /** Absolute expiry (ms epoch). The manager treats `expiresAt <= now` as no session. */
@@ -12,7 +12,7 @@ export interface SessionRecord {
12
12
  }
13
13
  /**
14
14
  * Pluggable session backend (store mode). Async so a network store (KV/Redis) fits the same shape.
15
- * **Production needs a shared/durable store** so sessions hold across instances — {@link MemorySessionStore}
15
+ * **Production needs a shared/durable store** so sessions hold across instances - {@link MemorySessionStore}
16
16
  * prod-guards against the per-instance footgun.
17
17
  */
18
18
  export interface SessionStore {
@@ -24,7 +24,7 @@ export interface SessionStore {
24
24
  delete(id: string): Promise<void>;
25
25
  }
26
26
  export interface MemorySessionStoreOptions {
27
- /** Allow the in-memory store in production. Off by default — per-instance sessions don't hold across
27
+ /** Allow the in-memory store in production. Off by default - per-instance sessions don't hold across
28
28
  * instances and are lost on restart. */
29
29
  readonly allowInProduction?: boolean;
30
30
  /** Hard cap on stored sessions; the oldest-inserted is evicted past it (default 10_000). */
@@ -32,7 +32,7 @@ export interface MemorySessionStoreOptions {
32
32
  }
33
33
  /**
34
34
  * In-process session store. Refuses to run in production unless explicitly allowed (mirrors the ISR
35
- * `MemoryCacheStore` + the rate-limit `MemoryStore` — a per-instance store is unsafe across instances).
35
+ * `MemoryCacheStore` + the rate-limit `MemoryStore` - a per-instance store is unsafe across instances).
36
36
  * Bounded: oldest-inserted entries evict past `max`.
37
37
  */
38
38
  export declare class MemorySessionStore implements SessionStore {
@@ -44,7 +44,7 @@ export declare class MemorySessionStore implements SessionStore {
44
44
  delete(id: string): Promise<void>;
45
45
  }
46
46
  /**
47
- * Minimal structural shape of a Cloudflare Workers **KV namespace** binding — just the three methods
47
+ * Minimal structural shape of a Cloudflare Workers **KV namespace** binding - just the three methods
48
48
  * {@link KVSessionStore} uses. Structural (no `@cloudflare/workers-types` dependency) so any KV-like
49
49
  * binding satisfies it and tests can pass an in-memory double.
50
50
  */
@@ -58,9 +58,9 @@ export interface KVNamespaceLike {
58
58
  }
59
59
  /**
60
60
  * A {@link SessionStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike})
61
- * — the durable, shared production store: sessions hold across worker instances and survive restarts.
61
+ * - the durable, shared production store: sessions hold across worker instances and survive restarts.
62
62
  * Records serialize to JSON; the entry's KV `expiration` is set from the record's `expiresAt` so KV
63
- * auto-evicts the session (a GC backstop — the manager still authoritatively checks expiry on read).
63
+ * auto-evicts the session (a GC backstop - the manager still authoritatively checks expiry on read).
64
64
  * Every read is validated, so a corrupt/skewed entry reads as no session.
65
65
  */
66
66
  export declare class KVSessionStore implements SessionStore {
package/dist/store.js CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Session stores (the **store mode** backend) — a pluggable `SessionStore` plus an in-memory
2
+ * Session stores (the **store mode** backend) - a pluggable `SessionStore` plus an in-memory
3
3
  * implementation (dev / single-instance, prod-guarded) and a Cloudflare Workers KV implementation
4
4
  * (the durable, shared production path). The store is dumb persistence: the session *manager* owns the
5
5
  * clock and authoritatively enforces expiry, so the store stays clock-free and trivially testable.
6
6
  */
7
7
  /**
8
8
  * In-process session store. Refuses to run in production unless explicitly allowed (mirrors the ISR
9
- * `MemoryCacheStore` + the rate-limit `MemoryStore` — a per-instance store is unsafe across instances).
9
+ * `MemoryCacheStore` + the rate-limit `MemoryStore` - a per-instance store is unsafe across instances).
10
10
  * Bounded: oldest-inserted entries evict past `max`.
11
11
  */
12
12
  export class MemorySessionStore {
@@ -54,9 +54,9 @@ const isSessionRecord = (value) => {
54
54
  };
55
55
  /**
56
56
  * A {@link SessionStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike})
57
- * — the durable, shared production store: sessions hold across worker instances and survive restarts.
57
+ * - the durable, shared production store: sessions hold across worker instances and survive restarts.
58
58
  * Records serialize to JSON; the entry's KV `expiration` is set from the record's `expiresAt` so KV
59
- * auto-evicts the session (a GC backstop — the manager still authoritatively checks expiry on read).
59
+ * auto-evicts the session (a GC backstop - the manager still authoritatively checks expiry on read).
60
60
  * Every read is validated, so a corrupt/skewed entry reads as no session.
61
61
  */
62
62
  export class KVSessionStore {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/auth",
3
- "version": "2.2.0",
4
- "description": "Session primitives for nifra — signed-cookie + server-store sessions, CSRF, and route guards. Bring your own identity (Better Auth / Lucia / OAuth); nifra owns the session.",
3
+ "version": "2.4.0",
4
+ "description": "Session primitives for nifra - signed-cookie + server-store sessions, CSRF, and route guards. Bring your own identity (Better Auth / Lucia / OAuth); nifra owns the session.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "peerDependencies": {
38
- "@nifrajs/core": "^2.2.0"
38
+ "@nifrajs/core": "^2.4.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@nifrajs/core": "workspace:*"
package/src/csrf.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CSRF protection via an **Origin / Referer check** on state-changing requests — OWASP's recommended
2
+ * CSRF protection via an **Origin / Referer check** on state-changing requests - OWASP's recommended
3
3
  * defense for cookie-authenticated apps. A browser always attaches `Origin` (or at least `Referer`) to
4
4
  * a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
5
5
  * methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
@@ -10,7 +10,7 @@ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"])
10
10
 
11
11
  export interface CsrfOptions {
12
12
  /**
13
- * Allowed origins (e.g. `["https://example.com"]`). **Set this in production behind a proxy** — when
13
+ * Allowed origins (e.g. `["https://example.com"]`). **Set this in production behind a proxy** - when
14
14
  * omitted, the check derives same-origin from the request URL, which is correct in dev / when the
15
15
  * proxy preserves `Host` but not when the public origin differs from the worker's.
16
16
  */
@@ -31,7 +31,7 @@ export function csrf(options: CsrfOptions = {}): Middleware {
31
31
  const origin = req.headers.get("origin")
32
32
  if (origin !== null) return allowed.has(origin) ? undefined : forbidden()
33
33
 
34
- // Some same-origin requests omit `Origin` — fall back to the `Referer`'s origin.
34
+ // Some same-origin requests omit `Origin` - fall back to the `Referer`'s origin.
35
35
  const referer = req.headers.get("referer")
36
36
  if (referer !== null) {
37
37
  let refererOrigin: string
package/src/guards.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Route guards — run at the top of a protected loader/action/handler. On a missing session they
2
+ * Route guards - run at the top of a protected loader/action/handler. On a missing session they
3
3
  * **throw a `Response`** (a 302 to your login path, or a 401); nifra returns a thrown Response as-is, so
4
4
  * the guard short-circuits the rest of the handler. Pairs with `@nifrajs/auth` sessions but only needs the
5
5
  * `Session` shape, so it's framework-agnostic.
6
6
  */
7
7
  import type { Session } from "./session.ts"
8
8
 
9
- /** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or — omitted — a
9
+ /** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or - omitted - a
10
10
  * 401 JSON (`{ ok: false, error: "unauthorized" }`). */
11
11
  export interface GuardOptions {
12
12
  readonly redirectTo?: string
@@ -18,7 +18,7 @@ const rejection = (options: GuardOptions): Response => {
18
18
  return Response.json({ ok: false, error: "unauthorized" }, { status: 401 })
19
19
  }
20
20
  // Same-origin guard (mirrors @nifrajs/web `redirect`): a single leading "/", never "//host" or an
21
- // absolute URL. `redirectTo` is dev-authored, so a bad value is a config bug — fail loud here.
21
+ // absolute URL. `redirectTo` is dev-authored, so a bad value is a config bug - fail loud here.
22
22
  if (!to.startsWith("/") || to.startsWith("//")) {
23
23
  throw new Error(
24
24
  `[nifra/auth] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`,
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/auth` — session primitives for nifra. Signed-cookie + server-store sessions, with the same
2
+ * `@nifrajs/auth` - session primitives for nifra. Signed-cookie + server-store sessions, with the same
3
3
  * pluggable-store + prod-guard discipline as the ISR cache. **Bring your own identity** (Better Auth,
4
4
  * Lucia, your own OAuth/password logic); nifra owns the session cookie, store, CSRF, and route guards.
5
5
  */
package/src/session.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  /**
2
- * The session manager — `createSessions<Data>()` returns get/commit/destroy/regenerate bound to a
2
+ * The session manager - `createSessions<Data>()` returns get/commit/destroy/regenerate bound to a
3
3
  * signing secret + cookie config. Two modes:
4
4
  * • **store mode** (a {@link SessionStore} is given): a random session id rides a signed cookie; the
5
5
  * data lives in the store. Big/sensitive data, server-side revocation.
6
6
  * • **cookie mode** (no store): the data itself is signed into the cookie (stateless, edge-friendly,
7
7
  * ≤4 KB). No server state.
8
8
  * The cookie value is **always HMAC-signed** (tamper-evident; verified constant-time) and the session
9
- * cookie is always `HttpOnly`. Reads **fail closed** — a tampered/missing/expired cookie yields a fresh
9
+ * cookie is always `HttpOnly`. Reads **fail closed** - a tampered/missing/expired cookie yields a fresh
10
10
  * anonymous session, never an error.
11
11
  */
12
12
  import { type CookieOptions, parseCookies, signValue, unsignValue } from "@nifrajs/core/cookies"
13
13
  import type { SessionRecord, SessionStore } from "./store.ts"
14
14
 
15
- /** The cookie + response surface the manager needs — a structural subset of nifra's `Context`, so any
15
+ /** The cookie + response surface the manager needs - a structural subset of nifra's `Context`, so any
16
16
  * `c` satisfies it and it's testable with a stub. */
17
17
  export interface SessionContext {
18
18
  readonly cookies: Readonly<Record<string, string>>
@@ -22,7 +22,7 @@ export interface SessionContext {
22
22
  }
23
23
  }
24
24
 
25
- /** A typed session handle. Every key is optional — a fresh session is empty. */
25
+ /** A typed session handle. Every key is optional - a fresh session is empty. */
26
26
  export interface Session<Data extends Record<string, unknown> = Record<string, unknown>> {
27
27
  get<K extends keyof Data>(key: K): Data[K] | undefined
28
28
  set<K extends keyof Data>(key: K, value: Data[K]): void
@@ -35,7 +35,7 @@ export interface Session<Data extends Record<string, unknown> = Record<string, u
35
35
  readonly isEmpty: boolean
36
36
  }
37
37
 
38
- /** Cookie attributes a session may tune. `httpOnly` is **not** offered — a session cookie is always
38
+ /** Cookie attributes a session may tune. `httpOnly` is **not** offered - a session cookie is always
39
39
  * HttpOnly; `maxAge`/`expires` are derived from the session's lifetime. */
40
40
  export type SessionCookieOptions = Pick<CookieOptions, "secure" | "sameSite" | "path" | "domain">
41
41
 
@@ -53,21 +53,21 @@ export interface SessionOptions {
53
53
  /** Extra cookie attributes (the session cookie is always HttpOnly; `Secure`/`SameSite=Lax`/`Path=/`
54
54
  * default via `c.set.cookie`). Pass `{ secure: false }` for local http dev. */
55
55
  readonly cookie?: SessionCookieOptions
56
- /** Clock (ms) — injected for testability; production passes `() => Date.now()` (the default). */
56
+ /** Clock (ms) - injected for testability; production passes `() => Date.now()` (the default). */
57
57
  readonly now?: () => number
58
58
  }
59
59
 
60
60
  export interface SessionManager<Data extends Record<string, unknown> = Record<string, unknown>> {
61
61
  /** Load the session from the request cookie (verified + un-expired), or a fresh anonymous one. */
62
62
  get(c: SessionContext): Promise<Session<Data>>
63
- /** Read-only load from a raw `Request` — for `@nifrajs/web` loaders (which can read the request but not
63
+ /** Read-only load from a raw `Request` - for `@nifrajs/web` loaders (which can read the request but not
64
64
  * write cookies). Identical verify/expiry to {@link get}; commit/destroy in a route or action. */
65
65
  read(request: Request): Promise<Session<Data>>
66
66
  /** Persist the session: write the store (store mode) + the signed cookie. */
67
67
  commit(c: SessionContext, session: Session<Data>): Promise<void>
68
68
  /** End the session: drop the store record (store mode) + clear the cookie. */
69
69
  destroy(c: SessionContext, session?: Session<Data>): Promise<void>
70
- /** Rotate the session id on the next commit (call on privilege change — login — to defend against
70
+ /** Rotate the session id on the next commit (call on privilege change - login - to defend against
71
71
  * session fixation). Data is preserved; the old store record is dropped. */
72
72
  regenerate(session: Session<Data>): void
73
73
  }
@@ -85,7 +85,7 @@ const encodeBase64Url = (bytes: Uint8Array): string => {
85
85
  return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
86
86
  }
87
87
 
88
- /** A 256-bit random session id (base64url). High-entropy + unguessable — the cookie also signs it. */
88
+ /** A 256-bit random session id (base64url). High-entropy + unguessable - the cookie also signs it. */
89
89
  const randomId = (): string => encodeBase64Url(crypto.getRandomValues(new Uint8Array(32)))
90
90
 
91
91
  interface Payload {
@@ -93,7 +93,7 @@ interface Payload {
93
93
  expiresAt: number
94
94
  }
95
95
 
96
- /** Validate a cookie-mode payload at the trust boundary — malformed JSON / shape → `null` (no session). */
96
+ /** Validate a cookie-mode payload at the trust boundary - malformed JSON / shape → `null` (no session). */
97
97
  const parsePayload = (json: string): Payload | null => {
98
98
  let value: unknown
99
99
  try {
@@ -121,7 +121,7 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
121
121
  const maxAge = options.maxAge ?? 7 * DAY_SECONDS
122
122
  const rolling = options.rolling ?? true
123
123
  const now = options.now ?? (() => Date.now())
124
- // Annotated (not just `?? {}`) so `cookieOpts.path`/`.domain` stay typed — `?? {}` alone widens the
124
+ // Annotated (not just `?? {}`) so `cookieOpts.path`/`.domain` stay typed - `?? {}` alone widens the
125
125
  // type to include the empty-object literal, which the package's build tsconfig rejects on access.
126
126
  const cookieOpts: SessionCookieOptions = options.cookie ?? {}
127
127
 
@@ -187,7 +187,7 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
187
187
  const record = await store.get(unsigned)
188
188
  if (record === undefined) return make({})
189
189
  if (record.expiresAt <= now()) {
190
- await store.delete(unsigned) // expired — evict + treat as no session
190
+ await store.delete(unsigned) // expired - evict + treat as no session
191
191
  return make({})
192
192
  }
193
193
  return make({ id: unsigned, data: record.data, expiresAt: record.expiresAt })
@@ -200,7 +200,7 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
200
200
 
201
201
  const get = (c: SessionContext): Promise<Session<Data>> => loadFromRaw(c.cookies[cookieName])
202
202
 
203
- // Read-only load from a raw Request — for @nifrajs/web loaders, which have the request but can't write
203
+ // Read-only load from a raw Request - for @nifrajs/web loaders, which have the request but can't write
204
204
  // cookies (commit/destroy belong in a route/action with the full Context). Same verify + expiry.
205
205
  const read = (request: Request): Promise<Session<Data>> =>
206
206
  loadFromRaw(parseCookies(request.headers.get("cookie"))[cookieName])
package/src/store.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Session stores (the **store mode** backend) — a pluggable `SessionStore` plus an in-memory
2
+ * Session stores (the **store mode** backend) - a pluggable `SessionStore` plus an in-memory
3
3
  * implementation (dev / single-instance, prod-guarded) and a Cloudflare Workers KV implementation
4
4
  * (the durable, shared production path). The store is dumb persistence: the session *manager* owns the
5
5
  * clock and authoritatively enforces expiry, so the store stays clock-free and trivially testable.
6
6
  */
7
7
 
8
- /** A persisted session — its data plus an absolute expiry (ms epoch). */
8
+ /** A persisted session - its data plus an absolute expiry (ms epoch). */
9
9
  export interface SessionRecord {
10
10
  readonly data: Record<string, unknown>
11
11
  /** Absolute expiry (ms epoch). The manager treats `expiresAt <= now` as no session. */
@@ -14,7 +14,7 @@ export interface SessionRecord {
14
14
 
15
15
  /**
16
16
  * Pluggable session backend (store mode). Async so a network store (KV/Redis) fits the same shape.
17
- * **Production needs a shared/durable store** so sessions hold across instances — {@link MemorySessionStore}
17
+ * **Production needs a shared/durable store** so sessions hold across instances - {@link MemorySessionStore}
18
18
  * prod-guards against the per-instance footgun.
19
19
  */
20
20
  export interface SessionStore {
@@ -27,7 +27,7 @@ export interface SessionStore {
27
27
  }
28
28
 
29
29
  export interface MemorySessionStoreOptions {
30
- /** Allow the in-memory store in production. Off by default — per-instance sessions don't hold across
30
+ /** Allow the in-memory store in production. Off by default - per-instance sessions don't hold across
31
31
  * instances and are lost on restart. */
32
32
  readonly allowInProduction?: boolean
33
33
  /** Hard cap on stored sessions; the oldest-inserted is evicted past it (default 10_000). */
@@ -36,7 +36,7 @@ export interface MemorySessionStoreOptions {
36
36
 
37
37
  /**
38
38
  * In-process session store. Refuses to run in production unless explicitly allowed (mirrors the ISR
39
- * `MemoryCacheStore` + the rate-limit `MemoryStore` — a per-instance store is unsafe across instances).
39
+ * `MemoryCacheStore` + the rate-limit `MemoryStore` - a per-instance store is unsafe across instances).
40
40
  * Bounded: oldest-inserted entries evict past `max`.
41
41
  */
42
42
  export class MemorySessionStore implements SessionStore {
@@ -77,7 +77,7 @@ export class MemorySessionStore implements SessionStore {
77
77
  }
78
78
 
79
79
  /**
80
- * Minimal structural shape of a Cloudflare Workers **KV namespace** binding — just the three methods
80
+ * Minimal structural shape of a Cloudflare Workers **KV namespace** binding - just the three methods
81
81
  * {@link KVSessionStore} uses. Structural (no `@cloudflare/workers-types` dependency) so any KV-like
82
82
  * binding satisfies it and tests can pass an in-memory double.
83
83
  */
@@ -104,9 +104,9 @@ const isSessionRecord = (value: unknown): value is SessionRecord => {
104
104
 
105
105
  /**
106
106
  * A {@link SessionStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike})
107
- * — the durable, shared production store: sessions hold across worker instances and survive restarts.
107
+ * - the durable, shared production store: sessions hold across worker instances and survive restarts.
108
108
  * Records serialize to JSON; the entry's KV `expiration` is set from the record's `expiresAt` so KV
109
- * auto-evicts the session (a GC backstop — the manager still authoritatively checks expiry on read).
109
+ * auto-evicts the session (a GC backstop - the manager still authoritatively checks expiry on read).
110
110
  * Every read is validated, so a corrupt/skewed entry reads as no session.
111
111
  */
112
112
  export class KVSessionStore implements SessionStore {