@nifrajs/auth 2.14.1 → 3.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/auth",
3
- "version": "2.14.1",
3
+ "version": "3.1.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.1.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
  */