@nifrajs/auth 2.14.1 → 3.2.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.
@@ -1,7 +1,4 @@
1
- /**
2
- * Public authorization seam. Nifra supplies the control-flow contract; the application or data layer
3
- * supplies the actual policy. No subject/resource data is persisted or interpreted here.
4
- */
1
+ /** What a policy is asked to decide: this subject, doing this action, optionally to this resource. */
5
2
  export interface AuthorizationRequest<Subject = unknown, Resource = unknown> {
6
3
  readonly subject: Subject;
7
4
  readonly action: string;
@@ -10,6 +7,10 @@ export interface AuthorizationRequest<Subject = unknown, Resource = unknown> {
10
7
  export type Authorizer<Subject = unknown, Resource = unknown> = (request: AuthorizationRequest<Subject, Resource>) => boolean | Promise<boolean>;
11
8
  /** Evaluate a policy, failing closed for any non-true result. */
12
9
  export declare function isAuthorized<Subject, Resource>(authorizer: Authorizer<Subject, Resource>, request: AuthorizationRequest<Subject, Resource>): Promise<boolean>;
13
- /** Require an application/data-layer policy to allow an action; denied requests throw a 403 Response. */
10
+ /** Require an application/data-layer policy to allow an action. A denied request throws a plain
11
+ * `status(403)` render - the same control-flow signal a guard throws, on the same rendering lane as a
12
+ * returned one, so the denial never builds a `Response`. Like the guards, it throws because it is
13
+ * called for effect from inside the work it protects; where the caller can return, return
14
+ * `status(403, ...)` instead. */
14
15
  export declare function requireAuthorization<Subject, Resource>(authorizer: Authorizer<Subject, Resource>, request: AuthorizationRequest<Subject, Resource>): Promise<void>;
15
16
  //# sourceMappingURL=authorization.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["../src/authorization.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,oBAAoB,CAAC,OAAO,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAC7B;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO,IAAI,CAC9D,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,KAC7C,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAE/B,iEAAiE;AACjE,wBAAsB,YAAY,CAAC,OAAO,EAAE,QAAQ,EAClD,UAAU,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,EACzC,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,GAC/C,OAAO,CAAC,OAAO,CAAC,CAElB;AAED,yGAAyG;AACzG,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAC1D,UAAU,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,EACzC,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,GAC/C,OAAO,CAAC,IAAI,CAAC,CAGf"}
1
+ {"version":3,"file":"authorization.d.ts","sourceRoot":"","sources":["../src/authorization.ts"],"names":[],"mappings":"AAMA,sGAAsG;AACtG,MAAM,WAAW,oBAAoB,CAAC,OAAO,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAC7B;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO,IAAI,CAC9D,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,KAC7C,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;AAE/B,iEAAiE;AACjE,wBAAsB,YAAY,CAAC,OAAO,EAAE,QAAQ,EAClD,UAAU,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,EACzC,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,GAC/C,OAAO,CAAC,OAAO,CAAC,CAElB;AAED;;;;iCAIiC;AACjC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,QAAQ,EAC1D,UAAU,EAAE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,EACzC,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,QAAQ,CAAC,GAC/C,OAAO,CAAC,IAAI,CAAC,CAGf"}
@@ -1,11 +1,20 @@
1
+ /**
2
+ * Public authorization seam. Nifra supplies the control-flow contract; the application or data layer
3
+ * supplies the actual policy. No subject/resource data is persisted or interpreted here.
4
+ */
5
+ import { status } from "@nifrajs/core/server";
1
6
  /** Evaluate a policy, failing closed for any non-true result. */
2
7
  export async function isAuthorized(authorizer, request) {
3
8
  return (await authorizer(request)) === true;
4
9
  }
5
- /** Require an application/data-layer policy to allow an action; denied requests throw a 403 Response. */
10
+ /** Require an application/data-layer policy to allow an action. A denied request throws a plain
11
+ * `status(403)` render - the same control-flow signal a guard throws, on the same rendering lane as a
12
+ * returned one, so the denial never builds a `Response`. Like the guards, it throws because it is
13
+ * called for effect from inside the work it protects; where the caller can return, return
14
+ * `status(403, ...)` instead. */
6
15
  export async function requireAuthorization(authorizer, request) {
7
16
  if (await isAuthorized(authorizer, request))
8
17
  return;
9
- throw Response.json({ ok: false, error: "forbidden" }, { status: 403 });
18
+ throw status(403, { ok: false, error: "forbidden" });
10
19
  }
11
20
  //# sourceMappingURL=authorization.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"authorization.js","sourceRoot":"","sources":["../src/authorization.ts"],"names":[],"mappings":"AAcA,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,UAAyC,EACzC,OAAgD;IAEhD,OAAO,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,CAAA;AAC7C,CAAC;AAED,yGAAyG;AACzG,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,UAAyC,EACzC,OAAgD;IAEhD,IAAI,MAAM,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC;QAAE,OAAM;IACnD,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;AACzE,CAAC"}
1
+ {"version":3,"file":"authorization.js","sourceRoot":"","sources":["../src/authorization.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAa7C,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,UAAyC,EACzC,OAAgD;IAEhD,OAAO,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,CAAA;AAC7C,CAAC;AAED;;;;iCAIiC;AACjC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,UAAyC,EACzC,OAAgD;IAEhD,IAAI,MAAM,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC;QAAE,OAAM;IACnD,MAAM,MAAM,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAA;AACtD,CAAC"}
package/dist/guards.d.ts CHANGED
@@ -1,9 +1,3 @@
1
- /**
2
- * Route guards - run at the top of a protected loader/action/handler. On a missing session they
3
- * **throw a `Response`** (a 302 to your login path, or a 401); nifra returns a thrown Response as-is, so
4
- * the guard short-circuits the rest of the handler. Pairs with `@nifrajs/auth` sessions but only needs the
5
- * `Session` shape, so it's framework-agnostic.
6
- */
7
1
  import type { Session } from "./session.js";
8
2
  /** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or - omitted - a
9
3
  * 401 JSON (`{ ok: false, error: "unauthorized" }`). */
@@ -11,7 +5,8 @@ export interface GuardOptions {
11
5
  readonly redirectTo?: string;
12
6
  }
13
7
  /**
14
- * Require a non-empty session. Returns it when present; otherwise throws a `Response` (302/401). Use at
8
+ * Require a non-empty session. Returns it when present; otherwise throws a `status(...)` render
9
+ * (302/401) - control flow, not a `Response`; see the module docs above. Use at
15
10
  * the top of a protected loader:
16
11
  * `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
17
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;wDACwD;AACxD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAiBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACtB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,EACpF,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACtB,GAAG,EAAE,CAAC,EACN,OAAO,GAAE,YAAiB,GACzB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAKtB"}
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C;wDACwD;AACxD,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B;AAgBD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACtB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,IAAI,EACpF,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACtB,GAAG,EAAE,CAAC,EACN,OAAO,GAAE,YAAiB,GACzB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAKtB"}
package/dist/guards.js CHANGED
@@ -1,17 +1,33 @@
1
+ /**
2
+ * Route guards - run at the top of a protected loader/action/handler. On a missing session they
3
+ * **throw** a `status(...)` render (a 302 to your login path, or a 401); nifra treats a thrown
4
+ * `status(...)` as control flow and renders it on the ordinary plain-data lane, so the guard
5
+ * short-circuits the rest of the handler at the cost of a normal return, without a `Response` being
6
+ * built or drained.
7
+ *
8
+ * The throw is the point of a guard: `requireSession(...)` is called for effect from inside the
9
+ * handler, so its caller cannot return the rejection on its behalf - only unwinding gets out of a
10
+ * half-finished handler. Everywhere the caller CAN return - a `beforeHandle`, a `derive`, the handler
11
+ * itself - return `status(...)` instead; see its docs.
12
+ *
13
+ * Pairs with `@nifrajs/auth` sessions but only needs the `Session` shape, so it's framework-agnostic.
14
+ */
15
+ import { isSameOriginPath, status } from "@nifrajs/core/server";
1
16
  const rejection = (options) => {
2
17
  const to = options.redirectTo;
3
- if (to === undefined) {
4
- return Response.json({ ok: false, error: "unauthorized" }, { status: 401 });
5
- }
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.
8
- if (!to.startsWith("/") || to.startsWith("//")) {
9
- throw new Error(`[nifra/auth] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`);
18
+ if (to === undefined)
19
+ return status(401, { ok: false, error: "unauthorized" });
20
+ // The kernel's same-origin predicate, the one `@nifrajs/web`'s `redirect` uses: a single leading
21
+ // "/", never "//host", an absolute URL, or a form a URL parser resolves off-origin. `redirectTo`
22
+ // is dev-authored, so a bad value is a config bug - fail loud here.
23
+ if (!isSameOriginPath(to)) {
24
+ throw new Error(`[nifra/auth] guard redirectTo must be a same-origin path beginning with "/" - never "//", a backslash, or a control character (got ${JSON.stringify(to)})`);
10
25
  }
11
- return new Response(null, { status: 302, headers: { location: to } });
26
+ return status(302, undefined, { headers: { location: to } });
12
27
  };
13
28
  /**
14
- * Require a non-empty session. Returns it when present; otherwise throws a `Response` (302/401). Use at
29
+ * Require a non-empty session. Returns it when present; otherwise throws a `status(...)` render
30
+ * (302/401) - control flow, not a `Response`; see the module docs above. Use at
15
31
  * the top of a protected loader:
16
32
  * `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
17
33
  */
@@ -1 +1 @@
1
- {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAcA,MAAM,SAAS,GAAG,CAAC,OAAqB,EAAY,EAAE;IACpD,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC7B,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;IAC7E,CAAC;IACD,kGAAkG;IAClG,+FAA+F;IAC/F,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,oFAAoF,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAC1G,CAAA;IACH,CAAC;IACD,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AACvE,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAsB,EACtB,UAAwB,EAAE;IAE1B,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO,OAAO,CAAA;IACpC,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,OAAsB,EACtB,GAAM,EACN,UAAwB,EAAE;IAE1B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9B,uGAAuG;IACvG,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAA6B,CAAA;IAC/E,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC"}
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,gBAAgB,EAAuB,MAAM,EAAE,MAAM,sBAAsB,CAAA;AASpF,MAAM,SAAS,GAAG,CAAC,OAAqB,EAAkB,EAAE;IAC1D,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC7B,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAA;IAC9E,iGAAiG;IACjG,iGAAiG;IACjG,oEAAoE;IACpE,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CACb,sIAAsI,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAC5J,CAAA;IACH,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;AAC9D,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAsB,EACtB,UAAwB,EAAE;IAE1B,IAAI,CAAC,OAAO,CAAC,OAAO;QAAE,OAAO,OAAO,CAAA;IACpC,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,OAAsB,EACtB,GAAM,EACN,UAAwB,EAAE;IAE1B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC9B,uGAAuG;IACvG,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAA6B,CAAA;IAC/E,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;AAC1B,CAAC"}
package/dist/store.d.ts CHANGED
@@ -65,7 +65,10 @@ export interface KVNamespaceLike {
65
65
  */
66
66
  export declare class KVSessionStore implements SessionStore {
67
67
  private readonly kv;
68
- constructor(kv: KVNamespaceLike);
68
+ private readonly now;
69
+ constructor(kv: KVNamespaceLike, options?: {
70
+ readonly now?: () => number;
71
+ });
69
72
  get(id: string): Promise<SessionRecord | undefined>;
70
73
  set(id: string, record: SessionRecord): Promise<void>;
71
74
  delete(id: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAA;IACnD,gDAAgD;IAChD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,4DAA4D;IAC5D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC;4CACwC;IACxC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IACpC,4FAA4F;IAC5F,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAEhB,OAAO,GAAE,yBAA8B;IAenD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAInD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAIlC;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACxC,8FAA8F;IAC9F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1F,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAgBD;;;;;;GAMG;AACH,qBAAa,cAAe,YAAW,YAAY;IACjD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;gBAExB,EAAE,EAAE,eAAe;IAIzB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAYnD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IASrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAA;IACnD,gDAAgD;IAChD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,4DAA4D;IAC5D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC;4CACwC;IACxC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IACpC,4FAA4F;IAC5F,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAC5D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAEhB,OAAO,GAAE,yBAA8B;IAenD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAInD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAIlC;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACxC,8FAA8F;IAC9F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1F,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAgBD;;;;;;GAMG;AACH,qBAAa,cAAe,YAAW,YAAY;IACjD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;gBAEtB,EAAE,EAAE,eAAe,EAAE,OAAO,GAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAO;IAKxE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAYnD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC"}
package/dist/store.js CHANGED
@@ -64,8 +64,10 @@ const isSessionRecord = (value) => {
64
64
  */
65
65
  export class KVSessionStore {
66
66
  kv;
67
- constructor(kv) {
67
+ now;
68
+ constructor(kv, options = {}) {
68
69
  this.kv = kv;
70
+ this.now = options.now ?? (() => Date.now());
69
71
  }
70
72
  async get(id) {
71
73
  const raw = await this.kv.get(id);
@@ -81,11 +83,13 @@ export class KVSessionStore {
81
83
  return isSessionRecord(parsed) ? parsed : undefined;
82
84
  }
83
85
  async set(id, record) {
84
- // Absolute KV expiry (unix seconds) as a GC backstop. Cloudflare requires it be ≥ 60s out; session
85
- // `maxAge` is realistically minutes+, so a real deploy satisfies that (the manager enforces the
86
- // exact expiry regardless).
86
+ // Cloudflare requires absolute expiration to be at least 60 seconds in the future. Clamp only the
87
+ // KV garbage-collection backstop; the session manager still rejects at the exact `expiresAt`, so a
88
+ // one-second session cannot remain valid for the platform's extra retention window.
89
+ const platformFloor = Math.ceil(this.now() / 1000) + 60;
90
+ const expiration = Math.max(Math.floor(record.expiresAt / 1000), platformFloor);
87
91
  await this.kv.put(id, JSON.stringify(record), {
88
- expiration: Math.floor(record.expiresAt / 1000),
92
+ expiration,
89
93
  });
90
94
  }
91
95
  async delete(id) {
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/auth",
3
- "version": "2.14.1",
3
+ "version": "3.2.0",
4
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",
@@ -37,7 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "peerDependencies": {
40
- "@nifrajs/core": "^2.14.1"
40
+ "@nifrajs/core": "^3.2.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@nifrajs/core": "workspace:*"
@@ -2,6 +2,9 @@
2
2
  * Public authorization seam. Nifra supplies the control-flow contract; the application or data layer
3
3
  * supplies the actual policy. No subject/resource data is persisted or interpreted here.
4
4
  */
5
+ import { status } from "@nifrajs/core/server"
6
+
7
+ /** What a policy is asked to decide: this subject, doing this action, optionally to this resource. */
5
8
  export interface AuthorizationRequest<Subject = unknown, Resource = unknown> {
6
9
  readonly subject: Subject
7
10
  readonly action: string
@@ -20,11 +23,15 @@ export async function isAuthorized<Subject, Resource>(
20
23
  return (await authorizer(request)) === true
21
24
  }
22
25
 
23
- /** Require an application/data-layer policy to allow an action; denied requests throw a 403 Response. */
26
+ /** Require an application/data-layer policy to allow an action. A denied request throws a plain
27
+ * `status(403)` render - the same control-flow signal a guard throws, on the same rendering lane as a
28
+ * returned one, so the denial never builds a `Response`. Like the guards, it throws because it is
29
+ * called for effect from inside the work it protects; where the caller can return, return
30
+ * `status(403, ...)` instead. */
24
31
  export async function requireAuthorization<Subject, Resource>(
25
32
  authorizer: Authorizer<Subject, Resource>,
26
33
  request: AuthorizationRequest<Subject, Resource>,
27
34
  ): Promise<void> {
28
35
  if (await isAuthorized(authorizer, request)) return
29
- throw Response.json({ ok: false, error: "forbidden" }, { status: 403 })
36
+ throw status(403, { ok: false, error: "forbidden" })
30
37
  }
package/src/guards.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  /**
2
2
  * Route guards - run at the top of a protected loader/action/handler. On a missing session they
3
- * **throw a `Response`** (a 302 to your login path, or a 401); nifra returns a thrown Response as-is, so
4
- * the guard short-circuits the rest of the handler. Pairs with `@nifrajs/auth` sessions but only needs the
5
- * `Session` shape, so it's framework-agnostic.
3
+ * **throw** a `status(...)` render (a 302 to your login path, or a 401); nifra treats a thrown
4
+ * `status(...)` as control flow and renders it on the ordinary plain-data lane, so the guard
5
+ * short-circuits the rest of the handler at the cost of a normal return, without a `Response` being
6
+ * built or drained.
7
+ *
8
+ * The throw is the point of a guard: `requireSession(...)` is called for effect from inside the
9
+ * handler, so its caller cannot return the rejection on its behalf - only unwinding gets out of a
10
+ * half-finished handler. Everywhere the caller CAN return - a `beforeHandle`, a `derive`, the handler
11
+ * itself - return `status(...)` instead; see its docs.
12
+ *
13
+ * Pairs with `@nifrajs/auth` sessions but only needs the `Session` shape, so it's framework-agnostic.
6
14
  */
15
+ import { isSameOriginPath, type ResponseResult, status } from "@nifrajs/core/server"
7
16
  import type { Session } from "./session.ts"
8
17
 
9
18
  /** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or - omitted - a
@@ -12,23 +21,23 @@ export interface GuardOptions {
12
21
  readonly redirectTo?: string
13
22
  }
14
23
 
15
- const rejection = (options: GuardOptions): Response => {
24
+ const rejection = (options: GuardOptions): ResponseResult => {
16
25
  const to = options.redirectTo
17
- if (to === undefined) {
18
- return Response.json({ ok: false, error: "unauthorized" }, { status: 401 })
19
- }
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.
22
- if (!to.startsWith("/") || to.startsWith("//")) {
26
+ if (to === undefined) return status(401, { ok: false, error: "unauthorized" })
27
+ // The kernel's same-origin predicate, the one `@nifrajs/web`'s `redirect` uses: a single leading
28
+ // "/", never "//host", an absolute URL, or a form a URL parser resolves off-origin. `redirectTo`
29
+ // is dev-authored, so a bad value is a config bug - fail loud here.
30
+ if (!isSameOriginPath(to)) {
23
31
  throw new Error(
24
- `[nifra/auth] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`,
32
+ `[nifra/auth] guard redirectTo must be a same-origin path beginning with "/" - never "//", a backslash, or a control character (got ${JSON.stringify(to)})`,
25
33
  )
26
34
  }
27
- return new Response(null, { status: 302, headers: { location: to } })
35
+ return status(302, undefined, { headers: { location: to } })
28
36
  }
29
37
 
30
38
  /**
31
- * Require a non-empty session. Returns it when present; otherwise throws a `Response` (302/401). Use at
39
+ * Require a non-empty session. Returns it when present; otherwise throws a `status(...)` render
40
+ * (302/401) - control flow, not a `Response`; see the module docs above. Use at
32
41
  * the top of a protected loader:
33
42
  * `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
34
43
  */
package/src/store.ts CHANGED
@@ -114,9 +114,11 @@ const isSessionRecord = (value: unknown): value is SessionRecord => {
114
114
  */
115
115
  export class KVSessionStore implements SessionStore {
116
116
  private readonly kv: KVNamespaceLike
117
+ private readonly now: () => number
117
118
 
118
- constructor(kv: KVNamespaceLike) {
119
+ constructor(kv: KVNamespaceLike, options: { readonly now?: () => number } = {}) {
119
120
  this.kv = kv
121
+ this.now = options.now ?? (() => Date.now())
120
122
  }
121
123
 
122
124
  async get(id: string): Promise<SessionRecord | undefined> {
@@ -132,11 +134,13 @@ export class KVSessionStore implements SessionStore {
132
134
  }
133
135
 
134
136
  async set(id: string, record: SessionRecord): Promise<void> {
135
- // Absolute KV expiry (unix seconds) as a GC backstop. Cloudflare requires it be ≥ 60s out; session
136
- // `maxAge` is realistically minutes+, so a real deploy satisfies that (the manager enforces the
137
- // exact expiry regardless).
137
+ // Cloudflare requires absolute expiration to be at least 60 seconds in the future. Clamp only the
138
+ // KV garbage-collection backstop; the session manager still rejects at the exact `expiresAt`, so a
139
+ // one-second session cannot remain valid for the platform's extra retention window.
140
+ const platformFloor = Math.ceil(this.now() / 1000) + 60
141
+ const expiration = Math.max(Math.floor(record.expiresAt / 1000), platformFloor)
138
142
  await this.kv.put(id, JSON.stringify(record), {
139
- expiration: Math.floor(record.expiresAt / 1000),
143
+ expiration,
140
144
  })
141
145
  }
142
146