@pramen/server 0.0.46 → 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 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";
@@ -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
  *
@@ -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) or an
89
- // $input marker (against the request input — a capability/by-key grant). An
90
- // unresolvable marker yields UNRESOLVED, which makes its rule match nothing.
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/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.46",
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,
@@ -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) or an
176
- // $input marker (against the request input — a capability/by-key grant). An
177
- // unresolvable marker yields UNRESOLVED, which makes its rule match nothing.
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/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 {