@nifrajs/auth 5.5.1 → 5.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @nifrajs/auth
2
2
 
3
+ ## 5.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b141cf4: `csrf()` takes `exempt`: route patterns whose requests skip the Origin check, for a route no browser
8
+ calls that verifies its sender another way, such as a signed payment webhook arriving with no
9
+ `Origin`. Patterns are written as routes are (`/inbound/:vendor`) and matched against the path the
10
+ server routes, so any other path stays checked.
11
+
12
+ ## 5.6.0
13
+
14
+ ### Patch Changes
15
+
16
+ - 2eabd83: `etag()` keeps an `ETag` the handler already set and answers `If-None-Match` against it, instead of replacing an app-managed version with a content hash. `etag()`, `conditionalResponse()` and `rangeResponse()` read an `If-None-Match` list with a comma inside a quoted ETag as one tag.
17
+
18
+ `env.number()` reads plain decimal text only (`0x10`, `1e3` and `Infinity` are refused), and `env.number()` and `env.port()` treat a value of only spaces as unset, so a `default` applies. `env.boolean({ default })` returns the default for a variable that is set but empty.
19
+
20
+ `serverFunctions()` mounts a second module under a namespace already in use, so `admin/todos.fn.ts` and `user/todos.fn.ts` are both served; an export name both modules use fails at boot as a duplicate route. Before, the second module was skipped and its functions answered 404. A module no app uses any more is not kept in memory by its namespace. The client build fails on a `*.fn.ts` file whose name `serverFunctions()` would refuse as a namespace, naming the file.
21
+
22
+ `requireSession()` documents that it passes any non-empty session, which a visitor can have before signing in; `requireUser()` is the signed-in guard.
23
+
3
24
  ## 5.5.1
4
25
 
5
26
  No changes in this release.
package/dist/csrf.d.ts CHANGED
@@ -1,9 +1,3 @@
1
- /**
2
- * CSRF protection via an **Origin / Referer check** on state-changing requests - OWASP's recommended
3
- * defense for cookie-authenticated apps. A browser always attaches `Origin` (or at least `Referer`) to
4
- * a cross-origin or same-origin *unsafe* request; it must match an allowed origin, else `403`. Safe
5
- * methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
6
- */
7
1
  import { type Middleware } from "@nifrajs/core/server";
8
2
  export interface CsrfOptions {
9
3
  /**
@@ -12,6 +6,14 @@ export interface CsrfOptions {
12
6
  * works). Set it when the proxy rewrites `Host`, so the public origin differs from the worker's.
13
7
  */
14
8
  readonly origins?: readonly string[];
9
+ /**
10
+ * Route patterns whose requests skip the check, for a route no browser calls that proves its sender
11
+ * another way - a signed payment webhook arrives with no `Origin` at all. Written as routes are
12
+ * (`/inbound/:vendor`) and matched against the path this server routes, so a different path to
13
+ * the same handler is still checked. Exempt nothing that acts on a session cookie: a cross-site page
14
+ * can post to it.
15
+ */
16
+ readonly exempt?: readonly string[];
15
17
  }
16
18
  export declare function csrf(options?: CsrfOptions): Middleware;
17
19
  //# sourceMappingURL=csrf.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAuB,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAQ3E,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACrC;AAKD,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAmC1D"}
1
+ {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AASA,OAAO,EAAuB,KAAK,UAAU,EAAc,MAAM,sBAAsB,CAAA;AAQvF,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAcD,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CAqC1D"}
package/dist/csrf.js CHANGED
@@ -2,22 +2,38 @@
2
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
- * methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
5
+ * methods (GET/HEAD/OPTIONS) pass, as do routes listed in `exempt`. Apply with
6
+ * `app.use(csrf({ origins: ["https://example.com"] }))`.
6
7
  */
7
- import { isSameOriginRequest } from "@nifrajs/core/server";
8
+ import { expandOptionalParams } from "@nifrajs/core/pattern";
9
+ import { Router } from "@nifrajs/core/router";
10
+ import { isSameOriginRequest, pathnameOf } from "@nifrajs/core/server";
8
11
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
9
12
  // A name per instance: `use()` skips a name it already applied, which would drop a second, stricter
10
13
  // `csrf()` inside a `group()`.
11
14
  let instances = 0;
12
15
  const forbidden = () => Response.json({ ok: false, error: "csrf_failed" }, { status: 403 });
16
+ function exemptionOf(patterns) {
17
+ if (patterns.length === 0)
18
+ return undefined;
19
+ const forms = new Set(patterns.flatMap((pattern) => expandOptionalParams(pattern)));
20
+ const router = new Router();
21
+ // An exemption names a path whatever the method, so every form sits under one method key.
22
+ for (const form of forms)
23
+ router.add("POST", form, true);
24
+ return (path) => router.find("POST", path).found;
25
+ }
13
26
  export function csrf(options = {}) {
14
27
  const configured = options.origins !== undefined ? new Set(options.origins) : undefined;
28
+ const exempt = exemptionOf(options.exempt ?? []);
15
29
  instances += 1;
16
30
  return {
17
31
  name: `csrf#${instances}`,
18
32
  onRequest(req) {
19
33
  if (SAFE_METHODS.has(req.method))
20
34
  return undefined;
35
+ if (exempt?.(pathnameOf(req.url)) === true)
36
+ return undefined;
21
37
  // The same-origin default uses core's check, which accepts an `https:` page reaching an `http:`
22
38
  // request URL on the same host - what every TLS-terminating proxy looks like from here.
23
39
  const allowed = (candidate) => configured !== undefined ? configured.has(candidate) : isSameOriginRequest(candidate, req);
package/dist/csrf.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAmB,MAAM,sBAAsB,CAAA;AAE3E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;AAExD,oGAAoG;AACpG,+BAA+B;AAC/B,IAAI,SAAS,GAAG,CAAC,CAAA;AAWjB,MAAM,SAAS,GAAG,GAAa,EAAE,CAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;AAErE,MAAM,UAAU,IAAI,CAAC,UAAuB,EAAE;IAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACvF,SAAS,IAAI,CAAC,CAAA;IACd,OAAO;QACL,IAAI,EAAE,QAAQ,SAAS,EAAE;QACzB,SAAS,CAAC,GAAG;YACX,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,SAAS,CAAA;YAClD,gGAAgG;YAChG,wFAAwF;YACxF,MAAM,OAAO,GAAG,CAAC,SAAiB,EAAW,EAAE,CAC7C,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;YAE5F,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YACxC,oFAAoF;YACpF,+FAA+F;YAC/F,IAAI,MAAM,KAAK,MAAM,IAAI,UAAU,KAAK,SAAS;gBAC/C,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;YACtF,IAAI,MAAM,KAAK,IAAI;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;YAErE,iFAAiF;YACjF,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC1C,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,IAAI,aAAqB,CAAA;gBACzB,IAAI,CAAC;oBACH,aAAa,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAA;gBACzC,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,SAAS,EAAE,CAAA,CAAC,oBAAoB;gBACzC,CAAC;gBACD,OAAO,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;YACzD,CAAC;YAED,mGAAmG;YACnG,OAAO,SAAS,EAAE,CAAA;QACpB,CAAC;KACF,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"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"}
package/dist/guards.d.ts CHANGED
@@ -19,6 +19,9 @@ export interface GuardOptions {
19
19
  * (302/401) - control flow, not a `Response`; see the module docs above. Use at
20
20
  * the top of a protected loader:
21
21
  * `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
22
+ *
23
+ * Non-empty is not signed in: a session that holds anything (a cart, a return path, CSRF state)
24
+ * passes. Guard a page for signed-in visitors with {@link requireUser} and the key a login sets.
22
25
  */
23
26
  export declare function requireSession<Data extends Record<string, unknown>>(session: Session<Data>, options?: GuardOptions): Session<Data>;
24
27
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,8FAA8F;AAC9F,MAAM,WAAW,uBAAuB;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,oGAAoG;IACpG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACpD;AAED;iGACiG;AACjG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE,uBAAuB,CAAA;CACnD;AA4CD;;;;;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,CAIf;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,CAMtB"}
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAE3C,8FAA8F;AAC9F,MAAM,WAAW,uBAAuB;IACtC,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,oGAAoG;IACpG,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACpD;AAED;iGACiG;AACjG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,eAAe,CAAC,EAAE,uBAAuB,CAAA;CACnD;AA4CD;;;;;;;;GAQG;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,CAIf;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,CAMtB"}
package/dist/guards.js CHANGED
@@ -52,6 +52,9 @@ const rejection = (options) => {
52
52
  * (302/401) - control flow, not a `Response`; see the module docs above. Use at
53
53
  * the top of a protected loader:
54
54
  * `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
55
+ *
56
+ * Non-empty is not signed in: a session that holds anything (a cart, a return path, CSRF state)
57
+ * passes. Guard a page for signed-in visitors with {@link requireUser} and the key a login sets.
55
58
  */
56
59
  export function requireSession(session, options = {}) {
57
60
  assertUnauthenticated(options);
@@ -1 +1 @@
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;AAoBpF,MAAM,YAAY,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAW,CAAA;AAElE,uGAAuG;AACvG,SAAS,qBAAqB,CAAC,OAG9B;IACC,MAAM,MAAM,GAAG,OAAO,CAAC,eAAe,CAAA;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAM;IAChC,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAA;IAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAChF,MAAM,IAAI,UAAU,CAClB,6EAA6E,MAAM,CAAC,IAAI,CAAC,GAAG,CAC7F,CAAA;IACH,CAAC;AACH,CAAC;AAED,MAAM,qBAAqB,GAAG,CAAC,MAA2C,EAAkB,EAAE;IAC5F,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAA;IAC1D,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS;QACjC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,CAAC;QACpC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;AACrE,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAAC,OAAqB,EAAkB,EAAE;IAC1D,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC7B,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,qBAAqB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;IAC3E,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,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9B,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,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9B,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;AAoBpF,MAAM,YAAY,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAW,CAAA;AAElE,uGAAuG;AACvG,SAAS,qBAAqB,CAAC,OAG9B;IACC,MAAM,MAAM,GAAG,OAAO,CAAC,eAAe,CAAA;IACtC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAM;IAChC,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAA;IAC1B,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,GAAG,IAAI,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAChF,MAAM,IAAI,UAAU,CAClB,6EAA6E,MAAM,CAAC,IAAI,CAAC,GAAG,CAC7F,CAAA;IACH,CAAC;AACH,CAAC;AAED,MAAM,qBAAqB,GAAG,CAAC,MAA2C,EAAkB,EAAE;IAC5F,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAG,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAA;IAC1D,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS;QACjC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,CAAC;QACpC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;AACrE,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAAC,OAAqB,EAAkB,EAAE;IAC1D,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAA;IAC7B,IAAI,EAAE,KAAK,SAAS;QAAE,OAAO,qBAAqB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;IAC3E,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;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAsB,EACtB,UAAwB,EAAE;IAE1B,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9B,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,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAC9B,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": "5.5.1",
3
+ "version": "5.7.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",
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "peerDependencies": {
44
- "@nifrajs/core": "^5.5.1"
44
+ "@nifrajs/core": "^5.7.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@nifrajs/core": "workspace:*"
package/src/csrf.ts CHANGED
@@ -2,9 +2,12 @@
2
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
- * methods (GET/HEAD/OPTIONS) pass. Apply with `app.use(csrf({ origins: ["https://example.com"] }))`.
5
+ * methods (GET/HEAD/OPTIONS) pass, as do routes listed in `exempt`. Apply with
6
+ * `app.use(csrf({ origins: ["https://example.com"] }))`.
6
7
  */
7
- import { isSameOriginRequest, type Middleware } from "@nifrajs/core/server"
8
+ import { expandOptionalParams } from "@nifrajs/core/pattern"
9
+ import { Router } from "@nifrajs/core/router"
10
+ import { isSameOriginRequest, type Middleware, pathnameOf } from "@nifrajs/core/server"
8
11
 
9
12
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"])
10
13
 
@@ -19,18 +22,37 @@ export interface CsrfOptions {
19
22
  * works). Set it when the proxy rewrites `Host`, so the public origin differs from the worker's.
20
23
  */
21
24
  readonly origins?: readonly string[]
25
+ /**
26
+ * Route patterns whose requests skip the check, for a route no browser calls that proves its sender
27
+ * another way - a signed payment webhook arrives with no `Origin` at all. Written as routes are
28
+ * (`/inbound/:vendor`) and matched against the path this server routes, so a different path to
29
+ * the same handler is still checked. Exempt nothing that acts on a session cookie: a cross-site page
30
+ * can post to it.
31
+ */
32
+ readonly exempt?: readonly string[]
22
33
  }
23
34
 
24
35
  const forbidden = (): Response =>
25
36
  Response.json({ ok: false, error: "csrf_failed" }, { status: 403 })
26
37
 
38
+ function exemptionOf(patterns: readonly string[]): ((path: string) => boolean) | undefined {
39
+ if (patterns.length === 0) return undefined
40
+ const forms = new Set(patterns.flatMap((pattern) => expandOptionalParams(pattern)))
41
+ const router = new Router<true>()
42
+ // An exemption names a path whatever the method, so every form sits under one method key.
43
+ for (const form of forms) router.add("POST", form, true)
44
+ return (path) => router.find("POST", path).found
45
+ }
46
+
27
47
  export function csrf(options: CsrfOptions = {}): Middleware {
28
48
  const configured = options.origins !== undefined ? new Set(options.origins) : undefined
49
+ const exempt = exemptionOf(options.exempt ?? [])
29
50
  instances += 1
30
51
  return {
31
52
  name: `csrf#${instances}`,
32
53
  onRequest(req) {
33
54
  if (SAFE_METHODS.has(req.method)) return undefined
55
+ if (exempt?.(pathnameOf(req.url)) === true) return undefined
34
56
  // The same-origin default uses core's check, which accepts an `https:` page reaching an `http:`
35
57
  // request URL on the same host - what every TLS-terminating proxy looks like from here.
36
58
  const allowed = (candidate: string): boolean =>
package/src/guards.ts CHANGED
@@ -79,6 +79,9 @@ const rejection = (options: GuardOptions): ResponseResult => {
79
79
  * (302/401) - control flow, not a `Response`; see the module docs above. Use at
80
80
  * the top of a protected loader:
81
81
  * `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
82
+ *
83
+ * Non-empty is not signed in: a session that holds anything (a cart, a return path, CSRF state)
84
+ * passes. Guard a page for signed-in visitors with {@link requireUser} and the key a login sets.
82
85
  */
83
86
  export function requireSession<Data extends Record<string, unknown>>(
84
87
  session: Session<Data>,