@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.
- package/dist/authorization.d.ts +6 -5
- package/dist/authorization.d.ts.map +1 -1
- package/dist/authorization.js +11 -2
- package/dist/authorization.js.map +1 -1
- package/dist/guards.d.ts +2 -7
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +25 -9
- package/dist/guards.js.map +1 -1
- package/dist/store.d.ts +4 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +9 -5
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
- package/src/authorization.ts +9 -2
- package/src/guards.ts +22 -13
- package/src/store.ts +9 -5
package/dist/authorization.d.ts
CHANGED
|
@@ -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
|
|
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":"
|
|
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"}
|
package/dist/authorization.js
CHANGED
|
@@ -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
|
|
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
|
|
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":"
|
|
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 `
|
|
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
|
*/
|
package/dist/guards.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"
|
|
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
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
if (!
|
|
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
|
|
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 `
|
|
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
|
*/
|
package/dist/guards.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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>;
|
package/dist/store.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
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
|
|
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":"AAAA;;;;;GAKG;AA+BH;;;;GAIG;AACH,MAAM,OAAO,kBAAkB;IACZ,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAA;IAC3C,GAAG,CAAQ;IAE5B,YAAY,UAAqC,EAAE;QACjD,MAAM,MAAM,GAAG,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,GAAG,EAAE,QAAQ,KAAK,YAAY,CAAA;QACvF,IAAI,OAAO,CAAC,iBAAiB,KAAK,IAAI,IAAI,MAAM,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CACb,gGAAgG;gBAC9F,4FAA4F;gBAC5F,gEAAgE,CACnE,CAAA;QACH,CAAC;QACD,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,MAAM,CAAA;QAChC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;YACrD,MAAM,IAAI,UAAU,CAAC,qEAAqE,CAAC,CAAA;QAC7F,CAAC;IACH,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAC/C,CAAC;IAED,GAAG,CAAC,EAAU,EAAE,MAAqB;QACnC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA,CAAC,0EAA0E;QACnG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QAC7B,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACrC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAA;YAChD,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAK;YAC/B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAC9B,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;IAC1B,CAAC;IAED,MAAM,CAAC,EAAU;QACf,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QACxB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;IAC1B,CAAC;CACF;AAcD;;qEAEqE;AACrE,MAAM,eAAe,GAAG,CAAC,KAAc,EAA0B,EAAE;IACjE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,CAAC,GAAG,KAAgC,CAAA;IAC1C,OAAO,CACL,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ;QAC/B,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;QAC5B,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,CAAC,CAAC,IAAI,KAAK,IAAI,CAChB,CAAA;AACH,CAAC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,OAAO,cAAc;IACR,EAAE,CAAiB;
|
|
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.
|
|
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.
|
|
40
|
+
"@nifrajs/core": "^3.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@nifrajs/core": "workspace:*"
|
package/src/authorization.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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):
|
|
24
|
+
const rejection = (options: GuardOptions): ResponseResult => {
|
|
16
25
|
const to = options.redirectTo
|
|
17
|
-
if (to === undefined) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
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
|
|
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 `
|
|
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
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
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
|
|
143
|
+
expiration,
|
|
140
144
|
})
|
|
141
145
|
}
|
|
142
146
|
|