@nifrajs/auth 4.0.2 → 5.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.
- package/dist/csrf.d.ts.map +1 -1
- package/dist/csrf.js +4 -0
- package/dist/csrf.js.map +1 -1
- package/dist/guards.d.ts +12 -2
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +25 -1
- package/dist/guards.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/session.d.ts +8 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +23 -5
- package/dist/session.js.map +1 -1
- package/package.json +2 -2
- package/src/csrf.ts +4 -0
- package/src/guards.ts +44 -3
- package/src/index.ts +6 -1
- package/src/session.ts +31 -9
package/dist/csrf.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/csrf.js
CHANGED
|
@@ -22,6 +22,10 @@ export function csrf(options = {}) {
|
|
|
22
22
|
// request URL on the same host - what every TLS-terminating proxy looks like from here.
|
|
23
23
|
const allowed = (candidate) => configured !== undefined ? configured.has(candidate) : isSameOriginRequest(candidate, req);
|
|
24
24
|
const origin = req.headers.get("origin");
|
|
25
|
+
// Under `Referrer-Policy: no-referrer` a browser sends a same-origin form POST with
|
|
26
|
+
// `Origin: null`; its own `Sec-Fetch-Site` still says same-origin, and script can set neither.
|
|
27
|
+
if (origin === "null" && configured === undefined)
|
|
28
|
+
return req.headers.get("sec-fetch-site") === "same-origin" ? undefined : forbidden();
|
|
25
29
|
if (origin !== null)
|
|
26
30
|
return allowed(origin) ? undefined : forbidden();
|
|
27
31
|
// Some same-origin requests omit `Origin` - fall back to the `Referer`'s origin.
|
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,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":"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"}
|
package/dist/guards.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { Session } from "./session.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/** The response a guard denies a missing session with, in place of the default `401` JSON. */
|
|
3
|
+
export interface UnauthenticatedResponse {
|
|
4
|
+
/** An error status, 400-599. Default `401`. */
|
|
5
|
+
readonly status?: number;
|
|
6
|
+
/** The body; present but `undefined` sends none. Default `{ ok: false, error: "unauthorized" }`. */
|
|
7
|
+
readonly body?: unknown;
|
|
8
|
+
/** Response headers, such as a `www-authenticate` challenge. */
|
|
9
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
10
|
+
}
|
|
11
|
+
/** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or - omitted - the
|
|
12
|
+
* `unauthenticated` response, by default a 401 JSON (`{ ok: false, error: "unauthorized" }`). */
|
|
4
13
|
export interface GuardOptions {
|
|
5
14
|
readonly redirectTo?: string;
|
|
15
|
+
readonly unauthenticated?: UnauthenticatedResponse;
|
|
6
16
|
}
|
|
7
17
|
/**
|
|
8
18
|
* Require a non-empty session. Returns it when present; otherwise throws a `status(...)` render
|
package/dist/guards.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/guards.js
CHANGED
|
@@ -13,10 +13,32 @@
|
|
|
13
13
|
* Pairs with `@nifrajs/auth` sessions but only needs the `Session` shape, so it's framework-agnostic.
|
|
14
14
|
*/
|
|
15
15
|
import { isSameOriginPath, status } from "@nifrajs/core/server";
|
|
16
|
+
const UNAUTHORIZED = { ok: false, error: "unauthorized" };
|
|
17
|
+
/** A dev-authored denial that is not an error status, or that competes with a redirect, fails loud. */
|
|
18
|
+
function assertUnauthenticated(options) {
|
|
19
|
+
const custom = options.unauthenticated;
|
|
20
|
+
if (custom === undefined)
|
|
21
|
+
return;
|
|
22
|
+
if (options.redirectTo !== undefined) {
|
|
23
|
+
throw new Error("[nifra/auth] a guard takes redirectTo or unauthenticated, not both");
|
|
24
|
+
}
|
|
25
|
+
const code = custom.status;
|
|
26
|
+
if (code !== undefined && (!Number.isInteger(code) || code < 400 || code > 599)) {
|
|
27
|
+
throw new RangeError(`[nifra/auth] guard unauthenticated.status must be an integer 400-599 (got ${String(code)})`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const unauthenticatedResult = (custom) => {
|
|
31
|
+
if (custom === undefined)
|
|
32
|
+
return status(401, UNAUTHORIZED);
|
|
33
|
+
const body = "body" in custom ? custom.body : UNAUTHORIZED;
|
|
34
|
+
return custom.headers === undefined
|
|
35
|
+
? status(custom.status ?? 401, body)
|
|
36
|
+
: status(custom.status ?? 401, body, { headers: custom.headers });
|
|
37
|
+
};
|
|
16
38
|
const rejection = (options) => {
|
|
17
39
|
const to = options.redirectTo;
|
|
18
40
|
if (to === undefined)
|
|
19
|
-
return
|
|
41
|
+
return unauthenticatedResult(options.unauthenticated);
|
|
20
42
|
// The kernel's same-origin predicate, the one `@nifrajs/web`'s `redirect` uses: a single leading
|
|
21
43
|
// "/", never "//host", an absolute URL, or a form a URL parser resolves off-origin. `redirectTo`
|
|
22
44
|
// is dev-authored, so a bad value is a config bug - fail loud here.
|
|
@@ -32,6 +54,7 @@ const rejection = (options) => {
|
|
|
32
54
|
* `const session = requireSession(await sessions.get(c), { redirectTo: "/login" })`.
|
|
33
55
|
*/
|
|
34
56
|
export function requireSession(session, options = {}) {
|
|
57
|
+
assertUnauthenticated(options);
|
|
35
58
|
if (!session.isEmpty)
|
|
36
59
|
return session;
|
|
37
60
|
throw rejection(options);
|
|
@@ -42,6 +65,7 @@ export function requireSession(session, options = {}) {
|
|
|
42
65
|
* guard: `const userId = requireUser(await sessions.get(c), "userId", { redirectTo: "/login" })`.
|
|
43
66
|
*/
|
|
44
67
|
export function requireUser(session, key, options = {}) {
|
|
68
|
+
assertUnauthenticated(options);
|
|
45
69
|
const value = session.get(key);
|
|
46
70
|
// Narrowed by the runtime check; the generic `Data[K] | undefined` doesn't auto-narrow to NonNullable.
|
|
47
71
|
if (value !== undefined && value !== null)
|
package/dist/guards.js.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { type AuthorizationRequest, type Authorizer, isAuthorized, requireAuthorization, } from "./authorization.js";
|
|
7
7
|
export { type CsrfOptions, csrf } from "./csrf.js";
|
|
8
|
-
export { type GuardOptions, requireSession, requireUser } from "./guards.js";
|
|
8
|
+
export { type GuardOptions, requireSession, requireUser, type UnauthenticatedResponse, } from "./guards.js";
|
|
9
9
|
export { createSessions, type Session, type SessionContext, type SessionCookieOptions, type SessionManager, type SessionOptions, } from "./session.js";
|
|
10
10
|
export { type KVNamespaceLike, KVSessionStore, MemorySessionStore, type MemorySessionStoreOptions, type SessionRecord, type SessionStore, } from "./store.js";
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,WAAW,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,WAAW,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EACL,KAAK,YAAY,EACjB,cAAc,EACd,WAAW,EACX,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,cAAc,EACd,KAAK,OAAO,EACZ,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,eAAe,EACpB,cAAc,EACd,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { isAuthorized, requireAuthorization, } from "./authorization.js";
|
|
7
7
|
export { csrf } from "./csrf.js";
|
|
8
|
-
export { requireSession, requireUser } from "./guards.js";
|
|
8
|
+
export { requireSession, requireUser, } from "./guards.js";
|
|
9
9
|
export { createSessions, } from "./session.js";
|
|
10
10
|
export { KVSessionStore, MemorySessionStore, } from "./store.js";
|
|
11
11
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGL,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAoB,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAGL,YAAY,EACZ,oBAAoB,GACrB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAoB,IAAI,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAEL,cAAc,EACd,WAAW,GAEZ,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,cAAc,GAMf,MAAM,cAAc,CAAA;AACrB,OAAO,EAEL,cAAc,EACd,kBAAkB,GAInB,MAAM,YAAY,CAAA"}
|
package/dist/session.d.ts
CHANGED
|
@@ -59,6 +59,14 @@ export interface SessionOptions {
|
|
|
59
59
|
readonly cookie?: SessionCookieOptions;
|
|
60
60
|
/** Clock (ms) - injected for testability; production passes `() => Date.now()` (the default). */
|
|
61
61
|
readonly now?: () => number;
|
|
62
|
+
/**
|
|
63
|
+
* Also accept a session cookie signed by `@nifrajs/auth` before 5.0, whose signature does not cover
|
|
64
|
+
* the cookie name, so upgrading from 4.x signs no one out. Off by default: while it is on, any
|
|
65
|
+
* value the app signs with this secret through `@nifrajs/core`'s `signValue` (a signed preference
|
|
66
|
+
* cookie, say) and that parses as a session reads as one. Turn it on for one `maxAge` after the
|
|
67
|
+
* upgrade, then remove it; each such session is re-signed in the current form on its next commit.
|
|
68
|
+
*/
|
|
69
|
+
readonly acceptLegacyCookies?: boolean;
|
|
62
70
|
}
|
|
63
71
|
export interface SessionManager<Data extends Record<string, unknown> = Record<string, unknown>> {
|
|
64
72
|
/** Load the session from the request cookie (verified + un-expired), or a fresh anonymous one. */
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAiB,YAAY,EAAE,MAAM,YAAY,CAAA;AAE7D;qDACqD;AACrD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAClD,gFAAgF;IAChF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,GAAG,EAAE;QACZ,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;QAClE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KACjG,CAAA;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO,CAAC,IAAI,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACrF,GAAG,CAAC,CAAC,SAAS,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,CAAA;IACtD,GAAG,CAAC,CAAC,SAAS,MAAM,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAA;IACvD,KAAK,CAAC,GAAG,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;IAC5B,GAAG,CAAC,GAAG,EAAE,MAAM,IAAI,GAAG,OAAO,CAAA;IAC7B,mGAAmG;IACnG,KAAK,IAAI,IAAI,CAAA;IACb,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;2EAC2E;AAC3E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAA;AAEjG,MAAM,WAAW,cAAc;IAC7B;;;gCAG4B;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAA;IAC7B,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,kGAAkG;IAClG,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B;mFAC+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,oBAAoB,CAAA;IACtC,iGAAiG;IACjG,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/dist/session.js
CHANGED
|
@@ -60,6 +60,7 @@ export function createSessions(options) {
|
|
|
60
60
|
}
|
|
61
61
|
const rolling = options.rolling ?? true;
|
|
62
62
|
const now = options.now ?? (() => Date.now());
|
|
63
|
+
const acceptLegacy = options.acceptLegacyCookies === true;
|
|
63
64
|
// Annotated (not just `?? {}`) so `cookieOpts.path`/`.domain` stay typed - `?? {}` alone widens the
|
|
64
65
|
// type to include the empty-object literal, which the package's build tsconfig rejects on access.
|
|
65
66
|
const cookieOpts = options.cookie ?? {};
|
|
@@ -103,10 +104,24 @@ export function createSessions(options) {
|
|
|
103
104
|
}
|
|
104
105
|
return state;
|
|
105
106
|
};
|
|
107
|
+
// The cookie name is signed with the value, so when two managers share a secret one's cookie does
|
|
108
|
+
// not verify as the other's. On the wire the cookie keeps its `value.signature` shape.
|
|
109
|
+
const bound = `${cookieName}\n`;
|
|
110
|
+
const sign = async (value) => (await signValue(`${bound}${value}`, secret)).slice(bound.length);
|
|
111
|
+
const unsign = async (raw) => {
|
|
112
|
+
const named = await unsignValue(`${bound}${raw}`, secret);
|
|
113
|
+
if (named !== null)
|
|
114
|
+
return named.slice(bound.length);
|
|
115
|
+
if (!acceptLegacy)
|
|
116
|
+
return null;
|
|
117
|
+
// Signed before names were bound; re-signed bound on its next commit.
|
|
118
|
+
const legacy = await unsignValue(raw, secret);
|
|
119
|
+
return legacy === null || legacy.includes("\n") ? null : legacy;
|
|
120
|
+
};
|
|
106
121
|
const loadFromRaw = async (raw) => {
|
|
107
122
|
if (raw === undefined || raw === "")
|
|
108
123
|
return make({});
|
|
109
|
-
const unsigned = await
|
|
124
|
+
const unsigned = await unsign(raw);
|
|
110
125
|
if (unsigned === null)
|
|
111
126
|
return make({}); // tampered / wrong secret → fail closed (anonymous)
|
|
112
127
|
if (store !== undefined) {
|
|
@@ -122,7 +137,10 @@ export function createSessions(options) {
|
|
|
122
137
|
const payload = parsePayload(unsigned);
|
|
123
138
|
if (payload === null || payload.expiresAt <= now())
|
|
124
139
|
return make({});
|
|
125
|
-
|
|
140
|
+
// At most a fresh session's life: a legacy cookie, or any other value signed with this secret,
|
|
141
|
+
// must not name its own expiry, which a non-rolling commit would keep.
|
|
142
|
+
const expiresAt = Math.min(payload.expiresAt, now() + maxAge * 1000);
|
|
143
|
+
return make({ data: payload.data, expiresAt });
|
|
126
144
|
};
|
|
127
145
|
const sessionCookie = (c) => c.request !== undefined && hasDuplicateCookie(c.request.headers.get("cookie"), cookieName)
|
|
128
146
|
? undefined
|
|
@@ -153,11 +171,11 @@ export function createSessions(options) {
|
|
|
153
171
|
if (state.id === undefined)
|
|
154
172
|
state.id = randomId();
|
|
155
173
|
await store.set(state.id, { data, expiresAt });
|
|
156
|
-
writeCookie(c, await
|
|
174
|
+
writeCookie(c, await sign(state.id), expiresAt);
|
|
157
175
|
return;
|
|
158
176
|
}
|
|
159
177
|
// Cookie mode: the (signed) payload IS the cookie. serializeCookie enforces the 4 KB cap.
|
|
160
|
-
writeCookie(c, await
|
|
178
|
+
writeCookie(c, await sign(JSON.stringify({ data, expiresAt })), expiresAt);
|
|
161
179
|
};
|
|
162
180
|
const destroy = async (c, session) => {
|
|
163
181
|
if (store !== undefined) {
|
|
@@ -170,7 +188,7 @@ export function createSessions(options) {
|
|
|
170
188
|
// first load a Session object. The signature check prevents deleting attacker-chosen IDs.
|
|
171
189
|
const raw = sessionCookie(c);
|
|
172
190
|
if (raw !== undefined)
|
|
173
|
-
id = (await
|
|
191
|
+
id = (await unsign(raw)) ?? undefined;
|
|
174
192
|
}
|
|
175
193
|
if (id !== undefined)
|
|
176
194
|
await store.delete(id);
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAEL,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,WAAW,GACZ,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@nifrajs/core": "^
|
|
43
|
+
"@nifrajs/core": "^5.1.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@nifrajs/core": "workspace:*"
|
package/src/csrf.ts
CHANGED
|
@@ -37,6 +37,10 @@ export function csrf(options: CsrfOptions = {}): Middleware {
|
|
|
37
37
|
configured !== undefined ? configured.has(candidate) : isSameOriginRequest(candidate, req)
|
|
38
38
|
|
|
39
39
|
const origin = req.headers.get("origin")
|
|
40
|
+
// Under `Referrer-Policy: no-referrer` a browser sends a same-origin form POST with
|
|
41
|
+
// `Origin: null`; its own `Sec-Fetch-Site` still says same-origin, and script can set neither.
|
|
42
|
+
if (origin === "null" && configured === undefined)
|
|
43
|
+
return req.headers.get("sec-fetch-site") === "same-origin" ? undefined : forbidden()
|
|
40
44
|
if (origin !== null) return allowed(origin) ? undefined : forbidden()
|
|
41
45
|
|
|
42
46
|
// Some same-origin requests omit `Origin` - fall back to the `Referer`'s origin.
|
package/src/guards.ts
CHANGED
|
@@ -15,15 +15,54 @@
|
|
|
15
15
|
import { isSameOriginPath, type ResponseResult, status } from "@nifrajs/core/server"
|
|
16
16
|
import type { Session } from "./session.ts"
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/** The response a guard denies a missing session with, in place of the default `401` JSON. */
|
|
19
|
+
export interface UnauthenticatedResponse {
|
|
20
|
+
/** An error status, 400-599. Default `401`. */
|
|
21
|
+
readonly status?: number
|
|
22
|
+
/** The body; present but `undefined` sends none. Default `{ ok: false, error: "unauthorized" }`. */
|
|
23
|
+
readonly body?: unknown
|
|
24
|
+
/** Response headers, such as a `www-authenticate` challenge. */
|
|
25
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** What a guard does when the check fails: 302 to `redirectTo` (a same-origin path), or - omitted - the
|
|
29
|
+
* `unauthenticated` response, by default a 401 JSON (`{ ok: false, error: "unauthorized" }`). */
|
|
20
30
|
export interface GuardOptions {
|
|
21
31
|
readonly redirectTo?: string
|
|
32
|
+
readonly unauthenticated?: UnauthenticatedResponse
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const UNAUTHORIZED = { ok: false, error: "unauthorized" } as const
|
|
36
|
+
|
|
37
|
+
/** A dev-authored denial that is not an error status, or that competes with a redirect, fails loud. */
|
|
38
|
+
function assertUnauthenticated(options: {
|
|
39
|
+
readonly redirectTo?: string
|
|
40
|
+
readonly unauthenticated?: UnauthenticatedResponse
|
|
41
|
+
}): void {
|
|
42
|
+
const custom = options.unauthenticated
|
|
43
|
+
if (custom === undefined) return
|
|
44
|
+
if (options.redirectTo !== undefined) {
|
|
45
|
+
throw new Error("[nifra/auth] a guard takes redirectTo or unauthenticated, not both")
|
|
46
|
+
}
|
|
47
|
+
const code = custom.status
|
|
48
|
+
if (code !== undefined && (!Number.isInteger(code) || code < 400 || code > 599)) {
|
|
49
|
+
throw new RangeError(
|
|
50
|
+
`[nifra/auth] guard unauthenticated.status must be an integer 400-599 (got ${String(code)})`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const unauthenticatedResult = (custom: UnauthenticatedResponse | undefined): ResponseResult => {
|
|
56
|
+
if (custom === undefined) return status(401, UNAUTHORIZED)
|
|
57
|
+
const body = "body" in custom ? custom.body : UNAUTHORIZED
|
|
58
|
+
return custom.headers === undefined
|
|
59
|
+
? status(custom.status ?? 401, body)
|
|
60
|
+
: status(custom.status ?? 401, body, { headers: custom.headers })
|
|
22
61
|
}
|
|
23
62
|
|
|
24
63
|
const rejection = (options: GuardOptions): ResponseResult => {
|
|
25
64
|
const to = options.redirectTo
|
|
26
|
-
if (to === undefined) return
|
|
65
|
+
if (to === undefined) return unauthenticatedResult(options.unauthenticated)
|
|
27
66
|
// The kernel's same-origin predicate, the one `@nifrajs/web`'s `redirect` uses: a single leading
|
|
28
67
|
// "/", never "//host", an absolute URL, or a form a URL parser resolves off-origin. `redirectTo`
|
|
29
68
|
// is dev-authored, so a bad value is a config bug - fail loud here.
|
|
@@ -45,6 +84,7 @@ export function requireSession<Data extends Record<string, unknown>>(
|
|
|
45
84
|
session: Session<Data>,
|
|
46
85
|
options: GuardOptions = {},
|
|
47
86
|
): Session<Data> {
|
|
87
|
+
assertUnauthenticated(options)
|
|
48
88
|
if (!session.isEmpty) return session
|
|
49
89
|
throw rejection(options)
|
|
50
90
|
}
|
|
@@ -59,6 +99,7 @@ export function requireUser<Data extends Record<string, unknown>, K extends keyo
|
|
|
59
99
|
key: K,
|
|
60
100
|
options: GuardOptions = {},
|
|
61
101
|
): NonNullable<Data[K]> {
|
|
102
|
+
assertUnauthenticated(options)
|
|
62
103
|
const value = session.get(key)
|
|
63
104
|
// Narrowed by the runtime check; the generic `Data[K] | undefined` doesn't auto-narrow to NonNullable.
|
|
64
105
|
if (value !== undefined && value !== null) return value as NonNullable<Data[K]>
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,12 @@ export {
|
|
|
11
11
|
requireAuthorization,
|
|
12
12
|
} from "./authorization.ts"
|
|
13
13
|
export { type CsrfOptions, csrf } from "./csrf.ts"
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
type GuardOptions,
|
|
16
|
+
requireSession,
|
|
17
|
+
requireUser,
|
|
18
|
+
type UnauthenticatedResponse,
|
|
19
|
+
} from "./guards.ts"
|
|
15
20
|
export {
|
|
16
21
|
createSessions,
|
|
17
22
|
type Session,
|
package/src/session.ts
CHANGED
|
@@ -66,6 +66,14 @@ export interface SessionOptions {
|
|
|
66
66
|
readonly cookie?: SessionCookieOptions
|
|
67
67
|
/** Clock (ms) - injected for testability; production passes `() => Date.now()` (the default). */
|
|
68
68
|
readonly now?: () => number
|
|
69
|
+
/**
|
|
70
|
+
* Also accept a session cookie signed by `@nifrajs/auth` before 5.0, whose signature does not cover
|
|
71
|
+
* the cookie name, so upgrading from 4.x signs no one out. Off by default: while it is on, any
|
|
72
|
+
* value the app signs with this secret through `@nifrajs/core`'s `signValue` (a signed preference
|
|
73
|
+
* cookie, say) and that parses as a session reads as one. Turn it on for one `maxAge` after the
|
|
74
|
+
* upgrade, then remove it; each such session is re-signed in the current form on its next commit.
|
|
75
|
+
*/
|
|
76
|
+
readonly acceptLegacyCookies?: boolean
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export interface SessionManager<Data extends Record<string, unknown> = Record<string, unknown>> {
|
|
@@ -141,6 +149,7 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
|
|
|
141
149
|
}
|
|
142
150
|
const rolling = options.rolling ?? true
|
|
143
151
|
const now = options.now ?? (() => Date.now())
|
|
152
|
+
const acceptLegacy = options.acceptLegacyCookies === true
|
|
144
153
|
// Annotated (not just `?? {}`) so `cookieOpts.path`/`.domain` stay typed - `?? {}` alone widens the
|
|
145
154
|
// type to include the empty-object literal, which the package's build tsconfig rejects on access.
|
|
146
155
|
const cookieOpts: SessionCookieOptions = options.cookie ?? {}
|
|
@@ -198,9 +207,23 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
|
|
|
198
207
|
return state
|
|
199
208
|
}
|
|
200
209
|
|
|
210
|
+
// The cookie name is signed with the value, so when two managers share a secret one's cookie does
|
|
211
|
+
// not verify as the other's. On the wire the cookie keeps its `value.signature` shape.
|
|
212
|
+
const bound = `${cookieName}\n`
|
|
213
|
+
const sign = async (value: string): Promise<string> =>
|
|
214
|
+
(await signValue(`${bound}${value}`, secret)).slice(bound.length)
|
|
215
|
+
const unsign = async (raw: string): Promise<string | null> => {
|
|
216
|
+
const named = await unsignValue(`${bound}${raw}`, secret)
|
|
217
|
+
if (named !== null) return named.slice(bound.length)
|
|
218
|
+
if (!acceptLegacy) return null
|
|
219
|
+
// Signed before names were bound; re-signed bound on its next commit.
|
|
220
|
+
const legacy = await unsignValue(raw, secret)
|
|
221
|
+
return legacy === null || legacy.includes("\n") ? null : legacy
|
|
222
|
+
}
|
|
223
|
+
|
|
201
224
|
const loadFromRaw = async (raw: string | undefined): Promise<Session<Data>> => {
|
|
202
225
|
if (raw === undefined || raw === "") return make({})
|
|
203
|
-
const unsigned = await
|
|
226
|
+
const unsigned = await unsign(raw)
|
|
204
227
|
if (unsigned === null) return make({}) // tampered / wrong secret → fail closed (anonymous)
|
|
205
228
|
|
|
206
229
|
if (store !== undefined) {
|
|
@@ -215,7 +238,10 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
|
|
|
215
238
|
|
|
216
239
|
const payload = parsePayload(unsigned)
|
|
217
240
|
if (payload === null || payload.expiresAt <= now()) return make({})
|
|
218
|
-
|
|
241
|
+
// At most a fresh session's life: a legacy cookie, or any other value signed with this secret,
|
|
242
|
+
// must not name its own expiry, which a non-rolling commit would keep.
|
|
243
|
+
const expiresAt = Math.min(payload.expiresAt, now() + maxAge * 1000)
|
|
244
|
+
return make({ data: payload.data, expiresAt })
|
|
219
245
|
}
|
|
220
246
|
|
|
221
247
|
const sessionCookie = (c: SessionContext): string | undefined =>
|
|
@@ -253,15 +279,11 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
|
|
|
253
279
|
}
|
|
254
280
|
if (state.id === undefined) state.id = randomId()
|
|
255
281
|
await store.set(state.id, { data, expiresAt } satisfies SessionRecord)
|
|
256
|
-
writeCookie(c, await
|
|
282
|
+
writeCookie(c, await sign(state.id), expiresAt)
|
|
257
283
|
return
|
|
258
284
|
}
|
|
259
285
|
// Cookie mode: the (signed) payload IS the cookie. serializeCookie enforces the 4 KB cap.
|
|
260
|
-
writeCookie(
|
|
261
|
-
c,
|
|
262
|
-
await signValue(JSON.stringify({ data, expiresAt } satisfies Payload), secret),
|
|
263
|
-
expiresAt,
|
|
264
|
-
)
|
|
286
|
+
writeCookie(c, await sign(JSON.stringify({ data, expiresAt } satisfies Payload)), expiresAt)
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
const destroy = async (c: SessionContext, session?: Session<Data>): Promise<void> => {
|
|
@@ -273,7 +295,7 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
|
|
|
273
295
|
// Revoke the record addressed by the signed request cookie even when the caller did not
|
|
274
296
|
// first load a Session object. The signature check prevents deleting attacker-chosen IDs.
|
|
275
297
|
const raw = sessionCookie(c)
|
|
276
|
-
if (raw !== undefined) id = (await
|
|
298
|
+
if (raw !== undefined) id = (await unsign(raw)) ?? undefined
|
|
277
299
|
}
|
|
278
300
|
if (id !== undefined) await store.delete(id)
|
|
279
301
|
}
|