@pramen/server 0.0.45 → 0.0.47
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/acl.d.ts +1 -1
- package/dist/runtime/acl.js +10 -7
- package/dist/runtime/storage.js +0 -0
- package/dist/sdk/acl.d.ts +21 -0
- package/dist/sdk/acl.js +25 -1
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/runtime/acl.ts +9 -6
- package/src/runtime/storage.ts +0 -0
- package/src/sdk/acl.ts +33 -1
package/dist/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export type { DefaultValue, FieldType, FieldDef, EntityFields, EntityDef, Schema
|
|
|
5
5
|
export { createApp } from "./sdk/app";
|
|
6
6
|
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
7
7
|
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
8
|
-
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
|
9
|
-
export type { Action, Identity, IdentityMarker, InputMarker, Policy, PolicyRule, PolicyRules, Role, Validator, WhereRule, ConditionalFields, FieldsFn, RelationAclRule, SetValue, ResolverFn, ResolverContext, ResolverDb, } from "./sdk/acl";
|
|
8
|
+
export { $identity, $input, $now, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker, isNowMarker } from "./sdk/acl";
|
|
9
|
+
export type { Action, Identity, IdentityMarker, InputMarker, NowMarker, 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";
|
|
11
11
|
export { Kv, denySession, allowSession, isSessionDenied } from "./runtime/kv";
|
|
12
12
|
export type { FileRef, Files, SignUploadOpts, SignDownloadOpts, HeadResult } from "./sdk/files";
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { isValidUuid } from "./sdk/uuid";
|
|
|
13
13
|
export { createApp } from "./sdk/app";
|
|
14
14
|
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
15
15
|
// --- ACL ---
|
|
16
|
-
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
|
16
|
+
export { $identity, $input, $now, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker, isNowMarker } from "./sdk/acl";
|
|
17
17
|
// --- kv (ctx.kv) + session denylist (hard token revocation) ---
|
|
18
18
|
export { Kv, denySession, allowSession, isSessionDenied } from "./runtime/kv";
|
|
19
19
|
export { R2Adapter, MemoryAdapter, createFiles, handleFileRequest } from "./runtime/storage";
|
package/dist/runtime/acl.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare const ALLOW_ALL: Scope;
|
|
|
63
63
|
export declare const MAX_REL_DEPTH = 5;
|
|
64
64
|
/** Compile a where-rule (user query or policy) into a SqlExpr. Plain columns go
|
|
65
65
|
* through the marker-resolving compiler; relation keys (`{ rel: { … } }`) become
|
|
66
|
-
* security-scoped subqueries. Supports operators, AND/OR, and $identity/$input
|
|
66
|
+
* security-scoped subqueries. Supports operators, AND/OR, and $identity/$input/$now
|
|
67
67
|
* markers; an unresolvable marker makes its branch match nothing. Schema-less
|
|
68
68
|
* contexts (no relations) behave exactly like the flat compiler.
|
|
69
69
|
*
|
package/dist/runtime/acl.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Given an identity + (entity, action), resolves a Scope: whether access is
|
|
3
3
|
// granted, the row-level predicate to merge into the query, and any field
|
|
4
4
|
// restriction. Deny-by-default; grants OR-merge across the identity's roles.
|
|
5
|
-
import { deny, isAllow, isDeny, isIdentityMarker, isInputMarker, isResolver, } from "../sdk/acl";
|
|
5
|
+
import { deny, isAllow, isDeny, isIdentityMarker, isInputMarker, isNowMarker, isResolver, } from "../sdk/acl";
|
|
6
6
|
import { and, compileWhere, evalExpr, FALSE, not, or, TRUE } from "./read-engine";
|
|
7
7
|
import { BadRequest, PramenError } from "./errors";
|
|
8
8
|
export class AclDenied extends PramenError {
|
|
@@ -85,10 +85,13 @@ function getPath(obj, path) {
|
|
|
85
85
|
return path.split(".").reduce((acc, seg) => (acc == null ? undefined : acc[seg]), obj ?? undefined);
|
|
86
86
|
}
|
|
87
87
|
const UNRESOLVED = Symbol("unresolved");
|
|
88
|
-
// Resolve a value that may be an $identity marker (against the caller)
|
|
89
|
-
// $input marker (against the request input — a capability/by-key grant)
|
|
90
|
-
//
|
|
88
|
+
// Resolve a value that may be an $identity marker (against the caller), an
|
|
89
|
+
// $input marker (against the request input — a capability/by-key grant), or a
|
|
90
|
+
// $now marker (the evaluation instant). An unresolvable marker yields UNRESOLVED,
|
|
91
|
+
// which makes its rule match nothing. $now always resolves.
|
|
91
92
|
function resolveValue(v, identity, input) {
|
|
93
|
+
if (isNowMarker(v))
|
|
94
|
+
return new Date().toISOString();
|
|
92
95
|
if (isIdentityMarker(v)) {
|
|
93
96
|
const value = getPath(identity, v.path);
|
|
94
97
|
return value === undefined ? UNRESOLVED : value;
|
|
@@ -99,7 +102,7 @@ function resolveValue(v, identity, input) {
|
|
|
99
102
|
}
|
|
100
103
|
return v;
|
|
101
104
|
}
|
|
102
|
-
// Resolve every $identity/$input marker in a SINGLE level of a where-rule (bare
|
|
105
|
+
// Resolve every $identity/$input/$now marker in a SINGLE level of a where-rule (bare
|
|
103
106
|
// values, operator objects, in/notIn arrays). AND/OR groups are split off by
|
|
104
107
|
// `compileScopedWhere` before this runs, so this only ever sees plain columns.
|
|
105
108
|
// Returns a plain WhereInput, or null if any marker is unresolvable — in which
|
|
@@ -111,7 +114,7 @@ function resolveValue(v, identity, input) {
|
|
|
111
114
|
function resolveMarkers(rule, identity, input) {
|
|
112
115
|
const out = {};
|
|
113
116
|
for (const [key, v] of Object.entries(rule)) {
|
|
114
|
-
const isMarker = isIdentityMarker(v) || isInputMarker(v);
|
|
117
|
+
const isMarker = isIdentityMarker(v) || isInputMarker(v) || isNowMarker(v);
|
|
115
118
|
if (v !== null && typeof v === "object" && !isMarker && !Array.isArray(v)) {
|
|
116
119
|
const ops = {};
|
|
117
120
|
for (const [op, val] of Object.entries(v)) {
|
|
@@ -241,7 +244,7 @@ function relationPredicate(rel, nested, parentEntity, ctx, depth) {
|
|
|
241
244
|
}
|
|
242
245
|
/** Compile a where-rule (user query or policy) into a SqlExpr. Plain columns go
|
|
243
246
|
* through the marker-resolving compiler; relation keys (`{ rel: { … } }`) become
|
|
244
|
-
* security-scoped subqueries. Supports operators, AND/OR, and $identity/$input
|
|
247
|
+
* security-scoped subqueries. Supports operators, AND/OR, and $identity/$input/$now
|
|
245
248
|
* markers; an unresolvable marker makes its branch match nothing. Schema-less
|
|
246
249
|
* contexts (no relations) behave exactly like the flat compiler.
|
|
247
250
|
*
|
package/dist/runtime/storage.js
CHANGED
|
Binary file
|
package/dist/sdk/acl.d.ts
CHANGED
|
@@ -29,6 +29,27 @@ export interface InputMarker {
|
|
|
29
29
|
* to enumerate. An absent input value makes the rule match nothing (safe deny). */
|
|
30
30
|
export declare function $input(path: string): InputMarker;
|
|
31
31
|
export declare function isInputMarker(v: unknown): v is InputMarker;
|
|
32
|
+
declare const NOW_MARKER: unique symbol;
|
|
33
|
+
export interface NowMarker {
|
|
34
|
+
readonly [NOW_MARKER]: true;
|
|
35
|
+
}
|
|
36
|
+
/** The current UTC instant in a policy `where`, resolved per request, as an ISO-8601
|
|
37
|
+
* string with a `Z` suffix — exactly what `new Date().toISOString()` produces.
|
|
38
|
+
*
|
|
39
|
+
* This is what makes a time boundary an ACL predicate rather than a filter every
|
|
40
|
+
* handler has to remember: `{ publishedAt: { lte: $now() } }` hides a row scheduled
|
|
41
|
+
* for the future from every caller the policy governs, not merely from the queries
|
|
42
|
+
* that thought to ask. `{ publishedAt: { isNull: false } }` cannot express it — a
|
|
43
|
+
* future timestamp is non-null, so a scheduled row would be readable the moment it
|
|
44
|
+
* is saved.
|
|
45
|
+
*
|
|
46
|
+
* Comparison is lexicographic TEXT, which is exact for ISO-8601 UTC but NOT
|
|
47
|
+
* cross-format: `expr.now()` defaults store `'YYYY-MM-DD HH:MM:SS'` (space, no `Z`)
|
|
48
|
+
* and will not compare correctly against this. Store the column with
|
|
49
|
+
* `toISOString()` — as the CMS `publish` field does — or compare it against
|
|
50
|
+
* `expr.now()`-shaped values only. */
|
|
51
|
+
export declare function $now(): NowMarker;
|
|
52
|
+
export declare function isNowMarker(v: unknown): v is NowMarker;
|
|
32
53
|
export interface AllowMarker {
|
|
33
54
|
readonly kind: "allow";
|
|
34
55
|
}
|
package/dist/sdk/acl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ACL primitives — the portable definition layer: role(), policy(), allow(),
|
|
2
|
-
// deny(), $identity(). Resolution semantics live in runtime/acl.ts.
|
|
2
|
+
// deny(), $identity(), $now(). Resolution semantics live in runtime/acl.ts.
|
|
3
3
|
//
|
|
4
4
|
// Model: an Identity carries one or more roles. A policy grants a (role) access
|
|
5
5
|
// to an (entity, action), optionally restricted by a row-level `where` predicate
|
|
@@ -27,6 +27,30 @@ export function $input(path) {
|
|
|
27
27
|
export function isInputMarker(v) {
|
|
28
28
|
return typeof v === "object" && v !== null && v[INPUT_MARKER] === true;
|
|
29
29
|
}
|
|
30
|
+
// --- $now markers: the request's evaluation instant inside a where rule, for a
|
|
31
|
+
// time-boxed grant (scheduled publication, an expiring share link).
|
|
32
|
+
const NOW_MARKER = Symbol.for("pramen.nowMarker");
|
|
33
|
+
/** The current UTC instant in a policy `where`, resolved per request, as an ISO-8601
|
|
34
|
+
* string with a `Z` suffix — exactly what `new Date().toISOString()` produces.
|
|
35
|
+
*
|
|
36
|
+
* This is what makes a time boundary an ACL predicate rather than a filter every
|
|
37
|
+
* handler has to remember: `{ publishedAt: { lte: $now() } }` hides a row scheduled
|
|
38
|
+
* for the future from every caller the policy governs, not merely from the queries
|
|
39
|
+
* that thought to ask. `{ publishedAt: { isNull: false } }` cannot express it — a
|
|
40
|
+
* future timestamp is non-null, so a scheduled row would be readable the moment it
|
|
41
|
+
* is saved.
|
|
42
|
+
*
|
|
43
|
+
* Comparison is lexicographic TEXT, which is exact for ISO-8601 UTC but NOT
|
|
44
|
+
* cross-format: `expr.now()` defaults store `'YYYY-MM-DD HH:MM:SS'` (space, no `Z`)
|
|
45
|
+
* and will not compare correctly against this. Store the column with
|
|
46
|
+
* `toISOString()` — as the CMS `publish` field does — or compare it against
|
|
47
|
+
* `expr.now()`-shaped values only. */
|
|
48
|
+
export function $now() {
|
|
49
|
+
return { [NOW_MARKER]: true };
|
|
50
|
+
}
|
|
51
|
+
export function isNowMarker(v) {
|
|
52
|
+
return typeof v === "object" && v !== null && v[NOW_MARKER] === true;
|
|
53
|
+
}
|
|
30
54
|
export function allow() {
|
|
31
55
|
return { kind: "allow" };
|
|
32
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
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
|
@@ -34,12 +34,13 @@ export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
|
34
34
|
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
35
35
|
|
|
36
36
|
// --- ACL ---
|
|
37
|
-
export { $identity, $input, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker } from "./sdk/acl";
|
|
37
|
+
export { $identity, $input, $now, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker, isNowMarker } from "./sdk/acl";
|
|
38
38
|
export type {
|
|
39
39
|
Action,
|
|
40
40
|
Identity,
|
|
41
41
|
IdentityMarker,
|
|
42
42
|
InputMarker,
|
|
43
|
+
NowMarker,
|
|
43
44
|
Policy,
|
|
44
45
|
PolicyRule,
|
|
45
46
|
PolicyRules,
|
package/src/runtime/acl.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isDeny,
|
|
21
21
|
isIdentityMarker,
|
|
22
22
|
isInputMarker,
|
|
23
|
+
isNowMarker,
|
|
23
24
|
isResolver,
|
|
24
25
|
} from "../sdk/acl";
|
|
25
26
|
import { and, compileWhere, evalExpr, FALSE, not, or, TRUE, type SqlExpr } from "./read-engine";
|
|
@@ -172,10 +173,12 @@ function getPath(obj: unknown, path: string): unknown {
|
|
|
172
173
|
|
|
173
174
|
const UNRESOLVED = Symbol("unresolved");
|
|
174
175
|
|
|
175
|
-
// Resolve a value that may be an $identity marker (against the caller)
|
|
176
|
-
// $input marker (against the request input — a capability/by-key grant)
|
|
177
|
-
//
|
|
176
|
+
// Resolve a value that may be an $identity marker (against the caller), an
|
|
177
|
+
// $input marker (against the request input — a capability/by-key grant), or a
|
|
178
|
+
// $now marker (the evaluation instant). An unresolvable marker yields UNRESOLVED,
|
|
179
|
+
// which makes its rule match nothing. $now always resolves.
|
|
178
180
|
function resolveValue(v: unknown, identity: Identity | null, input: unknown): unknown {
|
|
181
|
+
if (isNowMarker(v)) return new Date().toISOString();
|
|
179
182
|
if (isIdentityMarker(v)) {
|
|
180
183
|
const value = getPath(identity, v.path);
|
|
181
184
|
return value === undefined ? UNRESOLVED : value;
|
|
@@ -187,7 +190,7 @@ function resolveValue(v: unknown, identity: Identity | null, input: unknown): un
|
|
|
187
190
|
return v;
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
// Resolve every $identity/$input marker in a SINGLE level of a where-rule (bare
|
|
193
|
+
// Resolve every $identity/$input/$now marker in a SINGLE level of a where-rule (bare
|
|
191
194
|
// values, operator objects, in/notIn arrays). AND/OR groups are split off by
|
|
192
195
|
// `compileScopedWhere` before this runs, so this only ever sees plain columns.
|
|
193
196
|
// Returns a plain WhereInput, or null if any marker is unresolvable — in which
|
|
@@ -199,7 +202,7 @@ function resolveValue(v: unknown, identity: Identity | null, input: unknown): un
|
|
|
199
202
|
function resolveMarkers(rule: Record<string, unknown>, identity: Identity | null, input: unknown): Record<string, unknown> | null {
|
|
200
203
|
const out: Record<string, unknown> = {};
|
|
201
204
|
for (const [key, v] of Object.entries(rule)) {
|
|
202
|
-
const isMarker = isIdentityMarker(v) || isInputMarker(v);
|
|
205
|
+
const isMarker = isIdentityMarker(v) || isInputMarker(v) || isNowMarker(v);
|
|
203
206
|
if (v !== null && typeof v === "object" && !isMarker && !Array.isArray(v)) {
|
|
204
207
|
const ops: Record<string, unknown> = {};
|
|
205
208
|
for (const [op, val] of Object.entries(v as Record<string, unknown>)) {
|
|
@@ -317,7 +320,7 @@ function relationPredicate(rel: RelationDef, nested: unknown, parentEntity: stri
|
|
|
317
320
|
|
|
318
321
|
/** Compile a where-rule (user query or policy) into a SqlExpr. Plain columns go
|
|
319
322
|
* through the marker-resolving compiler; relation keys (`{ rel: { … } }`) become
|
|
320
|
-
* security-scoped subqueries. Supports operators, AND/OR, and $identity/$input
|
|
323
|
+
* security-scoped subqueries. Supports operators, AND/OR, and $identity/$input/$now
|
|
321
324
|
* markers; an unresolvable marker makes its branch match nothing. Schema-less
|
|
322
325
|
* contexts (no relations) behave exactly like the flat compiler.
|
|
323
326
|
*
|
package/src/runtime/storage.ts
CHANGED
|
Binary file
|
package/src/sdk/acl.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ACL primitives — the portable definition layer: role(), policy(), allow(),
|
|
2
|
-
// deny(), $identity(). Resolution semantics live in runtime/acl.ts.
|
|
2
|
+
// deny(), $identity(), $now(). Resolution semantics live in runtime/acl.ts.
|
|
3
3
|
//
|
|
4
4
|
// Model: an Identity carries one or more roles. A policy grants a (role) access
|
|
5
5
|
// to an (entity, action), optionally restricted by a row-level `where` predicate
|
|
@@ -60,6 +60,38 @@ export function isInputMarker(v: unknown): v is InputMarker {
|
|
|
60
60
|
return typeof v === "object" && v !== null && (v as Record<symbol, unknown>)[INPUT_MARKER] === true;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// --- $now markers: the request's evaluation instant inside a where rule, for a
|
|
64
|
+
// time-boxed grant (scheduled publication, an expiring share link).
|
|
65
|
+
|
|
66
|
+
const NOW_MARKER = Symbol.for("pramen.nowMarker");
|
|
67
|
+
|
|
68
|
+
export interface NowMarker {
|
|
69
|
+
readonly [NOW_MARKER]: true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The current UTC instant in a policy `where`, resolved per request, as an ISO-8601
|
|
73
|
+
* string with a `Z` suffix — exactly what `new Date().toISOString()` produces.
|
|
74
|
+
*
|
|
75
|
+
* This is what makes a time boundary an ACL predicate rather than a filter every
|
|
76
|
+
* handler has to remember: `{ publishedAt: { lte: $now() } }` hides a row scheduled
|
|
77
|
+
* for the future from every caller the policy governs, not merely from the queries
|
|
78
|
+
* that thought to ask. `{ publishedAt: { isNull: false } }` cannot express it — a
|
|
79
|
+
* future timestamp is non-null, so a scheduled row would be readable the moment it
|
|
80
|
+
* is saved.
|
|
81
|
+
*
|
|
82
|
+
* Comparison is lexicographic TEXT, which is exact for ISO-8601 UTC but NOT
|
|
83
|
+
* cross-format: `expr.now()` defaults store `'YYYY-MM-DD HH:MM:SS'` (space, no `Z`)
|
|
84
|
+
* and will not compare correctly against this. Store the column with
|
|
85
|
+
* `toISOString()` — as the CMS `publish` field does — or compare it against
|
|
86
|
+
* `expr.now()`-shaped values only. */
|
|
87
|
+
export function $now(): NowMarker {
|
|
88
|
+
return { [NOW_MARKER]: true };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isNowMarker(v: unknown): v is NowMarker {
|
|
92
|
+
return typeof v === "object" && v !== null && (v as Record<symbol, unknown>)[NOW_MARKER] === true;
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
// --- allow / deny markers ---
|
|
64
96
|
|
|
65
97
|
export interface AllowMarker {
|