@agilesyndrome/cf-genai-base 2.0.0 → 2.0.1
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/CONTRACT.md +2 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/data.js +5 -5
- package/src/index.js +2 -2
package/CONTRACT.md
CHANGED
|
@@ -62,6 +62,8 @@ manifest through `feature.dataResources`. Each resource must declare a safe
|
|
|
62
62
|
name, table, explicit columns, and one scope: `user`, `tenant`, or `system`.
|
|
63
63
|
Resources may also declare allowed operations (`read`, `create`, `update`, and
|
|
64
64
|
`delete`); reads support bounded cursor pagination through `reader.page()`.
|
|
65
|
+
Anonymous tenant reads require an explicit worker `publicTenantId` and a
|
|
66
|
+
resource-level `publicRead: true` declaration.
|
|
65
67
|
Request handlers receive `state.data`, whose scope-specific readers apply the
|
|
66
68
|
validated user or tenant predicate. Domain handlers must not use unrestricted
|
|
67
69
|
`env.DB` for registered resources. Base cannot provide row-level security to
|
package/README.md
CHANGED
|
@@ -82,6 +82,9 @@ the reader applies ownership predicates, supports bounded cursor pagination via
|
|
|
82
82
|
`.page()`, and never accepts raw SQL. Resources can explicitly restrict their
|
|
83
83
|
operations to `read`, `create`, `update`, and `delete`.
|
|
84
84
|
|
|
85
|
+
Anonymous tenant reads require both `publicTenantId` on `createWorker` and
|
|
86
|
+
`publicRead: true` on the resource; they never grant anonymous system access.
|
|
87
|
+
|
|
85
88
|
For example, a tenant-owned resource registers its `tenant_id` column with
|
|
86
89
|
base, while feature code calls `state.data.tenant.list("recipes")` without
|
|
87
90
|
passing a tenant ID. The active tenant must be a validated membership. A
|
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -36,7 +36,7 @@ export function normalizeDataResources(resources = []) {
|
|
|
36
36
|
if (writableColumns.some((column) => !columns.includes(column))) throw new TypeError(`Data resource ${resource.name} references an unwritable column`);
|
|
37
37
|
const operations = [...new Set((resource.operations || ["read", ...(writableColumns.length ? ["create", "update", "delete"] : [])]).map((operation) => String(operation).toLowerCase()))];
|
|
38
38
|
if (!operations.length || operations.some((operation) => !DATA_OPERATIONS.includes(operation)) || !operations.includes("read")) throw new TypeError(`Data resource ${name} has invalid operations`);
|
|
39
|
-
return { ...resource, name, table: String(resource.table), scope, columns, idColumn, ownerColumn, tenantColumn, filterableColumns, orderableColumns, writableColumns, operations };
|
|
39
|
+
return { ...resource, name, table: String(resource.table), scope, columns, idColumn, ownerColumn, tenantColumn, filterableColumns, orderableColumns, writableColumns, operations, publicRead: Boolean(resource.publicRead) };
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -122,18 +122,18 @@ export function createDataReader(env, { resources = [], context } = {}) {
|
|
|
122
122
|
return { user: scope("user"), tenant: scope("tenant"), system: scope("system"), resources: [...registry.values()] };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
export async function requestDataContext(env, { state = {}, request } = {}) {
|
|
125
|
+
export async function requestDataContext(env, { state = {}, request, publicTenantId = null } = {}) {
|
|
126
126
|
const authUser = state.authUser || (state.user ? await ensureUser(env, state.user, { who: `user:${state.user.sub || "unknown"}` }) : null);
|
|
127
127
|
const system = Boolean(state.user?.auth_strategy === "http_basic" || (authUser && authUser.is_admin));
|
|
128
|
-
if (!authUser) return { userId: null, tenantId:
|
|
128
|
+
if (!authUser) return { userId: null, tenantId: publicTenantId, public: Boolean(publicTenantId), system: false };
|
|
129
129
|
const tenants = await listUserTenants(env, authUser.id, { who: `user:${authUser.id}` });
|
|
130
130
|
const requestedTenant = state.tenantId || request?.headers?.get("X-Tenant-ID") || null;
|
|
131
131
|
const tenant = requestedTenant ? tenants.find((item) => item.id === requestedTenant) : tenants.length === 1 ? tenants[0] : null;
|
|
132
|
-
return { userId: authUser.id, tenantId: tenant?.id || null, system, tenants };
|
|
132
|
+
return { userId: authUser.id, tenantId: tenant?.id || null, public: false, system, tenants };
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function isAllowed(resource, requestedScope, actor, operation) {
|
|
136
|
-
const allowed = requestedScope === "system" ? Boolean(actor.system) : requestedScope === resource.scope && (requestedScope === "user" ? Boolean(actor.userId) : Boolean(actor.
|
|
136
|
+
const allowed = requestedScope === "system" ? Boolean(actor.system) : requestedScope === resource.scope && (requestedScope === "user" ? Boolean(actor.userId) : Boolean(actor.tenantId && (actor.userId || (actor.public && resource.publicRead))));
|
|
137
137
|
if (!allowed || !resource.operations.includes(operation)) {
|
|
138
138
|
auditLog({ who: actorLabel(actor), operation: "deny", resource: `data:${resource.name}:${requestedScope}:${operation}` });
|
|
139
139
|
return false;
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getCircuitBreaker, evaluateCircuitBreaker, listCircuitBreakers, listHea
|
|
|
7
7
|
import { createDataReader, DataScopeError, normalizeDataResources, requestDataContext } from "./data.js";
|
|
8
8
|
export * from "./core.js";
|
|
9
9
|
export * from "./data.js";
|
|
10
|
-
export function createWorker({ fetch, scheduled, auth, authorize, scopes = [], scopeRoutes = [], middleware = [], features = [], dataResources = [], health, boot, metrics, security = true, adminPage, siteAdminPage }) {
|
|
10
|
+
export function createWorker({ fetch, scheduled, auth, authorize, scopes = [], scopeRoutes = [], middleware = [], features = [], dataResources = [], publicTenantId = null, health, boot, metrics, security = true, adminPage, siteAdminPage }) {
|
|
11
11
|
if (typeof fetch !== "function") throw new TypeError("createWorker requires a fetch handler");
|
|
12
12
|
const provider = auth || features.find((feature) => typeof feature?.getUser === "function");
|
|
13
13
|
const registeredDataResources = normalizeDataResources([...dataResources, ...features.flatMap((feature) => Array.isArray(feature?.dataResources) ? feature.dataResources : [])]);
|
|
@@ -23,7 +23,7 @@ export function createWorker({ fetch, scheduled, auth, authorize, scopes = [], s
|
|
|
23
23
|
if (boot) await boot(env, { request, ctx });
|
|
24
24
|
const url = new URL(request.url);
|
|
25
25
|
const state = Object.create(null);
|
|
26
|
-
state.data = createDataReader(env, { resources: registeredDataResources, context: () => requestDataContext(env, { state, request }) });
|
|
26
|
+
state.data = createDataReader(env, { resources: registeredDataResources, context: () => requestDataContext(env, { state, request, publicTenantId }) });
|
|
27
27
|
if (env?.DB && features.some((feature) => typeof feature?.healthcheck === "function" || feature?.healthchecks?.length || feature?.healthChecks?.length || feature?.circuitBreakers?.length || feature?.circuit_breakers?.length)) ctx?.waitUntil?.(registerFeatureManifests(env, features, { who: "system:update" }).then(() => listCircuitBreakers(env, { who: "system:update" }).then((breakers) => Promise.all(breakers.filter(Boolean).map((breaker) => evaluateCircuitBreaker(env, breaker.id, { who: "system:update" }))))).catch((error) => console.error("[EventLog] feature manifest registration failed", error)));
|
|
28
28
|
const dispatch = async (index, currentRequest = request) => {
|
|
29
29
|
const layer = chain[index];
|