@nifrajs/auth 2.13.0 → 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.
- 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/package.json +2 -2
- package/src/authorization.ts +9 -2
- package/src/guards.ts +22 -13
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/auth",
|
|
3
|
-
"version": "
|
|
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": "^
|
|
40
|
+
"@nifrajs/core": "^3.1.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
|
*/
|