@pramen/server 0.0.12 → 0.0.13
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/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/runtime/dispatch.d.ts +1 -1
- package/dist/runtime/dispatch.js +7 -1
- package/dist/sdk/app.js +2 -2
- package/dist/sdk/handlers.d.ts +15 -0
- package/dist/sdk/handlers.js +14 -2
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/runtime/dispatch.ts +8 -2
- package/src/sdk/app.ts +2 -2
- package/src/sdk/handlers.ts +26 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type { TriggerDef, TriggerOp } from "./sdk/schema";
|
|
|
3
3
|
export { isValidUuid } from "./sdk/uuid";
|
|
4
4
|
export type { DefaultValue, FieldType, FieldDef, EntityFields, EntityDef, SchemaDef, RelationDef, RelationDefs, BelongsToDef, HasManyDef, } from "./sdk/schema";
|
|
5
5
|
export { createApp } from "./sdk/app";
|
|
6
|
-
export { query, mutation } from "./sdk/handlers";
|
|
7
|
-
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, Tasks, TaskHandler, AppTaskMap } from "./sdk/handlers";
|
|
6
|
+
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
7
|
+
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap } from "./sdk/handlers";
|
|
8
8
|
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
|
9
9
|
export type { Action, Identity, IdentityMarker, InputMarker, Policy, PolicyRule, PolicyRules, Role, Validator, WhereRule, ConditionalFields, FieldsFn, RelationAclRule, SetValue, ResolverFn, ResolverContext, ResolverDb, } from "./sdk/acl";
|
|
10
10
|
export type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, JsonValue, ProjectedRow, RelationsOf, RelationsResult, WhereClause, WhereInput, WhereOps, } from "./sdk/infer";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { Entity, defineSchema, renamedFrom, notNull, unique, indexed, hidden, de
|
|
|
11
11
|
export { isValidUuid } from "./sdk/uuid";
|
|
12
12
|
// --- app + handlers ---
|
|
13
13
|
export { createApp } from "./sdk/app";
|
|
14
|
-
export { query, mutation } from "./sdk/handlers";
|
|
14
|
+
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
15
15
|
// --- ACL ---
|
|
16
16
|
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
|
17
17
|
export { R2Adapter, MemoryAdapter, createFiles, handleFileRequest } from "./runtime/storage";
|
|
@@ -4,7 +4,7 @@ import type { Driver } from "./driver";
|
|
|
4
4
|
import type { Kv } from "./kv";
|
|
5
5
|
import type { Files } from "../sdk/files";
|
|
6
6
|
import type { SchemaDef } from "../sdk/schema";
|
|
7
|
-
import type
|
|
7
|
+
import { type AppTaskMap, type HandlerContext, type HandlerKind, type HandlerMap, type Tasks } from "../sdk/handlers";
|
|
8
8
|
export interface DispatchResult {
|
|
9
9
|
readonly result: unknown;
|
|
10
10
|
readonly kind: HandlerKind;
|
package/dist/runtime/dispatch.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
// layer can match a mutation's writes against each subscription's reads.
|
|
10
10
|
import { Db } from "./db";
|
|
11
11
|
import { warmup } from "./acl";
|
|
12
|
-
import { BadRequest } from "./errors";
|
|
12
|
+
import { BadRequest, Forbidden } from "./errors";
|
|
13
13
|
import { enqueueTask } from "./outbox";
|
|
14
14
|
import { createMail } from "./mail";
|
|
15
|
+
import { authorizeHandler } from "../sdk/handlers";
|
|
15
16
|
/** The `ctx.tasks` facade over the outbox. `onEnqueue` lets the caller count enqueues
|
|
16
17
|
* so it can wake the drainer. */
|
|
17
18
|
export function tasksFacade(driver, onEnqueue) {
|
|
@@ -33,6 +34,11 @@ export async function dispatch(handlers, schema, driver, kv, files, env, acl, na
|
|
|
33
34
|
const handler = handlers[name];
|
|
34
35
|
if (!handler)
|
|
35
36
|
throw new BadRequest(`unknown handler: ${name}`);
|
|
37
|
+
// Per-handler authorization, enforced before any work (input parse / handler body) —
|
|
38
|
+
// gates handlers that bypass the row-ACL by touching ctx.kv/ctx.env/ctx.mail directly.
|
|
39
|
+
if (handler.auth && !authorizeHandler(handler.auth, acl.identity)) {
|
|
40
|
+
throw new Forbidden(`not authorized to call '${name}'`);
|
|
41
|
+
}
|
|
36
42
|
// Validate/parse the request input at the boundary, if the handler declares it.
|
|
37
43
|
let parsed = input;
|
|
38
44
|
if (handler.input) {
|
package/dist/sdk/app.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// const { query, mutation } = createApp(schema);
|
|
6
6
|
// const listNotes = query((ctx) => ctx.db.find({ from: "notes" })); // typed!
|
|
7
7
|
export function createApp(schema) {
|
|
8
|
-
const query = (run, opts) => ({ kind: "query", run: run, input: opts?.input, partition: opts?.partition });
|
|
9
|
-
const mutation = (run, opts) => ({ kind: "mutation", run: run, input: opts?.input, partition: opts?.partition });
|
|
8
|
+
const query = (run, opts) => ({ kind: "query", run: run, input: opts?.input, partition: opts?.partition, auth: opts?.auth });
|
|
9
|
+
const mutation = (run, opts) => ({ kind: "mutation", run: run, input: opts?.input, partition: opts?.partition, auth: opts?.auth });
|
|
10
10
|
return { schema, query, mutation };
|
|
11
11
|
}
|
package/dist/sdk/handlers.d.ts
CHANGED
|
@@ -56,6 +56,17 @@ export type TaskHandler = (ctx: HandlerContext, payload: unknown, meta: TaskMeta
|
|
|
56
56
|
/** Map of `kind` → handler. Set as `app.tasks`; drained by the DO alarm / a Cron. */
|
|
57
57
|
export type AppTaskMap = Record<string, TaskHandler>;
|
|
58
58
|
export type HandlerKind = "query" | "mutation";
|
|
59
|
+
/** Authorization required to CALL a handler, enforced BEFORE its body runs. This is
|
|
60
|
+
* distinct from the row-level ACL (which gates `ctx.db`): use it to gate handlers that
|
|
61
|
+
* touch `ctx.kv`/`ctx.env`/`ctx.mail`/`ctx.tasks` directly — those bypass the ACL, so an
|
|
62
|
+
* un-gated such handler is callable by anyone (incl. anonymous) on an open tenant. Forms:
|
|
63
|
+
* - `"authenticated"` — any non-anonymous caller (identity != null)
|
|
64
|
+
* - `string[]` — the caller must hold one of these roles
|
|
65
|
+
* - `(identity) => boolean` — a custom predicate
|
|
66
|
+
* Absent ⇒ open (the prior behavior; a `ctx.db` handler is still ACL-gated). */
|
|
67
|
+
export type HandlerAuth = "authenticated" | readonly string[] | ((identity: Identity | null) => boolean);
|
|
68
|
+
/** Evaluate a handler's `auth` requirement against the caller's identity. */
|
|
69
|
+
export declare function authorizeHandler(auth: HandlerAuth, identity: Identity | null): boolean;
|
|
59
70
|
export interface Handler<I = unknown, O = unknown> {
|
|
60
71
|
readonly kind: HandlerKind;
|
|
61
72
|
readonly run: (ctx: HandlerContext<any>, input: I) => O | Promise<O>;
|
|
@@ -66,11 +77,15 @@ export interface Handler<I = unknown, O = unknown> {
|
|
|
66
77
|
* routes the request to the matching partition-DO before dispatch. Absent ⇒ the
|
|
67
78
|
* default partition (routed to the bare tenant key). */
|
|
68
79
|
readonly partition?: string;
|
|
80
|
+
/** Optional call-authorization, enforced before the handler runs (see HandlerAuth). */
|
|
81
|
+
readonly auth?: HandlerAuth;
|
|
69
82
|
}
|
|
70
83
|
export interface HandlerOpts<I> {
|
|
71
84
|
input?: (raw: unknown) => I;
|
|
72
85
|
/** DO partition this handler runs in. Absent ⇒ the default partition. */
|
|
73
86
|
partition?: string;
|
|
87
|
+
/** Authorization to CALL this handler (see HandlerAuth) — gate non-`ctx.db` handlers. */
|
|
88
|
+
auth?: HandlerAuth;
|
|
74
89
|
}
|
|
75
90
|
export declare function query<I = unknown, O = unknown>(run: (ctx: HandlerContext, input: I) => O | Promise<O>, opts?: HandlerOpts<I>): Handler<I, O>;
|
|
76
91
|
export declare function mutation<I = unknown, O = unknown>(run: (ctx: HandlerContext, input: I) => O | Promise<O>, opts?: HandlerOpts<I>): Handler<I, O>;
|
package/dist/sdk/handlers.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
// Handler factories — `query()` and `mutation()`. A query reads; a mutation is
|
|
2
2
|
// wrapped in BEGIN/COMMIT by the dispatcher and
|
|
3
3
|
// rolls back on throw (see runtime/dispatch.ts).
|
|
4
|
+
/** Evaluate a handler's `auth` requirement against the caller's identity. */
|
|
5
|
+
export function authorizeHandler(auth, identity) {
|
|
6
|
+
if (auth === "authenticated")
|
|
7
|
+
return identity != null;
|
|
8
|
+
if (typeof auth === "function")
|
|
9
|
+
return auth(identity);
|
|
10
|
+
if (!identity)
|
|
11
|
+
return false; // a role list can never be satisfied by an anonymous caller
|
|
12
|
+
const roles = Array.isArray(identity.roles) ? identity.roles : [];
|
|
13
|
+
const held = identity.role ? [identity.role, ...roles] : roles;
|
|
14
|
+
return auth.some((r) => held.includes(r));
|
|
15
|
+
}
|
|
4
16
|
// Standalone (schema-agnostic) handler factories. Prefer createApp(schema) for a
|
|
5
17
|
// typed ctx.db; these remain for untyped/ad-hoc use.
|
|
6
18
|
export function query(run, opts) {
|
|
7
|
-
return { kind: "query", run, input: opts?.input, partition: opts?.partition };
|
|
19
|
+
return { kind: "query", run, input: opts?.input, partition: opts?.partition, auth: opts?.auth };
|
|
8
20
|
}
|
|
9
21
|
export function mutation(run, opts) {
|
|
10
|
-
return { kind: "mutation", run, input: opts?.input, partition: opts?.partition };
|
|
22
|
+
return { kind: "mutation", run, input: opts?.input, partition: opts?.partition, auth: opts?.auth };
|
|
11
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "pramen server runtime — schema, ACL, ORM, live queries, files, and the createPramen(app) factory for Cloudflare Workers + Durable Objects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/index.ts
CHANGED
|
@@ -26,8 +26,8 @@ export type {
|
|
|
26
26
|
|
|
27
27
|
// --- app + handlers ---
|
|
28
28
|
export { createApp } from "./sdk/app";
|
|
29
|
-
export { query, mutation } from "./sdk/handlers";
|
|
30
|
-
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, Tasks, TaskHandler, AppTaskMap } from "./sdk/handlers";
|
|
29
|
+
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
30
|
+
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap } from "./sdk/handlers";
|
|
31
31
|
|
|
32
32
|
// --- ACL ---
|
|
33
33
|
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
package/src/runtime/dispatch.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { Db } from "./db";
|
|
12
12
|
import { warmup, type AclContext } from "./acl";
|
|
13
|
-
import { BadRequest } from "./errors";
|
|
13
|
+
import { BadRequest, Forbidden } from "./errors";
|
|
14
14
|
import { enqueueTask, type TaskMap } from "./outbox";
|
|
15
15
|
import { createMail } from "./mail";
|
|
16
16
|
import type { Driver } from "./driver";
|
|
@@ -18,7 +18,7 @@ import type { Kv } from "./kv";
|
|
|
18
18
|
import type { Files } from "../sdk/files";
|
|
19
19
|
import type { ResolverDb } from "../sdk/acl";
|
|
20
20
|
import type { SchemaDef } from "../sdk/schema";
|
|
21
|
-
import type
|
|
21
|
+
import { authorizeHandler, type AppTaskMap, type HandlerContext, type HandlerKind, type HandlerMap, type Tasks } from "../sdk/handlers";
|
|
22
22
|
|
|
23
23
|
export interface DispatchResult {
|
|
24
24
|
readonly result: unknown;
|
|
@@ -60,6 +60,12 @@ export async function dispatch(
|
|
|
60
60
|
const handler = handlers[name];
|
|
61
61
|
if (!handler) throw new BadRequest(`unknown handler: ${name}`);
|
|
62
62
|
|
|
63
|
+
// Per-handler authorization, enforced before any work (input parse / handler body) —
|
|
64
|
+
// gates handlers that bypass the row-ACL by touching ctx.kv/ctx.env/ctx.mail directly.
|
|
65
|
+
if (handler.auth && !authorizeHandler(handler.auth, acl.identity)) {
|
|
66
|
+
throw new Forbidden(`not authorized to call '${name}'`);
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
// Validate/parse the request input at the boundary, if the handler declares it.
|
|
64
70
|
let parsed = input;
|
|
65
71
|
if (handler.input) {
|
package/src/sdk/app.ts
CHANGED
|
@@ -14,12 +14,12 @@ export function createApp<S extends SchemaDef>(schema: S) {
|
|
|
14
14
|
const query = <I = unknown, O = unknown>(
|
|
15
15
|
run: (ctx: Ctx, input: I) => O | Promise<O>,
|
|
16
16
|
opts?: HandlerOpts<I>,
|
|
17
|
-
): Handler<I, O> => ({ kind: "query", run: run as Handler<I, O>["run"], input: opts?.input, partition: opts?.partition });
|
|
17
|
+
): Handler<I, O> => ({ kind: "query", run: run as Handler<I, O>["run"], input: opts?.input, partition: opts?.partition, auth: opts?.auth });
|
|
18
18
|
|
|
19
19
|
const mutation = <I = unknown, O = unknown>(
|
|
20
20
|
run: (ctx: Ctx, input: I) => O | Promise<O>,
|
|
21
21
|
opts?: HandlerOpts<I>,
|
|
22
|
-
): Handler<I, O> => ({ kind: "mutation", run: run as Handler<I, O>["run"], input: opts?.input, partition: opts?.partition });
|
|
22
|
+
): Handler<I, O> => ({ kind: "mutation", run: run as Handler<I, O>["run"], input: opts?.input, partition: opts?.partition, auth: opts?.auth });
|
|
23
23
|
|
|
24
24
|
return { schema, query, mutation };
|
|
25
25
|
}
|
package/src/sdk/handlers.ts
CHANGED
|
@@ -62,6 +62,26 @@ export type AppTaskMap = Record<string, TaskHandler>;
|
|
|
62
62
|
|
|
63
63
|
export type HandlerKind = "query" | "mutation";
|
|
64
64
|
|
|
65
|
+
/** Authorization required to CALL a handler, enforced BEFORE its body runs. This is
|
|
66
|
+
* distinct from the row-level ACL (which gates `ctx.db`): use it to gate handlers that
|
|
67
|
+
* touch `ctx.kv`/`ctx.env`/`ctx.mail`/`ctx.tasks` directly — those bypass the ACL, so an
|
|
68
|
+
* un-gated such handler is callable by anyone (incl. anonymous) on an open tenant. Forms:
|
|
69
|
+
* - `"authenticated"` — any non-anonymous caller (identity != null)
|
|
70
|
+
* - `string[]` — the caller must hold one of these roles
|
|
71
|
+
* - `(identity) => boolean` — a custom predicate
|
|
72
|
+
* Absent ⇒ open (the prior behavior; a `ctx.db` handler is still ACL-gated). */
|
|
73
|
+
export type HandlerAuth = "authenticated" | readonly string[] | ((identity: Identity | null) => boolean);
|
|
74
|
+
|
|
75
|
+
/** Evaluate a handler's `auth` requirement against the caller's identity. */
|
|
76
|
+
export function authorizeHandler(auth: HandlerAuth, identity: Identity | null): boolean {
|
|
77
|
+
if (auth === "authenticated") return identity != null;
|
|
78
|
+
if (typeof auth === "function") return auth(identity);
|
|
79
|
+
if (!identity) return false; // a role list can never be satisfied by an anonymous caller
|
|
80
|
+
const roles = Array.isArray(identity.roles) ? identity.roles : [];
|
|
81
|
+
const held = identity.role ? [identity.role, ...roles] : roles;
|
|
82
|
+
return auth.some((r) => held.includes(r));
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
export interface Handler<I = unknown, O = unknown> {
|
|
66
86
|
readonly kind: HandlerKind;
|
|
67
87
|
// Stored handlers are schema-agnostic; createApp() binds the typed surface.
|
|
@@ -74,12 +94,16 @@ export interface Handler<I = unknown, O = unknown> {
|
|
|
74
94
|
* routes the request to the matching partition-DO before dispatch. Absent ⇒ the
|
|
75
95
|
* default partition (routed to the bare tenant key). */
|
|
76
96
|
readonly partition?: string;
|
|
97
|
+
/** Optional call-authorization, enforced before the handler runs (see HandlerAuth). */
|
|
98
|
+
readonly auth?: HandlerAuth;
|
|
77
99
|
}
|
|
78
100
|
|
|
79
101
|
export interface HandlerOpts<I> {
|
|
80
102
|
input?: (raw: unknown) => I;
|
|
81
103
|
/** DO partition this handler runs in. Absent ⇒ the default partition. */
|
|
82
104
|
partition?: string;
|
|
105
|
+
/** Authorization to CALL this handler (see HandlerAuth) — gate non-`ctx.db` handlers. */
|
|
106
|
+
auth?: HandlerAuth;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
// Standalone (schema-agnostic) handler factories. Prefer createApp(schema) for a
|
|
@@ -88,14 +112,14 @@ export function query<I = unknown, O = unknown>(
|
|
|
88
112
|
run: (ctx: HandlerContext, input: I) => O | Promise<O>,
|
|
89
113
|
opts?: HandlerOpts<I>,
|
|
90
114
|
): Handler<I, O> {
|
|
91
|
-
return { kind: "query", run, input: opts?.input, partition: opts?.partition };
|
|
115
|
+
return { kind: "query", run, input: opts?.input, partition: opts?.partition, auth: opts?.auth };
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
export function mutation<I = unknown, O = unknown>(
|
|
95
119
|
run: (ctx: HandlerContext, input: I) => O | Promise<O>,
|
|
96
120
|
opts?: HandlerOpts<I>,
|
|
97
121
|
): Handler<I, O> {
|
|
98
|
-
return { kind: "mutation", run, input: opts?.input, partition: opts?.partition };
|
|
122
|
+
return { kind: "mutation", run, input: opts?.input, partition: opts?.partition, auth: opts?.auth };
|
|
99
123
|
}
|
|
100
124
|
|
|
101
125
|
// Registry of handlers keyed by RPC name. Uses `any` for the per-handler input/
|