@siremzam/sentinel 0.3.2 → 0.3.3
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/README.md +20 -4
- package/dist/middleware/hono.cjs +57 -0
- package/dist/middleware/hono.cjs.map +1 -0
- package/dist/middleware/hono.d.cts +45 -0
- package/dist/middleware/hono.d.ts +45 -0
- package/dist/middleware/hono.js +32 -0
- package/dist/middleware/hono.js.map +1 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -28,11 +28,11 @@ This library was built from a different starting point:
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
### What's New in 0.3.
|
|
31
|
+
### What's New in 0.3.3
|
|
32
32
|
|
|
33
|
+
- Hono middleware — `honoGuard()` via `@siremzam/sentinel/middleware/hono`
|
|
33
34
|
- README rewrite: evaluation walkthrough, concepts glossary, patterns & recipes, migration guide, benchmark data
|
|
34
35
|
- Standalone example (`examples/standalone/`) — no HTTP server needed
|
|
35
|
-
- "When NOT to Use This" and "Testing Your Policies" sections
|
|
36
36
|
|
|
37
37
|
See the full [CHANGELOG](./CHANGELOG.md).
|
|
38
38
|
|
|
@@ -61,7 +61,7 @@ See the full [CHANGELOG](./CHANGELOG.md).
|
|
|
61
61
|
- [toAuditEntry()](#toauditentry)
|
|
62
62
|
- [permitted() — UI Rendering](#permitted--ui-rendering)
|
|
63
63
|
- [Integration](#integration)
|
|
64
|
-
- [Middleware (Express, Fastify, NestJS)](#middleware)
|
|
64
|
+
- [Middleware (Express, Fastify, Hono, NestJS)](#middleware)
|
|
65
65
|
- [Server Mode](#server-mode)
|
|
66
66
|
- [JSON Policy Serialization](#json-policy-serialization)
|
|
67
67
|
- [Performance](#performance)
|
|
@@ -93,7 +93,7 @@ See the full [CHANGELOG](./CHANGELOG.md).
|
|
|
93
93
|
| UI permission set | `permitted()` returns `Set` | No | `permission.filter()` | `ability.can()` per action |
|
|
94
94
|
| JSON policy storage | `exportRules` / `importRules` + `ConditionRegistry` | CSV / JSON adapters | No | Via `@casl/ability/extra` |
|
|
95
95
|
| Server mode (HTTP microservice) | Built-in (`createAuthServer`) | No | No | No |
|
|
96
|
-
| Middleware | Express, Fastify, NestJS | Express (community) | Express (community) | Express, NestJS |
|
|
96
|
+
| Middleware | Express, Fastify, Hono, NestJS | Express (community) | Express (community) | Express, NestJS |
|
|
97
97
|
| Dependencies | **0** | 2+ | 2 | 1+ |
|
|
98
98
|
| DSL required | **No** (pure TypeScript) | Yes (Casbin model) | No | No |
|
|
99
99
|
|
|
@@ -573,6 +573,22 @@ fastify.post("/invoices/:id/approve", {
|
|
|
573
573
|
}, handler);
|
|
574
574
|
```
|
|
575
575
|
|
|
576
|
+
**Hono:**
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
import { honoGuard } from "@siremzam/sentinel/middleware/hono";
|
|
580
|
+
|
|
581
|
+
app.post(
|
|
582
|
+
"/invoices/:id/approve",
|
|
583
|
+
honoGuard(engine, "invoice:approve", "invoice", {
|
|
584
|
+
getSubject: (c) => c.get("user"),
|
|
585
|
+
getResourceContext: (c) => ({ id: c.req.param("id") }),
|
|
586
|
+
getTenantId: (c) => c.req.header("x-tenant-id"),
|
|
587
|
+
}),
|
|
588
|
+
handler,
|
|
589
|
+
);
|
|
590
|
+
```
|
|
591
|
+
|
|
576
592
|
**NestJS:**
|
|
577
593
|
|
|
578
594
|
```typescript
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/middleware/hono.ts
|
|
21
|
+
var hono_exports = {};
|
|
22
|
+
__export(hono_exports, {
|
|
23
|
+
honoGuard: () => honoGuard
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(hono_exports);
|
|
26
|
+
function honoGuard(engine, action, resource, options) {
|
|
27
|
+
return async (c, next) => {
|
|
28
|
+
const subject = options.getSubject(c);
|
|
29
|
+
if (!subject) {
|
|
30
|
+
return c.json({ error: "Unauthorized \u2014 no subject" }, 401);
|
|
31
|
+
}
|
|
32
|
+
let decision;
|
|
33
|
+
try {
|
|
34
|
+
const resourceContext = options.getResourceContext?.(c) ?? {};
|
|
35
|
+
const tenantId = options.getTenantId?.(c);
|
|
36
|
+
decision = engine.evaluate(subject, action, resource, resourceContext, tenantId);
|
|
37
|
+
} catch {
|
|
38
|
+
return c.json({ error: "Internal authorization error" }, 500);
|
|
39
|
+
}
|
|
40
|
+
if (decision.allowed) {
|
|
41
|
+
await next();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (options.onDenied) {
|
|
45
|
+
return options.onDenied(c);
|
|
46
|
+
}
|
|
47
|
+
return c.json({
|
|
48
|
+
error: "Forbidden",
|
|
49
|
+
reason: decision.reason
|
|
50
|
+
}, 403);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
54
|
+
0 && (module.exports = {
|
|
55
|
+
honoGuard
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=hono.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/hono.ts"],"sourcesContent":["import type { AccessEngine } from \"../engine.js\";\nimport type { SchemaDefinition, InferAction, InferResource, Subject, ResourceContext } from \"../types.js\";\n\n/**\n * Minimal Hono-compatible types so we don't depend on hono at runtime.\n */\ninterface HonoContext {\n req: {\n raw: Request;\n header(name: string): string | undefined;\n param(name: string): string | undefined;\n [key: string]: unknown;\n };\n json(data: unknown, status?: number): Response;\n get<T = unknown>(key: string): T;\n set(key: string, value: unknown): void;\n [key: string]: unknown;\n}\n\ntype HonoNext = () => Promise<void>;\ntype HonoMiddleware = (c: HonoContext, next: HonoNext) => Promise<Response | void>;\n\nexport interface HonoGuardOptions<S extends SchemaDefinition> {\n /** Extract the subject from the Hono context. */\n getSubject: (c: HonoContext) => Subject<S> | undefined;\n /** Extract the resource context from the Hono context (optional). */\n getResourceContext?: (c: HonoContext) => ResourceContext;\n /** Extract the tenant ID from the Hono context (optional). */\n getTenantId?: (c: HonoContext) => string | undefined;\n /** Custom denial handler. Return a Response to override the default 403. */\n onDenied?: (c: HonoContext) => Response;\n}\n\n/**\n * Hono middleware factory.\n *\n * Usage:\n * app.post(\n * \"/invoices/:id/approve\",\n * honoGuard(engine, \"invoice:approve\", \"invoice\", {\n * getSubject: (c) => c.get(\"user\"),\n * getTenantId: (c) => c.req.header(\"x-tenant-id\"),\n * }),\n * handler,\n * );\n */\nexport function honoGuard<S extends SchemaDefinition>(\n engine: AccessEngine<S>,\n action: InferAction<S>,\n resource: InferResource<S>,\n options: HonoGuardOptions<S>,\n): HonoMiddleware {\n return async (c: HonoContext, next: HonoNext) => {\n const subject = options.getSubject(c);\n if (!subject) {\n return c.json({ error: \"Unauthorized — no subject\" }, 401);\n }\n\n let decision;\n try {\n const resourceContext = options.getResourceContext?.(c) ?? {};\n const tenantId = options.getTenantId?.(c);\n decision = engine.evaluate(subject, action, resource, resourceContext, tenantId);\n } catch {\n return c.json({ error: \"Internal authorization error\" }, 500);\n }\n\n if (decision.allowed) {\n await next();\n return;\n }\n\n if (options.onDenied) {\n return options.onDenied(c);\n }\n\n return c.json({\n error: \"Forbidden\",\n reason: decision.reason,\n }, 403);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CO,SAAS,UACd,QACA,QACA,UACA,SACgB;AAChB,SAAO,OAAO,GAAgB,SAAmB;AAC/C,UAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,QAAI,CAAC,SAAS;AACZ,aAAO,EAAE,KAAK,EAAE,OAAO,iCAA4B,GAAG,GAAG;AAAA,IAC3D;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,kBAAkB,QAAQ,qBAAqB,CAAC,KAAK,CAAC;AAC5D,YAAM,WAAW,QAAQ,cAAc,CAAC;AACxC,iBAAW,OAAO,SAAS,SAAS,QAAQ,UAAU,iBAAiB,QAAQ;AAAA,IACjF,QAAQ;AACN,aAAO,EAAE,KAAK,EAAE,OAAO,+BAA+B,GAAG,GAAG;AAAA,IAC9D;AAEA,QAAI,SAAS,SAAS;AACpB,YAAM,KAAK;AACX;AAAA,IACF;AAEA,QAAI,QAAQ,UAAU;AACpB,aAAO,QAAQ,SAAS,CAAC;AAAA,IAC3B;AAEA,WAAO,EAAE,KAAK;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ,SAAS;AAAA,IACnB,GAAG,GAAG;AAAA,EACR;AACF;","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { S as SchemaDefinition, r as Subject, R as ResourceContext, A as AccessEngine, I as InferAction, k as InferResource } from '../engine-C6IASR5F.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Hono-compatible types so we don't depend on hono at runtime.
|
|
5
|
+
*/
|
|
6
|
+
interface HonoContext {
|
|
7
|
+
req: {
|
|
8
|
+
raw: Request;
|
|
9
|
+
header(name: string): string | undefined;
|
|
10
|
+
param(name: string): string | undefined;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
json(data: unknown, status?: number): Response;
|
|
14
|
+
get<T = unknown>(key: string): T;
|
|
15
|
+
set(key: string, value: unknown): void;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
type HonoNext = () => Promise<void>;
|
|
19
|
+
type HonoMiddleware = (c: HonoContext, next: HonoNext) => Promise<Response | void>;
|
|
20
|
+
interface HonoGuardOptions<S extends SchemaDefinition> {
|
|
21
|
+
/** Extract the subject from the Hono context. */
|
|
22
|
+
getSubject: (c: HonoContext) => Subject<S> | undefined;
|
|
23
|
+
/** Extract the resource context from the Hono context (optional). */
|
|
24
|
+
getResourceContext?: (c: HonoContext) => ResourceContext;
|
|
25
|
+
/** Extract the tenant ID from the Hono context (optional). */
|
|
26
|
+
getTenantId?: (c: HonoContext) => string | undefined;
|
|
27
|
+
/** Custom denial handler. Return a Response to override the default 403. */
|
|
28
|
+
onDenied?: (c: HonoContext) => Response;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hono middleware factory.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* app.post(
|
|
35
|
+
* "/invoices/:id/approve",
|
|
36
|
+
* honoGuard(engine, "invoice:approve", "invoice", {
|
|
37
|
+
* getSubject: (c) => c.get("user"),
|
|
38
|
+
* getTenantId: (c) => c.req.header("x-tenant-id"),
|
|
39
|
+
* }),
|
|
40
|
+
* handler,
|
|
41
|
+
* );
|
|
42
|
+
*/
|
|
43
|
+
declare function honoGuard<S extends SchemaDefinition>(engine: AccessEngine<S>, action: InferAction<S>, resource: InferResource<S>, options: HonoGuardOptions<S>): HonoMiddleware;
|
|
44
|
+
|
|
45
|
+
export { type HonoGuardOptions, honoGuard };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { S as SchemaDefinition, r as Subject, R as ResourceContext, A as AccessEngine, I as InferAction, k as InferResource } from '../engine-C6IASR5F.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Hono-compatible types so we don't depend on hono at runtime.
|
|
5
|
+
*/
|
|
6
|
+
interface HonoContext {
|
|
7
|
+
req: {
|
|
8
|
+
raw: Request;
|
|
9
|
+
header(name: string): string | undefined;
|
|
10
|
+
param(name: string): string | undefined;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
};
|
|
13
|
+
json(data: unknown, status?: number): Response;
|
|
14
|
+
get<T = unknown>(key: string): T;
|
|
15
|
+
set(key: string, value: unknown): void;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
type HonoNext = () => Promise<void>;
|
|
19
|
+
type HonoMiddleware = (c: HonoContext, next: HonoNext) => Promise<Response | void>;
|
|
20
|
+
interface HonoGuardOptions<S extends SchemaDefinition> {
|
|
21
|
+
/** Extract the subject from the Hono context. */
|
|
22
|
+
getSubject: (c: HonoContext) => Subject<S> | undefined;
|
|
23
|
+
/** Extract the resource context from the Hono context (optional). */
|
|
24
|
+
getResourceContext?: (c: HonoContext) => ResourceContext;
|
|
25
|
+
/** Extract the tenant ID from the Hono context (optional). */
|
|
26
|
+
getTenantId?: (c: HonoContext) => string | undefined;
|
|
27
|
+
/** Custom denial handler. Return a Response to override the default 403. */
|
|
28
|
+
onDenied?: (c: HonoContext) => Response;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hono middleware factory.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* app.post(
|
|
35
|
+
* "/invoices/:id/approve",
|
|
36
|
+
* honoGuard(engine, "invoice:approve", "invoice", {
|
|
37
|
+
* getSubject: (c) => c.get("user"),
|
|
38
|
+
* getTenantId: (c) => c.req.header("x-tenant-id"),
|
|
39
|
+
* }),
|
|
40
|
+
* handler,
|
|
41
|
+
* );
|
|
42
|
+
*/
|
|
43
|
+
declare function honoGuard<S extends SchemaDefinition>(engine: AccessEngine<S>, action: InferAction<S>, resource: InferResource<S>, options: HonoGuardOptions<S>): HonoMiddleware;
|
|
44
|
+
|
|
45
|
+
export { type HonoGuardOptions, honoGuard };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/middleware/hono.ts
|
|
2
|
+
function honoGuard(engine, action, resource, options) {
|
|
3
|
+
return async (c, next) => {
|
|
4
|
+
const subject = options.getSubject(c);
|
|
5
|
+
if (!subject) {
|
|
6
|
+
return c.json({ error: "Unauthorized \u2014 no subject" }, 401);
|
|
7
|
+
}
|
|
8
|
+
let decision;
|
|
9
|
+
try {
|
|
10
|
+
const resourceContext = options.getResourceContext?.(c) ?? {};
|
|
11
|
+
const tenantId = options.getTenantId?.(c);
|
|
12
|
+
decision = engine.evaluate(subject, action, resource, resourceContext, tenantId);
|
|
13
|
+
} catch {
|
|
14
|
+
return c.json({ error: "Internal authorization error" }, 500);
|
|
15
|
+
}
|
|
16
|
+
if (decision.allowed) {
|
|
17
|
+
await next();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (options.onDenied) {
|
|
21
|
+
return options.onDenied(c);
|
|
22
|
+
}
|
|
23
|
+
return c.json({
|
|
24
|
+
error: "Forbidden",
|
|
25
|
+
reason: decision.reason
|
|
26
|
+
}, 403);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
honoGuard
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=hono.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/hono.ts"],"sourcesContent":["import type { AccessEngine } from \"../engine.js\";\nimport type { SchemaDefinition, InferAction, InferResource, Subject, ResourceContext } from \"../types.js\";\n\n/**\n * Minimal Hono-compatible types so we don't depend on hono at runtime.\n */\ninterface HonoContext {\n req: {\n raw: Request;\n header(name: string): string | undefined;\n param(name: string): string | undefined;\n [key: string]: unknown;\n };\n json(data: unknown, status?: number): Response;\n get<T = unknown>(key: string): T;\n set(key: string, value: unknown): void;\n [key: string]: unknown;\n}\n\ntype HonoNext = () => Promise<void>;\ntype HonoMiddleware = (c: HonoContext, next: HonoNext) => Promise<Response | void>;\n\nexport interface HonoGuardOptions<S extends SchemaDefinition> {\n /** Extract the subject from the Hono context. */\n getSubject: (c: HonoContext) => Subject<S> | undefined;\n /** Extract the resource context from the Hono context (optional). */\n getResourceContext?: (c: HonoContext) => ResourceContext;\n /** Extract the tenant ID from the Hono context (optional). */\n getTenantId?: (c: HonoContext) => string | undefined;\n /** Custom denial handler. Return a Response to override the default 403. */\n onDenied?: (c: HonoContext) => Response;\n}\n\n/**\n * Hono middleware factory.\n *\n * Usage:\n * app.post(\n * \"/invoices/:id/approve\",\n * honoGuard(engine, \"invoice:approve\", \"invoice\", {\n * getSubject: (c) => c.get(\"user\"),\n * getTenantId: (c) => c.req.header(\"x-tenant-id\"),\n * }),\n * handler,\n * );\n */\nexport function honoGuard<S extends SchemaDefinition>(\n engine: AccessEngine<S>,\n action: InferAction<S>,\n resource: InferResource<S>,\n options: HonoGuardOptions<S>,\n): HonoMiddleware {\n return async (c: HonoContext, next: HonoNext) => {\n const subject = options.getSubject(c);\n if (!subject) {\n return c.json({ error: \"Unauthorized — no subject\" }, 401);\n }\n\n let decision;\n try {\n const resourceContext = options.getResourceContext?.(c) ?? {};\n const tenantId = options.getTenantId?.(c);\n decision = engine.evaluate(subject, action, resource, resourceContext, tenantId);\n } catch {\n return c.json({ error: \"Internal authorization error\" }, 500);\n }\n\n if (decision.allowed) {\n await next();\n return;\n }\n\n if (options.onDenied) {\n return options.onDenied(c);\n }\n\n return c.json({\n error: \"Forbidden\",\n reason: decision.reason,\n }, 403);\n };\n}\n"],"mappings":";AA8CO,SAAS,UACd,QACA,QACA,UACA,SACgB;AAChB,SAAO,OAAO,GAAgB,SAAmB;AAC/C,UAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,QAAI,CAAC,SAAS;AACZ,aAAO,EAAE,KAAK,EAAE,OAAO,iCAA4B,GAAG,GAAG;AAAA,IAC3D;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,kBAAkB,QAAQ,qBAAqB,CAAC,KAAK,CAAC;AAC5D,YAAM,WAAW,QAAQ,cAAc,CAAC;AACxC,iBAAW,OAAO,SAAS,SAAS,QAAQ,UAAU,iBAAiB,QAAQ;AAAA,IACjF,QAAQ;AACN,aAAO,EAAE,KAAK,EAAE,OAAO,+BAA+B,GAAG,GAAG;AAAA,IAC9D;AAEA,QAAI,SAAS,SAAS;AACpB,YAAM,KAAK;AACX;AAAA,IACF;AAEA,QAAI,QAAQ,UAAU;AACpB,aAAO,QAAQ,SAAS,CAAC;AAAA,IAC3B;AAEA,WAAO,EAAE,KAAK;AAAA,MACZ,OAAO;AAAA,MACP,QAAQ,SAAS;AAAA,IACnB,GAAG,GAAG;AAAA,EACR;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siremzam/sentinel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "TypeScript-first, domain-driven authorization engine for modern SaaS apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
"require": "./dist/middleware/nestjs.cjs",
|
|
31
31
|
"default": "./dist/middleware/nestjs.js"
|
|
32
32
|
},
|
|
33
|
+
"./middleware/hono": {
|
|
34
|
+
"types": "./dist/middleware/hono.d.ts",
|
|
35
|
+
"import": "./dist/middleware/hono.js",
|
|
36
|
+
"require": "./dist/middleware/hono.cjs",
|
|
37
|
+
"default": "./dist/middleware/hono.js"
|
|
38
|
+
},
|
|
33
39
|
"./server": {
|
|
34
40
|
"types": "./dist/server.d.ts",
|
|
35
41
|
"import": "./dist/server.js",
|