@pramen/server 0.0.48 → 0.0.50
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/auth.d.ts +4 -3
- package/dist/cli.js +17 -30
- package/dist/dev.d.ts +1 -0
- package/dist/dev.js +9 -0
- package/dist/durable-object.d.ts +1 -0
- package/dist/durable-object.js +13 -6
- package/dist/index.d.ts +7 -5
- package/dist/index.js +6 -2
- package/dist/pramen.d.ts +4 -2
- package/dist/runtime/acl.d.ts +14 -5
- package/dist/runtime/acl.js +1 -5
- package/dist/runtime/db.d.ts +3 -2
- package/dist/runtime/dev-token.d.ts +4 -0
- package/dist/runtime/dev-token.js +34 -0
- package/dist/runtime/dispatch.d.ts +3 -1
- package/dist/runtime/dispatch.js +2 -0
- package/dist/runtime/driver.d.ts +8 -5
- package/dist/runtime/errors.d.ts +6 -0
- package/dist/runtime/errors.js +8 -0
- package/dist/runtime/mail.d.ts +2 -1
- package/dist/runtime/protocol.d.ts +4 -3
- package/dist/runtime/queue-consumer.d.ts +4 -2
- package/dist/runtime/queue.d.ts +3 -2
- package/dist/runtime/read-engine.d.ts +11 -9
- package/dist/runtime/read-engine.js +4 -1
- package/dist/runtime/registry.d.ts +4 -1
- package/dist/runtime/registry.js +0 -3
- package/dist/runtime/schema-diff.d.ts +6 -6
- package/dist/runtime/schema-diff.js +4 -4
- package/dist/runtime/storage.d.ts +4 -4
- package/dist/runtime/storage.js +8 -63
- package/dist/runtime/token.d.ts +22 -0
- package/dist/runtime/token.js +88 -0
- package/dist/sdk/acl.d.ts +18 -11
- package/dist/sdk/handlers.d.ts +16 -3
- package/dist/sdk/infer.d.ts +17 -0
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +20 -10
- package/package.json +13 -3
- package/src/auth.ts +7 -6
- package/src/cli.ts +22 -36
- package/src/dev.ts +10 -0
- package/src/durable-object.ts +18 -9
- package/src/index.ts +14 -4
- package/src/pramen.ts +4 -2
- package/src/runtime/acl.ts +45 -31
- package/src/runtime/db.ts +14 -12
- package/src/runtime/dev-token.ts +36 -0
- package/src/runtime/dispatch.ts +7 -3
- package/src/runtime/driver.ts +11 -7
- package/src/runtime/errors.ts +9 -0
- package/src/runtime/mail.ts +2 -1
- package/src/runtime/migrate.ts +2 -1
- package/src/runtime/outbox.ts +2 -1
- package/src/runtime/protocol.ts +5 -3
- package/src/runtime/queue-consumer.ts +4 -2
- package/src/runtime/queue.ts +4 -2
- package/src/runtime/read-engine.ts +27 -21
- package/src/runtime/registry.ts +5 -1
- package/src/runtime/schema-diff.ts +14 -14
- package/src/runtime/storage.ts +14 -62
- package/src/runtime/token.ts +94 -0
- package/src/sdk/acl.ts +29 -14
- package/src/sdk/handlers.ts +17 -3
- package/src/sdk/infer.ts +21 -0
- package/src/worker.ts +24 -11
package/dist/worker.js
CHANGED
|
@@ -18,6 +18,10 @@ import { Kv, isSessionDenied } from "./runtime/kv";
|
|
|
18
18
|
import { listDOs, partitionDoName } from "./runtime/registry";
|
|
19
19
|
import { createFiles, handleFileRequest, handleMediaRequest, R2Adapter } from "./runtime/storage";
|
|
20
20
|
import { DEFAULT_PARTITION, partitionsOf } from "./sdk/schema";
|
|
21
|
+
/** Widen the closed `Env` interface to the open `EnvBag` handlers and services see.
|
|
22
|
+
* Spreading yields an anonymous object type, which TypeScript gives an implicit index
|
|
23
|
+
* signature — so this needs no type assertion. */
|
|
24
|
+
const envBag = (env) => ({ ...env });
|
|
21
25
|
/** The secret used to sign/verify file tokens — a dedicated FILES_SECRET if set,
|
|
22
26
|
* else AUTH_SECRET (so HS256 setups work out of the box). */
|
|
23
27
|
const filesSecret = (env) => env.FILES_SECRET || env.AUTH_SECRET;
|
|
@@ -172,7 +176,10 @@ export function makeWorker(app) {
|
|
|
172
176
|
const files = createFiles({ tenant: "main", secret: filesSecret(env), adapter: new R2Adapter(env.FILES) });
|
|
173
177
|
const db = new Db(driver, { acl: d1Acl, identity, system: true, schema: app.schema, suppressTriggers: true }, app.schema);
|
|
174
178
|
const kv = new Kv(env.KV);
|
|
175
|
-
|
|
179
|
+
const bag = envBag(env);
|
|
180
|
+
// The D1 store is not per-tenant addressed (one shared database, no DO), so the
|
|
181
|
+
// task context runs as the default tenant — matching the `files` scope just above.
|
|
182
|
+
return { db, kv, files, env: bag, identity, tenant: "main", store: "d1", tasks: tasksFacade(driver), mail: createMail(bag, kv), queue: createQueue(bag) };
|
|
176
183
|
};
|
|
177
184
|
/** Drain the D1 outbox in the Worker (no DO/alarm on this path) — called by the
|
|
178
185
|
* /admin/tasks/drain route with `x-pramen-store: d1`, and by `scheduled()` (Cron). */
|
|
@@ -216,7 +223,7 @@ export function makeWorker(app) {
|
|
|
216
223
|
for (const r of app.routes ?? []) {
|
|
217
224
|
if (request.method === r.method && url.pathname === r.path) {
|
|
218
225
|
const routeCtx = { callPrivileged: (opts) => callPrivileged(env, opts) };
|
|
219
|
-
return r.handler(request, env, routeCtx);
|
|
226
|
+
return r.handler(request, envBag(env), routeCtx);
|
|
220
227
|
}
|
|
221
228
|
}
|
|
222
229
|
// CORS (opt-in via CORS_ORIGINS) for cross-origin browser clients. Answer the
|
|
@@ -396,9 +403,10 @@ export function makeWorker(app) {
|
|
|
396
403
|
}
|
|
397
404
|
// (isLive is excluded by useD1Store — live always routes to the DO below.)
|
|
398
405
|
const name = url.pathname.replace(/^\/rpc\//, "");
|
|
399
|
-
|
|
406
|
+
// The RPC body is JSON — parse it into the domain type once, here at the boundary.
|
|
407
|
+
let input = null;
|
|
400
408
|
if (request.method === "POST")
|
|
401
|
-
input = await request.json().catch(() =>
|
|
409
|
+
input = ((await request.json().catch(() => null)) ?? null);
|
|
402
410
|
// Pick where the D1 session may start its first read. A mutation ALWAYS pins the
|
|
403
411
|
// primary (`first-primary` is a superset of read-your-writes) so a read-modify-write
|
|
404
412
|
// can't run off a lagging replica — an inbound bookmark must not widen that window.
|
|
@@ -414,10 +422,12 @@ export function makeWorker(app) {
|
|
|
414
422
|
start = "first-unconstrained";
|
|
415
423
|
const driver = new D1Driver(env.DB, { start });
|
|
416
424
|
const files = createFiles({ tenant, secret: filesSecret(env), adapter: new R2Adapter(env.FILES) });
|
|
417
|
-
const
|
|
425
|
+
const bag = envBag(env);
|
|
418
426
|
try {
|
|
419
427
|
await ensureD1Migrated(driver, env.PRAMEN_ALLOW_DESTRUCTIVE === "true");
|
|
420
|
-
|
|
428
|
+
// `tenant` matters here: a handler minting a tenant-scoped capability (a signed
|
|
429
|
+
// preview link) would otherwise stamp it "main" while reading acme's rows.
|
|
430
|
+
const { result, enqueued } = await dispatch(app.handlers, app.schema, driver, new Kv(env.KV), files, bag, { acl: d1Acl, identity, tenant, store: "d1" }, name, input);
|
|
421
431
|
// Kick an immediate drain in the request tail when this handler enqueued tasks
|
|
422
432
|
// (e.g. sendMagicLinkEmail). Without this, tasks wait for the next Cron trigger
|
|
423
433
|
// — up to a full minute. `waitUntil` lets the response return now while the
|
|
@@ -488,13 +498,13 @@ export function makeWorker(app) {
|
|
|
488
498
|
// handler (ACK on success / RETRY on throw, per message). A consumer is Worker-level
|
|
489
499
|
// (no tenant DO): its ctx carries env/kv/mail/queue + callPrivileged to reach a DO.
|
|
490
500
|
async queue(batch, env) {
|
|
491
|
-
const
|
|
501
|
+
const bag = envBag(env);
|
|
492
502
|
const kv = new Kv(env.KV);
|
|
493
503
|
const ctx = {
|
|
494
|
-
env:
|
|
504
|
+
env: bag,
|
|
495
505
|
kv,
|
|
496
|
-
mail: createMail(
|
|
497
|
-
queue: createQueue(
|
|
506
|
+
mail: createMail(bag, kv),
|
|
507
|
+
queue: createQueue(bag),
|
|
498
508
|
callPrivileged: (opts) => callPrivileged(env, opts),
|
|
499
509
|
};
|
|
500
510
|
await dispatchQueueBatch(app.queues ?? {}, ctx, batch);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/server",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "pramen server runtime
|
|
3
|
+
"version": "0.0.50",
|
|
4
|
+
"description": "pramen server runtime \u2014 schema, ACL, ORM, live queries, files, and the createPramen(app) factory for Cloudflare Workers + Durable Objects.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
"workerd": "./src/worker-entry.ts",
|
|
27
27
|
"types": "./dist/worker-entry.d.ts",
|
|
28
28
|
"default": "./dist/worker-entry.js"
|
|
29
|
+
},
|
|
30
|
+
"./dev": {
|
|
31
|
+
"development": "./src/dev.ts",
|
|
32
|
+
"bun": "./src/dev.ts",
|
|
33
|
+
"workerd": "./src/dev.ts",
|
|
34
|
+
"types": "./dist/dev.d.ts",
|
|
35
|
+
"default": "./dist/dev.js"
|
|
29
36
|
}
|
|
30
37
|
},
|
|
31
38
|
"main": "./dist/index.js",
|
|
@@ -33,7 +40,10 @@
|
|
|
33
40
|
"bin": {
|
|
34
41
|
"pramen": "./dist/cli.js"
|
|
35
42
|
},
|
|
36
|
-
"files": [
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"src"
|
|
46
|
+
],
|
|
37
47
|
"scripts": {
|
|
38
48
|
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
39
49
|
},
|
package/src/auth.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// non-standard claims pass through.
|
|
12
12
|
|
|
13
13
|
import type { Identity } from "./sdk/acl";
|
|
14
|
+
import type { JsonObject } from "./sdk/infer";
|
|
14
15
|
|
|
15
16
|
const STANDARD_CLAIMS = new Set(["exp", "iat", "nbf", "iss", "aud", "jti", "sub", "role", "roles", "userId"]);
|
|
16
17
|
|
|
@@ -34,7 +35,7 @@ interface JwtHeader {
|
|
|
34
35
|
/** Verifies a JWT and returns its claims, or null if invalid. Implementations
|
|
35
36
|
* differ only in how they verify the signature. */
|
|
36
37
|
export interface VerifyStrategy {
|
|
37
|
-
verify(token: string): Promise<
|
|
38
|
+
verify(token: string): Promise<JsonObject | null>;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/** Verify a signature over `${header}.${payload}` for the parsed header. */
|
|
@@ -69,7 +70,7 @@ async function verifyJwt(
|
|
|
69
70
|
token: string,
|
|
70
71
|
verifySignature: SignatureVerifier,
|
|
71
72
|
opts: VerifyOptions = {},
|
|
72
|
-
): Promise<
|
|
73
|
+
): Promise<JsonObject | null> {
|
|
73
74
|
const parts = token.split(".");
|
|
74
75
|
if (parts.length !== 3) return null;
|
|
75
76
|
const [h, p, sig] = parts;
|
|
@@ -89,7 +90,7 @@ async function verifyJwt(
|
|
|
89
90
|
}
|
|
90
91
|
if (!valid) return null;
|
|
91
92
|
|
|
92
|
-
let payload:
|
|
93
|
+
let payload: JsonObject;
|
|
93
94
|
try {
|
|
94
95
|
payload = JSON.parse(b64urlToString(p!));
|
|
95
96
|
} catch {
|
|
@@ -113,7 +114,7 @@ export class HmacStrategy implements VerifyStrategy {
|
|
|
113
114
|
private readonly opts: VerifyOptions = {},
|
|
114
115
|
) {}
|
|
115
116
|
|
|
116
|
-
verify(token: string): Promise<
|
|
117
|
+
verify(token: string): Promise<JsonObject | null> {
|
|
117
118
|
return verifyJwt(
|
|
118
119
|
token,
|
|
119
120
|
async (input, signature, header) => {
|
|
@@ -152,7 +153,7 @@ export class JwksStrategy implements VerifyStrategy {
|
|
|
152
153
|
private readonly opts: VerifyOptions = {},
|
|
153
154
|
) {}
|
|
154
155
|
|
|
155
|
-
verify(token: string): Promise<
|
|
156
|
+
verify(token: string): Promise<JsonObject | null> {
|
|
156
157
|
return verifyJwt(
|
|
157
158
|
token,
|
|
158
159
|
async (input, signature, header) => {
|
|
@@ -219,7 +220,7 @@ export class JwksStrategy implements VerifyStrategy {
|
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
function toIdentity(claims:
|
|
223
|
+
function toIdentity(claims: JsonObject): Identity {
|
|
223
224
|
const roles = Array.isArray(claims.roles)
|
|
224
225
|
? (claims.roles as string[])
|
|
225
226
|
: typeof claims.role === "string"
|
package/src/cli.ts
CHANGED
|
@@ -21,37 +21,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
21
21
|
import { dirname, resolve } from "node:path";
|
|
22
22
|
import { createTableSql } from "./runtime/ddl";
|
|
23
23
|
import { schemaHash } from "./runtime/migrate";
|
|
24
|
-
import {
|
|
24
|
+
import { diffSchemaFingerprint, schemaFingerprint, type SchemaFingerprint } from "./runtime/schema-diff";
|
|
25
25
|
import { entitiesInPartition, partitionsOf, type SchemaDef } from "./sdk/schema";
|
|
26
|
+
import { signDevToken } from "./runtime/dev-token";
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
* with AUTH_SECRET when set, else the dev secret from the scaffolded oblaka.ts. */
|
|
30
|
-
const DEV_SECRET = "dev-secret-change-me";
|
|
28
|
+
/** The dev JWT claims `pramen token` mints. See `runtime/dev-token.ts` for the signer. */
|
|
29
|
+
type TokenClaims = { sub: string; roles: string[]; tenants?: string[] };
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
let bin = "";
|
|
34
|
-
for (const b of bytes) bin += String.fromCharCode(b);
|
|
35
|
-
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
36
|
-
}
|
|
37
|
-
const strToB64url = (s: string) => bytesToB64url(new TextEncoder().encode(s));
|
|
38
|
-
|
|
39
|
-
async function sign(payload: Record<string, unknown>): Promise<string> {
|
|
40
|
-
const secret = process.env.AUTH_SECRET || DEV_SECRET;
|
|
41
|
-
const now = Math.floor(Date.now() / 1000);
|
|
42
|
-
const header = strToB64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
|
|
43
|
-
const body = strToB64url(JSON.stringify({ iat: now, exp: now + 3600, ...payload }));
|
|
44
|
-
const data = `${header}.${body}`;
|
|
45
|
-
const key = await crypto.subtle.importKey(
|
|
46
|
-
"raw",
|
|
47
|
-
new TextEncoder().encode(secret),
|
|
48
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
49
|
-
false,
|
|
50
|
-
["sign"],
|
|
51
|
-
);
|
|
52
|
-
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
|
|
53
|
-
return `${data}.${bytesToB64url(new Uint8Array(sig))}`;
|
|
54
|
-
}
|
|
31
|
+
const sign = (payload: Record<string, unknown>): Promise<string> => signDevToken(payload);
|
|
55
32
|
|
|
56
33
|
/** The sub-schema of a single partition (used to mirror the DO's per-partition hash,
|
|
57
34
|
* which migrate() computes over exactly this subset). For a single-partition app the
|
|
@@ -102,7 +79,7 @@ Usage: pramen <command>
|
|
|
102
79
|
init [dir] scaffold a new project (app.ts + worker.ts + oblaka.ts)
|
|
103
80
|
schema sql print CREATE TABLE statements for the schema
|
|
104
81
|
schema hash print the schema hash
|
|
105
|
-
schema snapshot save the schema
|
|
82
|
+
schema snapshot save the schema fingerprint to .pramen/schema.json
|
|
106
83
|
schema diff compare the schema to the snapshot (safe vs unsafe)
|
|
107
84
|
schema status compare a deployed tenant's schema to the local schema
|
|
108
85
|
[--tenant t] [--url u] [--token jwt]
|
|
@@ -126,20 +103,27 @@ async function schemaCmd(sub: string | undefined): Promise<void> {
|
|
|
126
103
|
if (sub === "snapshot") {
|
|
127
104
|
const { schema } = await loadApp();
|
|
128
105
|
mkdirSync(dirname(snapshotPath), { recursive: true });
|
|
129
|
-
const snap = { hash: schemaHash(schema),
|
|
106
|
+
const snap = { hash: schemaHash(schema), fingerprint: schemaFingerprint(schema) };
|
|
130
107
|
writeFileSync(snapshotPath, JSON.stringify(snap, null, 2) + "\n");
|
|
131
|
-
console.log(`wrote ${snapshotPath} (${Object.keys(snap.
|
|
108
|
+
console.log(`wrote ${snapshotPath} (${Object.keys(snap.fingerprint).length} tables)`);
|
|
132
109
|
return;
|
|
133
110
|
}
|
|
134
111
|
if (sub === "diff") {
|
|
135
112
|
const { schema } = await loadApp();
|
|
136
|
-
const next =
|
|
113
|
+
const next = schemaFingerprint(schema);
|
|
137
114
|
if (!existsSync(snapshotPath)) {
|
|
138
115
|
console.log("no snapshot — run `pramen schema snapshot` to set a baseline.");
|
|
139
116
|
return;
|
|
140
117
|
}
|
|
141
|
-
|
|
142
|
-
|
|
118
|
+
// `fingerprint` was called `shape` before; keep reading an existing snapshot so an
|
|
119
|
+
// upgrade doesn't force a re-baseline.
|
|
120
|
+
const snap = JSON.parse(readFileSync(snapshotPath, "utf8")) as {
|
|
121
|
+
fingerprint?: SchemaFingerprint;
|
|
122
|
+
// the pre-rename key, quoted because it names a stored JSON field, not a symbol
|
|
123
|
+
"shape"?: SchemaFingerprint;
|
|
124
|
+
};
|
|
125
|
+
const prev = snap.fingerprint ?? snap["shape"] ?? {};
|
|
126
|
+
const changes = diffSchemaFingerprint(prev, next);
|
|
143
127
|
if (changes.length === 0) {
|
|
144
128
|
console.log("no changes since snapshot.");
|
|
145
129
|
return;
|
|
@@ -203,7 +187,7 @@ async function schemaCmd(sub: string | undefined): Promise<void> {
|
|
|
203
187
|
console.log(`live: ${live.hash ?? "(none)"}`);
|
|
204
188
|
console.log(`current: ${current}`);
|
|
205
189
|
console.log(upToDate ? "✓ up to date" : "⚠ BEHIND — migrates on the tenant's next boot");
|
|
206
|
-
const want =
|
|
190
|
+
const want = schemaFingerprint(subset);
|
|
207
191
|
for (const table of Object.keys(want)) {
|
|
208
192
|
const liveCols = new Set(live.tables[table] ?? []);
|
|
209
193
|
const missing = Object.keys(want[table]!.columns).filter((col) => !liveCols.has(col));
|
|
@@ -222,7 +206,9 @@ async function tokenCmd(args: string[]): Promise<void> {
|
|
|
222
206
|
if (!sub) fail("token: <sub> required");
|
|
223
207
|
const roles = pos.slice(1);
|
|
224
208
|
const tenants = flag("tenant")?.split(",");
|
|
225
|
-
|
|
209
|
+
const claims: TokenClaims = { sub, roles: roles.length ? roles : ["admin"] };
|
|
210
|
+
if (tenants) claims.tenants = tenants;
|
|
211
|
+
console.log(await sign(claims));
|
|
226
212
|
}
|
|
227
213
|
|
|
228
214
|
function initCmd(args: string[]): void {
|
package/src/dev.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// `@pramen/server/dev` — tooling helpers, deliberately NOT on the authoring entry point.
|
|
2
|
+
//
|
|
3
|
+
// `signDevToken` mints an HS256 JWT for local development: the `pramen` and `pramen-cms`
|
|
4
|
+
// bins and the repo's test helper all use it. It is kept off `@pramen/server` because its
|
|
5
|
+
// secret lookup reads `process.env.AUTH_SECRET`, which does not exist in a deployed
|
|
6
|
+
// Worker (bindings and secrets arrive on `env`) — so an app handler importing it from the
|
|
7
|
+
// main entry would silently sign with the PUBLISHED dev constant while appearing to honour
|
|
8
|
+
// AUTH_SECRET. A separate subpath makes "this is dev tooling" part of the import.
|
|
9
|
+
|
|
10
|
+
export { signDevToken, DEV_SECRET } from "./runtime/dev-token";
|
package/src/durable-object.ts
CHANGED
|
@@ -34,6 +34,8 @@ import type { Identity } from "./sdk/acl";
|
|
|
34
34
|
import type { HandlerContext } from "./sdk/handlers";
|
|
35
35
|
import type { PramenApp } from "./pramen";
|
|
36
36
|
import type { ClientMsg, ServerMsg, Subscription } from "./runtime/protocol";
|
|
37
|
+
import type { EnvBag } from "./sdk/handlers";
|
|
38
|
+
import type { JsonValue } from "./sdk/infer";
|
|
37
39
|
|
|
38
40
|
/** Durable per-socket state — kept SMALL and stable, since it rides the WebSocket
|
|
39
41
|
* attachment which workerd caps at ~2 KB. Only auth/routing identity lives here so it
|
|
@@ -208,9 +210,10 @@ export class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
const name = new URL(request.url).pathname.replace(/^\/rpc\//, "");
|
|
211
|
-
|
|
213
|
+
// The RPC body is JSON — parse it into the domain type once, here at the boundary.
|
|
214
|
+
let input: JsonValue = null;
|
|
212
215
|
if (request.method === "POST") {
|
|
213
|
-
input = await request.json().catch(() =>
|
|
216
|
+
input = ((await request.json().catch(() => null)) ?? null) as JsonValue;
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
try {
|
|
@@ -260,6 +263,8 @@ export class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
260
263
|
files: this.filesFor(this.tenant),
|
|
261
264
|
env: this.envBag,
|
|
262
265
|
identity,
|
|
266
|
+
tenant: this.tenant,
|
|
267
|
+
store: "do",
|
|
263
268
|
tasks: tasksFacade(this.driver),
|
|
264
269
|
mail: createMail(this.envBag, this.kv),
|
|
265
270
|
queue: createQueue(this.envBag),
|
|
@@ -354,12 +359,12 @@ export class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
354
359
|
|
|
355
360
|
switch (msg.type) {
|
|
356
361
|
case "subscribe":
|
|
357
|
-
return this.onSubscribe(ws, msg.id, msg.name, msg.input);
|
|
362
|
+
return this.onSubscribe(ws, msg.id, msg.name, msg.input ?? null);
|
|
358
363
|
case "unsubscribe":
|
|
359
364
|
this.setSubs(ws, this.getSubs(ws).filter((s) => s.id !== msg.id));
|
|
360
365
|
return;
|
|
361
366
|
case "call":
|
|
362
|
-
return this.onCall(ws, msg.id, msg.name, msg.input);
|
|
367
|
+
return this.onCall(ws, msg.id, msg.name, msg.input ?? null);
|
|
363
368
|
default:
|
|
364
369
|
return this.send(ws, { type: "error", id: "", error: "unknown message type" });
|
|
365
370
|
}
|
|
@@ -381,7 +386,7 @@ export class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
381
386
|
|
|
382
387
|
// --- live-query internals ---
|
|
383
388
|
|
|
384
|
-
private async onSubscribe(ws: WebSocket, id: string, name: string, input:
|
|
389
|
+
private async onSubscribe(ws: WebSocket, id: string, name: string, input: JsonValue): Promise<void> {
|
|
385
390
|
const att = this.getAttachment(ws);
|
|
386
391
|
const subs = this.getSubs(ws);
|
|
387
392
|
try {
|
|
@@ -402,7 +407,7 @@ export class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
|
|
405
|
-
private async onCall(ws: WebSocket, id: string, name: string, input:
|
|
410
|
+
private async onCall(ws: WebSocket, id: string, name: string, input: JsonValue): Promise<void> {
|
|
406
411
|
const att = this.getAttachment(ws);
|
|
407
412
|
let outcome: Awaited<ReturnType<typeof dispatch>>;
|
|
408
413
|
try {
|
|
@@ -598,13 +603,17 @@ export class PramenDOBase extends DurableObject<DoEnv> {
|
|
|
598
603
|
// Carry the schema so any consumer of this context (not just Db) can compile
|
|
599
604
|
// relation-aware `where` rules into subqueries, and the active partition so Db's
|
|
600
605
|
// table-access guard rejects any table outside this DO's partition.
|
|
601
|
-
return { acl: this.acl, identity, schema: this.app.schema, partition };
|
|
606
|
+
return { acl: this.acl, identity, schema: this.app.schema, partition, tenant: this.tenant };
|
|
602
607
|
}
|
|
603
608
|
|
|
604
609
|
// The DO env (bindings + vars + secrets) handed to handlers as ctx.env. Loosely
|
|
605
610
|
// typed at the boundary so handlers can read any var/secret without a DoEnv cast.
|
|
606
|
-
private
|
|
607
|
-
|
|
611
|
+
private widenedEnv: EnvBag | null = null;
|
|
612
|
+
private get envBag(): EnvBag {
|
|
613
|
+
// `this.env` is fixed for the DO's lifetime, so widen it once. This getter is read
|
|
614
|
+
// inside the per-subscription live-query loop, where a copy per read would allocate
|
|
615
|
+
// one whole binding bag per subscription on every write.
|
|
616
|
+
return (this.widenedEnv ??= { ...this.env });
|
|
608
617
|
}
|
|
609
618
|
|
|
610
619
|
// One Files facade per DO (a DO serves one tenant). Backed by the R2 binding;
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// and codegen load an app.ts for its schema without dragging in the DO runtime.
|
|
9
9
|
|
|
10
10
|
// --- schema authoring ---
|
|
11
|
-
export { Entity, defineSchema, renamedFrom, notNull, unique, indexed, hidden, defaultTo, primaryKey, generated, expr, ExprDefault, trigger } from "./sdk/schema";
|
|
11
|
+
export { Entity, defineSchema, renamedFrom, notNull, unique, indexed, hidden, defaultTo, primaryKey, generated, expr, ExprDefault, trigger, partitionOf, DEFAULT_PARTITION } from "./sdk/schema";
|
|
12
12
|
export type { TriggerDef, TriggerOp } from "./sdk/schema";
|
|
13
13
|
export { isValidUuid } from "./sdk/uuid";
|
|
14
14
|
export type {
|
|
@@ -31,7 +31,7 @@ export type {
|
|
|
31
31
|
// --- app + handlers ---
|
|
32
32
|
export { createApp } from "./sdk/app";
|
|
33
33
|
export { query, mutation, authorizeHandler } from "./sdk/handlers";
|
|
34
|
-
export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
34
|
+
export type { EnvBag, Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
|
|
35
35
|
|
|
36
36
|
// --- ACL ---
|
|
37
37
|
export { $identity, $input, $now, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker, isNowMarker } from "./sdk/acl";
|
|
@@ -64,6 +64,10 @@ export type {
|
|
|
64
64
|
InferRow,
|
|
65
65
|
InferUpdate,
|
|
66
66
|
JsonValue,
|
|
67
|
+
JsonObject,
|
|
68
|
+
SqlValue,
|
|
69
|
+
CellValue,
|
|
70
|
+
Row,
|
|
67
71
|
ProjectedRow,
|
|
68
72
|
RelationsOf,
|
|
69
73
|
RelationsResult,
|
|
@@ -78,6 +82,12 @@ export { Kv, denySession, allowSession, isSessionDenied } from "./runtime/kv";
|
|
|
78
82
|
// --- files ---
|
|
79
83
|
export type { FileRef, Files, SignUploadOpts, SignDownloadOpts, HeadResult } from "./sdk/files";
|
|
80
84
|
export { R2Adapter, MemoryAdapter, createFiles, handleFileRequest } from "./runtime/storage";
|
|
85
|
+
|
|
86
|
+
// Signed capability tokens (HMAC-SHA256) — the machinery behind signed file urls and
|
|
87
|
+
// page-preview links. Exported so an app (or @pramen/cms) can mint its own capability url
|
|
88
|
+
// without a second signing implementation.
|
|
89
|
+
export { signToken, verifyToken, isUsableSecret, resolveSecret, MIN_TOKEN_SECRET_LEN } from "./runtime/token";
|
|
90
|
+
export type { ExpiringToken } from "./runtime/token";
|
|
81
91
|
export type { StorageAdapter, PutResult, GetResult } from "./runtime/storage";
|
|
82
92
|
|
|
83
93
|
// --- mail (ctx.mail) ---
|
|
@@ -91,8 +101,8 @@ export { routeQueue, dispatchQueueBatch } from "./runtime/queue-consumer";
|
|
|
91
101
|
export type { QueueContext, QueueHandler, QueueMessage, QueueBatch, AppQueueMap } from "./runtime/queue-consumer";
|
|
92
102
|
|
|
93
103
|
// --- errors ---
|
|
94
|
-
export { PramenError, BadRequest, Unauthorized, Forbidden } from "./runtime/errors";
|
|
104
|
+
export { PramenError, BadRequest, Unauthorized, Forbidden, Conflict } from "./runtime/errors";
|
|
95
105
|
|
|
96
106
|
// --- substrate seam (advanced: bring your own SQL backend) ---
|
|
97
107
|
export { sqliteDialect, postgresDialect, DoSqliteDriver, D1Driver } from "./runtime/driver";
|
|
98
|
-
export type { Driver, Dialect,
|
|
108
|
+
export type { Driver, Dialect, DriverRow } from "./runtime/driver";
|
package/src/pramen.ts
CHANGED
|
@@ -18,12 +18,14 @@ import { validateTriggerTasks, type SchemaDef } from "./sdk/schema";
|
|
|
18
18
|
import type { AppTaskMap, HandlerMap, BootstrapFn } from "./sdk/handlers";
|
|
19
19
|
import type { AppQueueMap, QueueBatch } from "./runtime/queue-consumer";
|
|
20
20
|
import type { Role } from "./sdk/acl";
|
|
21
|
+
import type { EnvBag } from "./sdk/handlers";
|
|
22
|
+
import type { JsonValue } from "./sdk/infer";
|
|
21
23
|
|
|
22
24
|
/** Injected into a public route's handler — forward a privileged mutation into the
|
|
23
25
|
* tenant's DO without the handler importing any deploy-side code (so app.ts stays
|
|
24
26
|
* authoring-only). The synthetic identity defaults to the admin role. */
|
|
25
27
|
export interface RouteContext {
|
|
26
|
-
callPrivileged(opts: { name: string; input?:
|
|
28
|
+
callPrivileged(opts: { name: string; input?: JsonValue; tenant?: string; roles?: string[] }): Promise<Response>;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/** A public, pre-auth route — matched before identity resolution, so it can host a
|
|
@@ -36,7 +38,7 @@ export interface PublicRoute {
|
|
|
36
38
|
method: string;
|
|
37
39
|
/** Exact pathname to match (e.g. "/stripe/webhook"). */
|
|
38
40
|
path: string;
|
|
39
|
-
handler: (request: Request, env:
|
|
41
|
+
handler: (request: Request, env: EnvBag, ctx: RouteContext) => Response | Promise<Response>;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** The user-facing app: a schema, the handler map, ACL roles, and optional public
|