@agent-native/core 0.135.0 → 0.135.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/sharing/access.ts +44 -2
- package/corpus/templates/content/actions/_database-utils.ts +113 -4
- package/corpus/templates/content/actions/get-document.ts +84 -3
- package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +29 -13
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +5 -12
- package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
- package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
- package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
- package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
- package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
- package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
- package/corpus/templates/content/app/i18n-data.ts +32 -1
- package/corpus/templates/content/app/lib/document-query.ts +36 -0
- package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
- package/corpus/templates/content/server/lib/document-context.ts +11 -6
- package/corpus/templates/content/shared/api.ts +8 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +32 -2
- package/dist/sharing/access.js.map +1 -1
- package/package.json +1 -1
- package/src/sharing/access.ts +44 -2
|
@@ -36,6 +36,7 @@ export interface Document {
|
|
|
36
36
|
properties?: DocumentProperty[];
|
|
37
37
|
database?: ContentDatabase;
|
|
38
38
|
databaseMembership?: ContentDatabaseMembership;
|
|
39
|
+
bodyHydration?: ContentDocumentBodyHydration;
|
|
39
40
|
contextPath?: ContentContextPathEntry[];
|
|
40
41
|
createdAt: string;
|
|
41
42
|
updatedAt: string;
|
|
@@ -407,6 +408,13 @@ export interface ContentDatabaseMembership {
|
|
|
407
408
|
bodyHydration?: ContentDatabaseBodyHydration;
|
|
408
409
|
}
|
|
409
410
|
|
|
411
|
+
export interface ContentDocumentBodyHydration {
|
|
412
|
+
provider?: "builder";
|
|
413
|
+
hydration?: ContentDatabaseBodyHydration;
|
|
414
|
+
sourceId?: string;
|
|
415
|
+
databaseDocumentId?: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
410
418
|
export type ContentDatabaseBodyHydrationState =
|
|
411
419
|
| "pending"
|
|
412
420
|
| "hydrating"
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -16,19 +16,19 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
|
|
|
16
16
|
error?: undefined;
|
|
17
17
|
ok?: undefined;
|
|
18
18
|
} | {
|
|
19
|
-
count?: undefined;
|
|
20
19
|
updated: number;
|
|
21
20
|
error?: undefined;
|
|
22
21
|
ok?: undefined;
|
|
23
|
-
} | {
|
|
24
22
|
count?: undefined;
|
|
23
|
+
} | {
|
|
25
24
|
updated?: undefined;
|
|
26
25
|
error: string;
|
|
27
26
|
ok?: undefined;
|
|
28
|
-
} | {
|
|
29
27
|
count?: undefined;
|
|
28
|
+
} | {
|
|
30
29
|
updated?: undefined;
|
|
31
30
|
ok: boolean;
|
|
32
31
|
error?: undefined;
|
|
32
|
+
count?: undefined;
|
|
33
33
|
}>>;
|
|
34
34
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
+
error?: undefined;
|
|
44
45
|
summary: import("./types.js").TraceSummary;
|
|
45
46
|
spans: import("./types.js").TraceSpan[];
|
|
46
47
|
id?: undefined;
|
|
47
|
-
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
+
error?: undefined;
|
|
50
51
|
summary?: undefined;
|
|
51
52
|
spans?: undefined;
|
|
52
53
|
id: string;
|
|
53
|
-
error?: undefined;
|
|
54
54
|
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
@@ -59,10 +59,10 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
59
59
|
error: any;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
+
error?: undefined;
|
|
62
63
|
summary?: undefined;
|
|
63
64
|
spans?: undefined;
|
|
64
65
|
id?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
|
-
error?: undefined;
|
|
67
67
|
}>>;
|
|
68
68
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -75,6 +75,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
75
75
|
user: "user";
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
|
+
message?: undefined;
|
|
79
|
+
id?: undefined;
|
|
78
80
|
found?: undefined;
|
|
79
81
|
deleted?: undefined;
|
|
80
82
|
providers: {
|
|
@@ -89,20 +91,19 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
89
91
|
count: number;
|
|
90
92
|
provider?: undefined;
|
|
91
93
|
registered?: undefined;
|
|
92
|
-
id?: undefined;
|
|
93
94
|
label?: undefined;
|
|
94
|
-
message?: undefined;
|
|
95
95
|
} | {
|
|
96
|
+
message?: undefined;
|
|
97
|
+
id?: undefined;
|
|
96
98
|
deleted?: undefined;
|
|
97
99
|
providers?: undefined;
|
|
98
100
|
count?: undefined;
|
|
99
101
|
found: boolean;
|
|
100
102
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
101
103
|
registered?: undefined;
|
|
102
|
-
id?: undefined;
|
|
103
104
|
label?: undefined;
|
|
104
|
-
message?: undefined;
|
|
105
105
|
} | {
|
|
106
|
+
message?: undefined;
|
|
106
107
|
deleted?: undefined;
|
|
107
108
|
providers?: undefined;
|
|
108
109
|
count?: undefined;
|
|
@@ -111,8 +112,8 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
111
112
|
id: string;
|
|
112
113
|
registered?: undefined;
|
|
113
114
|
label?: undefined;
|
|
114
|
-
message?: undefined;
|
|
115
115
|
} | {
|
|
116
|
+
message?: undefined;
|
|
116
117
|
found?: undefined;
|
|
117
118
|
providers?: undefined;
|
|
118
119
|
count?: undefined;
|
|
@@ -121,7 +122,6 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
121
122
|
id: string;
|
|
122
123
|
registered?: undefined;
|
|
123
124
|
label?: undefined;
|
|
124
|
-
message?: undefined;
|
|
125
125
|
} | {
|
|
126
126
|
found?: undefined;
|
|
127
127
|
deleted?: undefined;
|
|
@@ -311,9 +311,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
311
311
|
notes?: string | undefined;
|
|
312
312
|
scope?: "org" | "user" | undefined;
|
|
313
313
|
}, {
|
|
314
|
-
deleted?: undefined;
|
|
315
314
|
id?: undefined;
|
|
316
315
|
provider?: undefined;
|
|
316
|
+
deleted?: undefined;
|
|
317
317
|
found?: undefined;
|
|
318
318
|
providers: {
|
|
319
319
|
id: string;
|
|
@@ -329,9 +329,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
329
329
|
label?: undefined;
|
|
330
330
|
message?: undefined;
|
|
331
331
|
} | {
|
|
332
|
-
deleted?: undefined;
|
|
333
332
|
count?: undefined;
|
|
334
333
|
id?: undefined;
|
|
334
|
+
deleted?: undefined;
|
|
335
335
|
providers?: undefined;
|
|
336
336
|
found: boolean;
|
|
337
337
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
@@ -339,9 +339,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
339
339
|
label?: undefined;
|
|
340
340
|
message?: undefined;
|
|
341
341
|
} | {
|
|
342
|
-
deleted?: undefined;
|
|
343
342
|
count?: undefined;
|
|
344
343
|
provider?: undefined;
|
|
344
|
+
deleted?: undefined;
|
|
345
345
|
providers?: undefined;
|
|
346
346
|
found: boolean;
|
|
347
347
|
id: string;
|
|
@@ -359,9 +359,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
359
359
|
label?: undefined;
|
|
360
360
|
message?: undefined;
|
|
361
361
|
} | {
|
|
362
|
-
deleted?: undefined;
|
|
363
362
|
count?: undefined;
|
|
364
363
|
provider?: undefined;
|
|
364
|
+
deleted?: undefined;
|
|
365
365
|
found?: undefined;
|
|
366
366
|
providers?: undefined;
|
|
367
367
|
registered: boolean;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/sharing/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAoB,KAAK,GAAG,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/sharing/access.ts"],"names":[],"mappings":"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"}
|
package/dist/sharing/access.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* callers who lack the required role.
|
|
14
14
|
*/
|
|
15
15
|
import { and, eq, or, sql } from "drizzle-orm";
|
|
16
|
+
import { orgMembers } from "../org/schema.js";
|
|
16
17
|
import { getRequestAuthCapability, getRequestUserEmail, getRequestOrgId, } from "../server/request-context.js";
|
|
17
18
|
import { listShareableResources, requireShareableResource, } from "./registry.js";
|
|
18
19
|
import { ROLE_RANK } from "./schema.js";
|
|
@@ -61,6 +62,25 @@ function normalizeEmailForAccess(email) {
|
|
|
61
62
|
function emailColumnMatches(column, email) {
|
|
62
63
|
return sql `lower(${column}) = ${email}`;
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Real `org_members` membership, independent of the caller's currently
|
|
67
|
+
* active org (`ctx.orgId`). `org`-visibility access must key off actual
|
|
68
|
+
* membership — a user's active-org selection is a UI convenience, not a
|
|
69
|
+
* statement of which orgs they belong to.
|
|
70
|
+
*
|
|
71
|
+
* Queries through the resource's own `reg.getDb()` — the same connection
|
|
72
|
+
* every other lookup in this file uses — not the ambient `getDbExec()`,
|
|
73
|
+
* since `org_members` lives in that same app database.
|
|
74
|
+
*/
|
|
75
|
+
async function isOrgMember(reg, memberOrgId, email) {
|
|
76
|
+
const db = reg.getDb();
|
|
77
|
+
const rows = await db
|
|
78
|
+
.select({ id: orgMembers.id })
|
|
79
|
+
.from(orgMembers)
|
|
80
|
+
.where(and(eq(orgMembers.orgId, memberOrgId), emailColumnMatches(orgMembers.email, email)))
|
|
81
|
+
.limit(1);
|
|
82
|
+
return rows.length > 0;
|
|
83
|
+
}
|
|
64
84
|
/**
|
|
65
85
|
* Build a Drizzle `WHERE` clause that admits rows the current user can see.
|
|
66
86
|
* Pass the ownable resource table and its shares table; optional min role
|
|
@@ -284,7 +304,7 @@ async function resolveAccessImpl(resourceType, resourceId, rawCtx = currentAcces
|
|
|
284
304
|
const resource = await loadResourceForAccess(reg, resourceId, options);
|
|
285
305
|
if (!resource)
|
|
286
306
|
return null;
|
|
287
|
-
const { userEmail
|
|
307
|
+
const { userEmail } = ctx;
|
|
288
308
|
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
289
309
|
if (normalizedUserEmail &&
|
|
290
310
|
normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&
|
|
@@ -302,7 +322,17 @@ async function resolveAccessImpl(resourceType, resourceId, rawCtx = currentAcces
|
|
|
302
322
|
// `visibility === "public"` on an `allowPublic: false` resource is treated
|
|
303
323
|
// as private: only owner + explicit shares grant access. Falls through to
|
|
304
324
|
// the explicit-share lookup below.
|
|
305
|
-
|
|
325
|
+
//
|
|
326
|
+
// Membership in the resource's own org, not equality with the caller's
|
|
327
|
+
// currently active org: a caller can be a genuine member of the
|
|
328
|
+
// resource's org while a *different* org is their active selection, and
|
|
329
|
+
// `org` visibility should still admit them. Still requires some active
|
|
330
|
+
// org to be set at all (`orgId`), matching the pre-existing behavior for
|
|
331
|
+
// a caller with no active org.
|
|
332
|
+
if (resource.visibility === "org" &&
|
|
333
|
+
resource.orgId &&
|
|
334
|
+
normalizedUserEmail &&
|
|
335
|
+
(await isOrgMember(reg, resource.orgId, normalizedUserEmail))) {
|
|
306
336
|
const role = await highestShareRole(reg, resourceId, ctx, resource);
|
|
307
337
|
return { role: role ?? "viewer", resource };
|
|
308
338
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/sharing/access.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Access-control helpers for shareable resources.\n *\n * The access model combines:\n * 1. Direct ownership — `owner_email = currentUser`.\n * 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone\n * in the same org; `public` grants read to any authenticated user.\n * 3. Share rows — per-user or per-org grants in the `{type}_shares` table\n * with a role (`viewer | editor | admin`).\n *\n * Use `applyAccessFilter()` on list/read queries to filter rows the current\n * user can see. Use `assertAccess()` at the top of write actions to reject\n * callers who lack the required role.\n */\n\nimport { and, eq, or, sql, type SQL } from \"drizzle-orm\";\n\nimport {\n getRequestAuthCapability,\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../server/request-context.js\";\nimport {\n listShareableResources,\n requireShareableResource,\n type ShareableResourceRegistration,\n} from \"./registry.js\";\nimport { ROLE_RANK, type ShareRole, type Visibility } from \"./schema.js\";\n\n/**\n * Find a registration by its drizzle table identity. Used to look up\n * per-resource policy flags (e.g. `allowPublic`) inside `accessFilter`,\n * which receives only the table — not the resource-type name.\n *\n * Identity is stable within a single bundle (Vite dedupes module instances);\n * the SSR/server side is the only caller, so per-bundle identity is fine.\n */\nfunction findRegistrationByTable(\n resourceTable: any,\n): ShareableResourceRegistration | undefined {\n for (const reg of listShareableResources()) {\n if (reg.resourceTable === resourceTable) return reg;\n }\n return undefined;\n}\n\nexport class ForbiddenError extends Error {\n statusCode = 403;\n constructor(message = \"Forbidden\") {\n super(message);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport interface AccessContext {\n userEmail?: string;\n orgId?: string;\n authCapability?: string;\n}\n\n/** Current request's access context. Pulls from request-context ALS. */\nexport function currentAccess(): AccessContext {\n return {\n userEmail: getRequestUserEmail(),\n orgId: getRequestOrgId(),\n authCapability: getRequestAuthCapability(),\n };\n}\n\nexport function resolveRegisteredAccessContext(\n reg: ShareableResourceRegistration | undefined,\n ctx: AccessContext,\n): AccessContext {\n if (!reg?.resolveAccessContext) return ctx;\n const resolved = reg.resolveAccessContext(ctx);\n return ctx.authCapability\n ? { ...resolved, authCapability: ctx.authCapability }\n : resolved;\n}\n\nfunction normalizeEmailForAccess(email: string | undefined): string | null {\n const normalized = email?.trim().toLowerCase();\n return normalized || null;\n}\n\nfunction emailColumnMatches(column: any, email: string): SQL {\n return sql`lower(${column}) = ${email}`;\n}\n\n/**\n * Build a Drizzle `WHERE` clause that admits rows the current user can see.\n * Pass the ownable resource table and its shares table; optional min role\n * (defaults to 'viewer') gates which share rows count.\n *\n * `visibility = 'public'` is intentionally NOT admitted by default. Public\n * means \"anyone with the link can view\" (still honoured by `resolveAccess`\n * for read-by-id), not \"appears in every signed-in user's list/sidebar.\"\n * Pass `{ includePublic: true }` for the rare list endpoint that wants\n * cross-user public discovery (a public template gallery, for example).\n *\n * Example:\n *\n * const rows = await db\n * .select()\n * .from(schema.documents)\n * .where(accessFilter(schema.documents, schema.documentShares));\n */\nexport function accessFilter(\n resourceTable: any,\n sharesTable: any,\n rawCtx: AccessContext = currentAccess(),\n minRole: ShareRole = \"viewer\",\n options: { includePublic?: boolean } = {},\n): SQL {\n // Defense in depth — resources registered with `allowPublic: false` must\n // never participate in cross-user \"public\" discovery, even if a caller\n // accidentally passes `includePublic: true` or if a stale public row sits\n // in the DB.\n const reg = findRegistrationByTable(resourceTable);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n const publicAllowed = reg?.allowPublic !== false;\n const includePublic = (options.includePublic ?? false) && publicAllowed;\n const clauses: SQL[] = [];\n\n if (normalizedUserEmail) {\n clauses.push(\n and(\n emailColumnMatches(resourceTable.ownerEmail, normalizedUserEmail),\n ownerScopeFilter(reg, resourceTable, ctx),\n )!,\n );\n }\n if (minRole === \"viewer\") {\n if (includePublic) {\n clauses.push(eq(resourceTable.visibility, \"public\"));\n }\n if (orgId) {\n clauses.push(\n and(\n eq(resourceTable.visibility, \"org\"),\n eq(resourceTable.orgId, orgId),\n )!,\n );\n }\n }\n if (normalizedUserEmail) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'user'\n and lower(${sharesTable.principalId}) = ${normalizedUserEmail}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n if (orgId) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'org'\n and ${sharesTable.principalId} = ${orgId}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n\n return or(...clauses) ?? sql`1=0`;\n}\n\nfunction ownerScopeFilter(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n if (reg?.ownerAccessIgnoresOrg === true) return sql`1=1`;\n if (ctx.orgId) {\n // Rows created before org-scoping, or in solo mode, have no org_id. Keep\n // them manageable by their owner after the owner joins or switches into an\n // organization, while still keeping rows from other orgs out of scope.\n return or(\n eq(resourceTable.orgId, ctx.orgId),\n sql`${resourceTable.orgId} IS NULL`,\n )!;\n }\n return sql`${resourceTable.orgId} IS NULL`;\n}\n\nfunction ownerMatchesActiveScope(\n reg: ShareableResourceRegistration | undefined,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg?.ownerAccessIgnoresOrg === true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n if (!resourceOrgId) return true;\n return ctx.orgId === resourceOrgId;\n}\n\nfunction minRoleSql(minRole: ShareRole): SQL {\n if (minRole === \"viewer\") {\n // any role satisfies viewer\n return sql`1=1`;\n }\n if (minRole === \"editor\") {\n return sql`role in ('editor','admin')`;\n }\n return sql`role = 'admin'`;\n}\n\nfunction restrictedShareScopeSql(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n // Restricted resources (extensions) must stay inside their resource org even\n // if stale cross-org share rows already exist from older code or bad data.\n if (reg?.requireOrgMemberForUserShares !== true) return sql`1=1`;\n if (!ctx.orgId) return sql`1=0`;\n return eq(resourceTable.orgId, ctx.orgId);\n}\n\nfunction explicitSharesAllowedForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg.requireOrgMemberForUserShares !== true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n return !!resourceOrgId && !!ctx.orgId && resourceOrgId === ctx.orgId;\n}\n\nexport interface ResolvedAccess {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** The resource row (already loaded). */\n resource: any;\n}\n\n/**\n * Minimal resource shape returned when a caller opts into a projected access\n * load via `{ skipResourceBody: true }`. Contains exactly the columns the\n * access-decision logic itself reads — identity, ownership, org scope, and\n * visibility — never a resource type's heavy body columns (`data`,\n * `content`, and similar blobs).\n */\nexport interface AccessProjectedResource {\n id: string;\n ownerEmail: string;\n orgId: string | null;\n visibility: Visibility;\n}\n\nexport interface ResolvedAccessProjected {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** Only the access-decision columns — not the full resource row. */\n resource: AccessProjectedResource;\n}\n\nexport interface ResolveAccessOptions {\n /**\n * When true, load only the columns the access decision itself needs\n * (`id`, `ownerEmail`, `orgId`, `visibility`) instead of the full resource\n * row. Use this when the caller only needs the access decision — not the\n * resource body — and wants to skip fetching heavy type-specific columns\n * (e.g. `data`/`content` blobs) on every `assertAccess`/`resolveAccess`\n * call.\n *\n * Silently ignored (falls back to a full row load) for any resource type\n * registered with a `publicAccessRole` *function* resolver, since that\n * callback can read arbitrary resource fields the projection would have\n * omitted — see `hasDynamicPublicAccessRoleResolver` below.\n *\n * Default: `false` — the full row is loaded, matching historical behavior.\n * Callers that need resource body fields (most call sites today — for\n * many resource types `resolveAccess().resource` doubles as the action's\n * primary data read) must NOT pass this.\n */\n skipResourceBody?: boolean;\n}\n\nasync function publicAccessRoleForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): Promise<ShareRole> {\n const roleResolver = reg.publicAccessRole;\n if (!roleResolver) return \"viewer\";\n return typeof roleResolver === \"function\"\n ? await roleResolver(resource, ctx)\n : roleResolver;\n}\n\nfunction higherShareRole(a: ShareRole, b: ShareRole | null): ShareRole {\n if (!b) return a;\n return ROLE_RANK[b] > ROLE_RANK[a] ? b : a;\n}\n\nfunction columnName(column: unknown): string | null {\n const candidate = column as\n | {\n name?: unknown;\n config?: { name?: unknown };\n _: { name?: unknown };\n }\n | undefined;\n const name = candidate?.name ?? candidate?.config?.name ?? candidate?._?.name;\n return typeof name === \"string\" && name ? name : null;\n}\n\nfunction missingColumnName(err: unknown): string | null {\n const error = err as { code?: string; message?: string } | undefined;\n const message = error?.message ?? \"\";\n if (\n error?.code !== \"42703\" &&\n !/no such column|does not exist/i.test(message)\n ) {\n return null;\n }\n const quoted = message.match(/column\\s+\"([^\"]+)\"\\s+does not exist/i)?.[1];\n if (quoted) return quoted;\n const sqlite = message.match(/no such column:\\s+[\"`]?([\\w.]+)[\"`]?/i)?.[1];\n if (sqlite) return sqlite.split(\".\").pop() ?? sqlite;\n return null;\n}\n\nfunction selectExistingColumns(\n columns: Record<string, unknown>,\n omittedColumnNames: Set<string>,\n): Record<string, unknown> {\n const selection: Record<string, unknown> = {};\n for (const [key, column] of Object.entries(columns)) {\n const name = columnName(column);\n if (!name || omittedColumnNames.has(name)) continue;\n selection[key] = column;\n }\n return selection;\n}\n\n/**\n * The fixed column set the access-decision logic in `resolveAccess` itself\n * reads: identity (`id`), ownership (`ownerEmail`, `orgId`), and the coarse\n * `visibility` flag. This is intentionally NOT caller-configurable — an\n * arbitrary caller-supplied column list could omit a column the access\n * checks below depend on and silently break authorization. Every ownable\n * resource table has these columns (`ownableColumns()` + a primary key\n * named `id`), so this projection is safe for any registration that\n * doesn't also read the row through a dynamic resolver (see below).\n */\nfunction projectedAccessColumns(resourceTable: any): Record<string, unknown> {\n return {\n id: resourceTable.id,\n ownerEmail: resourceTable.ownerEmail,\n orgId: resourceTable.orgId,\n visibility: resourceTable.visibility,\n };\n}\n\n/**\n * True when this registration's `publicAccessRole` is a callback rather than\n * a fixed role string. Callbacks receive the loaded `resource` and may read\n * arbitrary fields on it (see e.g. `templates/design`'s\n * `publicDesignAccessRole`, which inspects design-specific data). The\n * projected access load must never be used for these registrations — always\n * fall back to a full row load so the resolver sees the complete resource.\n */\nfunction hasDynamicPublicAccessRoleResolver(\n reg: ShareableResourceRegistration,\n): boolean {\n return typeof reg.publicAccessRole === \"function\";\n}\n\nasync function loadResourceForAccess(\n reg: ShareableResourceRegistration,\n resourceId: string,\n options: ResolveAccessOptions = {},\n): Promise<any | null> {\n const db = reg.getDb() as any;\n const useProjection =\n options.skipResourceBody === true &&\n !hasDynamicPublicAccessRoleResolver(reg);\n const projectedColumns = useProjection\n ? projectedAccessColumns(reg.resourceTable)\n : null;\n const omittedColumnNames = new Set<string>();\n\n for (let attempt = 0; attempt < 12; attempt++) {\n try {\n const query =\n !projectedColumns && omittedColumnNames.size === 0\n ? db.select()\n : db.select(\n selectExistingColumns(\n projectedColumns ?? reg.resourceTable,\n omittedColumnNames,\n ),\n );\n const [resource] = await query\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, resourceId));\n return resource ?? null;\n } catch (err) {\n const missing = missingColumnName(err);\n if (!missing || omittedColumnNames.has(missing)) throw err;\n omittedColumnNames.add(missing);\n console.warn(\n `[sharing] ${reg.type} access lookup omitted missing column ${missing}`,\n );\n }\n }\n\n throw new Error(\n `Could not load ${reg.type} ${resourceId}: too many missing resource columns`,\n );\n}\n\n/**\n * Return the effective role the current user has on a specific resource, or\n * null if they have no access. Loads the resource and relevant share rows.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior — for most resource types `.resource` here doubles as the\n * action's primary data read). Pass `{ skipResourceBody: true }` when the\n * caller only needs the access decision to load just the access-decision\n * columns instead — see `ResolveAccessOptions`.\n */\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n return resolveAccessImpl(resourceType, resourceId, rawCtx, options);\n}\n\n/**\n * Un-overloaded implementation shared by the `resolveAccess` overloads and\n * called directly by `assertAccess` below. Kept separate from the exported\n * `resolveAccess` so internal callers passing a widened\n * `ResolveAccessOptions` (rather than a `{ skipResourceBody: true }` /\n * `{ skipResourceBody?: false }` literal) don't have to satisfy TypeScript's\n * overload resolution, which only matches against the declared overload\n * signatures, not the implementation signature.\n */\nasync function resolveAccessImpl(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n const reg = requireShareableResource(resourceType);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n\n const resource = await loadResourceForAccess(reg, resourceId, options);\n if (!resource) return null;\n\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n\n if (\n normalizedUserEmail &&\n normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&\n ownerMatchesActiveScope(reg, resource, ctx)\n ) {\n return { role: \"owner\", resource };\n }\n if (resource.visibility === \"public\" && reg.allowPublic !== false) {\n // No share row needed; default viewer unless the resource registration\n // deliberately grants a stronger public-by-link role or explicit shares\n // upgrade the current principal.\n const publicRole = await publicAccessRoleForResource(reg, resource, ctx);\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: higherShareRole(publicRole, role), resource };\n }\n // `visibility === \"public\"` on an `allowPublic: false` resource is treated\n // as private: only owner + explicit shares grant access. Falls through to\n // the explicit-share lookup below.\n if (resource.visibility === \"org\" && orgId && resource.orgId === orgId) {\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: role ?? \"viewer\", resource };\n }\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n if (role) return { role, resource };\n return null;\n}\n\nasync function highestShareRole(\n reg: ShareableResourceRegistration,\n resourceId: string,\n ctx: AccessContext,\n resource: any,\n): Promise<ShareRole | null> {\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n if (!normalizedUserEmail && !orgId) return null;\n if (!explicitSharesAllowedForResource(reg, resource, ctx)) return null;\n const db = reg.getDb() as any;\n\n const principalClauses: ReturnType<typeof and>[] = [];\n if (normalizedUserEmail) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"user\"),\n emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail),\n ),\n );\n }\n if (orgId) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"org\"),\n eq(reg.sharesTable.principalId, orgId),\n ),\n );\n }\n\n const rows = await db\n .select({ role: reg.sharesTable.role })\n .from(reg.sharesTable)\n .where(\n and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)),\n )\n .limit(10);\n\n let best: ShareRole | null = null;\n for (const r of rows as Array<{ role: ShareRole }>) {\n if (!best || ROLE_RANK[r.role] > ROLE_RANK[best]) best = r.role;\n }\n return best;\n}\n\n/**\n * Throw ForbiddenError if the current user can't act on this resource with at\n * least the given role. Used at the top of update/delete actions.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior). Pass `{ skipResourceBody: true }` as the fifth argument when\n * the caller only needs the access decision — see `ResolveAccessOptions`.\n */\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole?: ShareRole | \"owner\",\n ctx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" | undefined,\n ctx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" = \"viewer\",\n ctx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected> {\n const access = await resolveAccessImpl(\n resourceType,\n resourceId,\n ctx,\n options,\n );\n if (!access) {\n throw new ForbiddenError(`No access to ${resourceType} ${resourceId}`);\n }\n if (ROLE_RANK[access.role] < ROLE_RANK[minRole]) {\n throw new ForbiddenError(\n `Requires ${minRole} role on ${resourceType} ${resourceId} (have ${access.role})`,\n );\n }\n return access;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/sharing/access.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Access-control helpers for shareable resources.\n *\n * The access model combines:\n * 1. Direct ownership — `owner_email = currentUser`.\n * 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone\n * in the same org; `public` grants read to any authenticated user.\n * 3. Share rows — per-user or per-org grants in the `{type}_shares` table\n * with a role (`viewer | editor | admin`).\n *\n * Use `applyAccessFilter()` on list/read queries to filter rows the current\n * user can see. Use `assertAccess()` at the top of write actions to reject\n * callers who lack the required role.\n */\n\nimport { and, eq, or, sql, type SQL } from \"drizzle-orm\";\n\nimport { orgMembers } from \"../org/schema.js\";\nimport {\n getRequestAuthCapability,\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../server/request-context.js\";\nimport {\n listShareableResources,\n requireShareableResource,\n type ShareableResourceRegistration,\n} from \"./registry.js\";\nimport { ROLE_RANK, type ShareRole, type Visibility } from \"./schema.js\";\n\n/**\n * Find a registration by its drizzle table identity. Used to look up\n * per-resource policy flags (e.g. `allowPublic`) inside `accessFilter`,\n * which receives only the table — not the resource-type name.\n *\n * Identity is stable within a single bundle (Vite dedupes module instances);\n * the SSR/server side is the only caller, so per-bundle identity is fine.\n */\nfunction findRegistrationByTable(\n resourceTable: any,\n): ShareableResourceRegistration | undefined {\n for (const reg of listShareableResources()) {\n if (reg.resourceTable === resourceTable) return reg;\n }\n return undefined;\n}\n\nexport class ForbiddenError extends Error {\n statusCode = 403;\n constructor(message = \"Forbidden\") {\n super(message);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport interface AccessContext {\n userEmail?: string;\n orgId?: string;\n authCapability?: string;\n}\n\n/** Current request's access context. Pulls from request-context ALS. */\nexport function currentAccess(): AccessContext {\n return {\n userEmail: getRequestUserEmail(),\n orgId: getRequestOrgId(),\n authCapability: getRequestAuthCapability(),\n };\n}\n\nexport function resolveRegisteredAccessContext(\n reg: ShareableResourceRegistration | undefined,\n ctx: AccessContext,\n): AccessContext {\n if (!reg?.resolveAccessContext) return ctx;\n const resolved = reg.resolveAccessContext(ctx);\n return ctx.authCapability\n ? { ...resolved, authCapability: ctx.authCapability }\n : resolved;\n}\n\nfunction normalizeEmailForAccess(email: string | undefined): string | null {\n const normalized = email?.trim().toLowerCase();\n return normalized || null;\n}\n\nfunction emailColumnMatches(column: any, email: string): SQL {\n return sql`lower(${column}) = ${email}`;\n}\n\n/**\n * Real `org_members` membership, independent of the caller's currently\n * active org (`ctx.orgId`). `org`-visibility access must key off actual\n * membership — a user's active-org selection is a UI convenience, not a\n * statement of which orgs they belong to.\n *\n * Queries through the resource's own `reg.getDb()` — the same connection\n * every other lookup in this file uses — not the ambient `getDbExec()`,\n * since `org_members` lives in that same app database.\n */\nasync function isOrgMember(\n reg: ShareableResourceRegistration,\n memberOrgId: string,\n email: string,\n): Promise<boolean> {\n const db = reg.getDb() as any;\n const rows = await db\n .select({ id: orgMembers.id })\n .from(orgMembers)\n .where(\n and(\n eq(orgMembers.orgId, memberOrgId),\n emailColumnMatches(orgMembers.email, email),\n ),\n )\n .limit(1);\n return rows.length > 0;\n}\n\n/**\n * Build a Drizzle `WHERE` clause that admits rows the current user can see.\n * Pass the ownable resource table and its shares table; optional min role\n * (defaults to 'viewer') gates which share rows count.\n *\n * `visibility = 'public'` is intentionally NOT admitted by default. Public\n * means \"anyone with the link can view\" (still honoured by `resolveAccess`\n * for read-by-id), not \"appears in every signed-in user's list/sidebar.\"\n * Pass `{ includePublic: true }` for the rare list endpoint that wants\n * cross-user public discovery (a public template gallery, for example).\n *\n * Example:\n *\n * const rows = await db\n * .select()\n * .from(schema.documents)\n * .where(accessFilter(schema.documents, schema.documentShares));\n */\nexport function accessFilter(\n resourceTable: any,\n sharesTable: any,\n rawCtx: AccessContext = currentAccess(),\n minRole: ShareRole = \"viewer\",\n options: { includePublic?: boolean } = {},\n): SQL {\n // Defense in depth — resources registered with `allowPublic: false` must\n // never participate in cross-user \"public\" discovery, even if a caller\n // accidentally passes `includePublic: true` or if a stale public row sits\n // in the DB.\n const reg = findRegistrationByTable(resourceTable);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n const publicAllowed = reg?.allowPublic !== false;\n const includePublic = (options.includePublic ?? false) && publicAllowed;\n const clauses: SQL[] = [];\n\n if (normalizedUserEmail) {\n clauses.push(\n and(\n emailColumnMatches(resourceTable.ownerEmail, normalizedUserEmail),\n ownerScopeFilter(reg, resourceTable, ctx),\n )!,\n );\n }\n if (minRole === \"viewer\") {\n if (includePublic) {\n clauses.push(eq(resourceTable.visibility, \"public\"));\n }\n if (orgId) {\n clauses.push(\n and(\n eq(resourceTable.visibility, \"org\"),\n eq(resourceTable.orgId, orgId),\n )!,\n );\n }\n }\n if (normalizedUserEmail) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'user'\n and lower(${sharesTable.principalId}) = ${normalizedUserEmail}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n if (orgId) {\n const shareScope = restrictedShareScopeSql(reg, resourceTable, ctx);\n clauses.push(\n sql`exists (select 1 from ${sharesTable}\n where ${sharesTable.resourceId} = ${resourceTable.id}\n and ${sharesTable.principalType} = 'org'\n and ${sharesTable.principalId} = ${orgId}\n and ${shareScope}\n and ${minRoleSql(minRole)})`,\n );\n }\n\n return or(...clauses) ?? sql`1=0`;\n}\n\nfunction ownerScopeFilter(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n if (reg?.ownerAccessIgnoresOrg === true) return sql`1=1`;\n if (ctx.orgId) {\n // Rows created before org-scoping, or in solo mode, have no org_id. Keep\n // them manageable by their owner after the owner joins or switches into an\n // organization, while still keeping rows from other orgs out of scope.\n return or(\n eq(resourceTable.orgId, ctx.orgId),\n sql`${resourceTable.orgId} IS NULL`,\n )!;\n }\n return sql`${resourceTable.orgId} IS NULL`;\n}\n\nfunction ownerMatchesActiveScope(\n reg: ShareableResourceRegistration | undefined,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg?.ownerAccessIgnoresOrg === true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n if (!resourceOrgId) return true;\n return ctx.orgId === resourceOrgId;\n}\n\nfunction minRoleSql(minRole: ShareRole): SQL {\n if (minRole === \"viewer\") {\n // any role satisfies viewer\n return sql`1=1`;\n }\n if (minRole === \"editor\") {\n return sql`role in ('editor','admin')`;\n }\n return sql`role = 'admin'`;\n}\n\nfunction restrictedShareScopeSql(\n reg: ShareableResourceRegistration | undefined,\n resourceTable: any,\n ctx: AccessContext,\n): SQL {\n // Restricted resources (extensions) must stay inside their resource org even\n // if stale cross-org share rows already exist from older code or bad data.\n if (reg?.requireOrgMemberForUserShares !== true) return sql`1=1`;\n if (!ctx.orgId) return sql`1=0`;\n return eq(resourceTable.orgId, ctx.orgId);\n}\n\nfunction explicitSharesAllowedForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): boolean {\n if (reg.requireOrgMemberForUserShares !== true) return true;\n const resourceOrgId = resource?.orgId ?? null;\n return !!resourceOrgId && !!ctx.orgId && resourceOrgId === ctx.orgId;\n}\n\nexport interface ResolvedAccess {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** The resource row (already loaded). */\n resource: any;\n}\n\n/**\n * Minimal resource shape returned when a caller opts into a projected access\n * load via `{ skipResourceBody: true }`. Contains exactly the columns the\n * access-decision logic itself reads — identity, ownership, org scope, and\n * visibility — never a resource type's heavy body columns (`data`,\n * `content`, and similar blobs).\n */\nexport interface AccessProjectedResource {\n id: string;\n ownerEmail: string;\n orgId: string | null;\n visibility: Visibility;\n}\n\nexport interface ResolvedAccessProjected {\n /** Effective role: 'owner' for the resource owner, or the share role. */\n role: \"owner\" | ShareRole;\n /** Only the access-decision columns — not the full resource row. */\n resource: AccessProjectedResource;\n}\n\nexport interface ResolveAccessOptions {\n /**\n * When true, load only the columns the access decision itself needs\n * (`id`, `ownerEmail`, `orgId`, `visibility`) instead of the full resource\n * row. Use this when the caller only needs the access decision — not the\n * resource body — and wants to skip fetching heavy type-specific columns\n * (e.g. `data`/`content` blobs) on every `assertAccess`/`resolveAccess`\n * call.\n *\n * Silently ignored (falls back to a full row load) for any resource type\n * registered with a `publicAccessRole` *function* resolver, since that\n * callback can read arbitrary resource fields the projection would have\n * omitted — see `hasDynamicPublicAccessRoleResolver` below.\n *\n * Default: `false` — the full row is loaded, matching historical behavior.\n * Callers that need resource body fields (most call sites today — for\n * many resource types `resolveAccess().resource` doubles as the action's\n * primary data read) must NOT pass this.\n */\n skipResourceBody?: boolean;\n}\n\nasync function publicAccessRoleForResource(\n reg: ShareableResourceRegistration,\n resource: any,\n ctx: AccessContext,\n): Promise<ShareRole> {\n const roleResolver = reg.publicAccessRole;\n if (!roleResolver) return \"viewer\";\n return typeof roleResolver === \"function\"\n ? await roleResolver(resource, ctx)\n : roleResolver;\n}\n\nfunction higherShareRole(a: ShareRole, b: ShareRole | null): ShareRole {\n if (!b) return a;\n return ROLE_RANK[b] > ROLE_RANK[a] ? b : a;\n}\n\nfunction columnName(column: unknown): string | null {\n const candidate = column as\n | {\n name?: unknown;\n config?: { name?: unknown };\n _: { name?: unknown };\n }\n | undefined;\n const name = candidate?.name ?? candidate?.config?.name ?? candidate?._?.name;\n return typeof name === \"string\" && name ? name : null;\n}\n\nfunction missingColumnName(err: unknown): string | null {\n const error = err as { code?: string; message?: string } | undefined;\n const message = error?.message ?? \"\";\n if (\n error?.code !== \"42703\" &&\n !/no such column|does not exist/i.test(message)\n ) {\n return null;\n }\n const quoted = message.match(/column\\s+\"([^\"]+)\"\\s+does not exist/i)?.[1];\n if (quoted) return quoted;\n const sqlite = message.match(/no such column:\\s+[\"`]?([\\w.]+)[\"`]?/i)?.[1];\n if (sqlite) return sqlite.split(\".\").pop() ?? sqlite;\n return null;\n}\n\nfunction selectExistingColumns(\n columns: Record<string, unknown>,\n omittedColumnNames: Set<string>,\n): Record<string, unknown> {\n const selection: Record<string, unknown> = {};\n for (const [key, column] of Object.entries(columns)) {\n const name = columnName(column);\n if (!name || omittedColumnNames.has(name)) continue;\n selection[key] = column;\n }\n return selection;\n}\n\n/**\n * The fixed column set the access-decision logic in `resolveAccess` itself\n * reads: identity (`id`), ownership (`ownerEmail`, `orgId`), and the coarse\n * `visibility` flag. This is intentionally NOT caller-configurable — an\n * arbitrary caller-supplied column list could omit a column the access\n * checks below depend on and silently break authorization. Every ownable\n * resource table has these columns (`ownableColumns()` + a primary key\n * named `id`), so this projection is safe for any registration that\n * doesn't also read the row through a dynamic resolver (see below).\n */\nfunction projectedAccessColumns(resourceTable: any): Record<string, unknown> {\n return {\n id: resourceTable.id,\n ownerEmail: resourceTable.ownerEmail,\n orgId: resourceTable.orgId,\n visibility: resourceTable.visibility,\n };\n}\n\n/**\n * True when this registration's `publicAccessRole` is a callback rather than\n * a fixed role string. Callbacks receive the loaded `resource` and may read\n * arbitrary fields on it (see e.g. `templates/design`'s\n * `publicDesignAccessRole`, which inspects design-specific data). The\n * projected access load must never be used for these registrations — always\n * fall back to a full row load so the resolver sees the complete resource.\n */\nfunction hasDynamicPublicAccessRoleResolver(\n reg: ShareableResourceRegistration,\n): boolean {\n return typeof reg.publicAccessRole === \"function\";\n}\n\nasync function loadResourceForAccess(\n reg: ShareableResourceRegistration,\n resourceId: string,\n options: ResolveAccessOptions = {},\n): Promise<any | null> {\n const db = reg.getDb() as any;\n const useProjection =\n options.skipResourceBody === true &&\n !hasDynamicPublicAccessRoleResolver(reg);\n const projectedColumns = useProjection\n ? projectedAccessColumns(reg.resourceTable)\n : null;\n const omittedColumnNames = new Set<string>();\n\n for (let attempt = 0; attempt < 12; attempt++) {\n try {\n const query =\n !projectedColumns && omittedColumnNames.size === 0\n ? db.select()\n : db.select(\n selectExistingColumns(\n projectedColumns ?? reg.resourceTable,\n omittedColumnNames,\n ),\n );\n const [resource] = await query\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, resourceId));\n return resource ?? null;\n } catch (err) {\n const missing = missingColumnName(err);\n if (!missing || omittedColumnNames.has(missing)) throw err;\n omittedColumnNames.add(missing);\n console.warn(\n `[sharing] ${reg.type} access lookup omitted missing column ${missing}`,\n );\n }\n }\n\n throw new Error(\n `Could not load ${reg.type} ${resourceId}: too many missing resource columns`,\n );\n}\n\n/**\n * Return the effective role the current user has on a specific resource, or\n * null if they have no access. Loads the resource and relevant share rows.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior — for most resource types `.resource` here doubles as the\n * action's primary data read). Pass `{ skipResourceBody: true }` when the\n * caller only needs the access decision to load just the access-decision\n * columns instead — see `ResolveAccessOptions`.\n */\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected | null>;\nexport async function resolveAccess(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n return resolveAccessImpl(resourceType, resourceId, rawCtx, options);\n}\n\n/**\n * Un-overloaded implementation shared by the `resolveAccess` overloads and\n * called directly by `assertAccess` below. Kept separate from the exported\n * `resolveAccess` so internal callers passing a widened\n * `ResolveAccessOptions` (rather than a `{ skipResourceBody: true }` /\n * `{ skipResourceBody?: false }` literal) don't have to satisfy TypeScript's\n * overload resolution, which only matches against the declared overload\n * signatures, not the implementation signature.\n */\nasync function resolveAccessImpl(\n resourceType: string,\n resourceId: string,\n rawCtx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected | null> {\n const reg = requireShareableResource(resourceType);\n const ctx = resolveRegisteredAccessContext(reg, rawCtx);\n\n const resource = await loadResourceForAccess(reg, resourceId, options);\n if (!resource) return null;\n\n const { userEmail } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n\n if (\n normalizedUserEmail &&\n normalizeEmailForAccess(resource.ownerEmail) === normalizedUserEmail &&\n ownerMatchesActiveScope(reg, resource, ctx)\n ) {\n return { role: \"owner\", resource };\n }\n if (resource.visibility === \"public\" && reg.allowPublic !== false) {\n // No share row needed; default viewer unless the resource registration\n // deliberately grants a stronger public-by-link role or explicit shares\n // upgrade the current principal.\n const publicRole = await publicAccessRoleForResource(reg, resource, ctx);\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: higherShareRole(publicRole, role), resource };\n }\n // `visibility === \"public\"` on an `allowPublic: false` resource is treated\n // as private: only owner + explicit shares grant access. Falls through to\n // the explicit-share lookup below.\n //\n // Membership in the resource's own org, not equality with the caller's\n // currently active org: a caller can be a genuine member of the\n // resource's org while a *different* org is their active selection, and\n // `org` visibility should still admit them. Still requires some active\n // org to be set at all (`orgId`), matching the pre-existing behavior for\n // a caller with no active org.\n if (\n resource.visibility === \"org\" &&\n resource.orgId &&\n normalizedUserEmail &&\n (await isOrgMember(reg, resource.orgId, normalizedUserEmail))\n ) {\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n return { role: role ?? \"viewer\", resource };\n }\n const role = await highestShareRole(reg, resourceId, ctx, resource);\n if (role) return { role, resource };\n return null;\n}\n\nasync function highestShareRole(\n reg: ShareableResourceRegistration,\n resourceId: string,\n ctx: AccessContext,\n resource: any,\n): Promise<ShareRole | null> {\n const { userEmail, orgId } = ctx;\n const normalizedUserEmail = normalizeEmailForAccess(userEmail);\n if (!normalizedUserEmail && !orgId) return null;\n if (!explicitSharesAllowedForResource(reg, resource, ctx)) return null;\n const db = reg.getDb() as any;\n\n const principalClauses: ReturnType<typeof and>[] = [];\n if (normalizedUserEmail) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"user\"),\n emailColumnMatches(reg.sharesTable.principalId, normalizedUserEmail),\n ),\n );\n }\n if (orgId) {\n principalClauses.push(\n and(\n eq(reg.sharesTable.principalType, \"org\"),\n eq(reg.sharesTable.principalId, orgId),\n ),\n );\n }\n\n const rows = await db\n .select({ role: reg.sharesTable.role })\n .from(reg.sharesTable)\n .where(\n and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)),\n )\n .limit(10);\n\n let best: ShareRole | null = null;\n for (const r of rows as Array<{ role: ShareRole }>) {\n if (!best || ROLE_RANK[r.role] > ROLE_RANK[best]) best = r.role;\n }\n return best;\n}\n\n/**\n * Throw ForbiddenError if the current user can't act on this resource with at\n * least the given role. Used at the top of update/delete actions.\n *\n * By default the full resource row is loaded (unchanged historical\n * behavior). Pass `{ skipResourceBody: true }` as the fifth argument when\n * the caller only needs the access decision — see `ResolveAccessOptions`.\n */\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole?: ShareRole | \"owner\",\n ctx?: AccessContext,\n options?: { skipResourceBody?: false },\n): Promise<ResolvedAccess>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" | undefined,\n ctx: AccessContext | undefined,\n options: { skipResourceBody: true },\n): Promise<ResolvedAccessProjected>;\nexport async function assertAccess(\n resourceType: string,\n resourceId: string,\n minRole: ShareRole | \"owner\" = \"viewer\",\n ctx: AccessContext = currentAccess(),\n options: ResolveAccessOptions = {},\n): Promise<ResolvedAccess | ResolvedAccessProjected> {\n const access = await resolveAccessImpl(\n resourceType,\n resourceId,\n ctx,\n options,\n );\n if (!access) {\n throw new ForbiddenError(`No access to ${resourceType} ${resourceId}`);\n }\n if (ROLE_RANK[access.role] < ROLE_RANK[minRole]) {\n throw new ForbiddenError(\n `Requires ${minRole} role on ${resourceType} ${resourceId} (have ${access.role})`,\n );\n }\n return access;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.135.
|
|
3
|
+
"version": "0.135.1",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
package/src/sharing/access.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { and, eq, or, sql, type SQL } from "drizzle-orm";
|
|
17
17
|
|
|
18
|
+
import { orgMembers } from "../org/schema.js";
|
|
18
19
|
import {
|
|
19
20
|
getRequestAuthCapability,
|
|
20
21
|
getRequestUserEmail,
|
|
@@ -87,6 +88,35 @@ function emailColumnMatches(column: any, email: string): SQL {
|
|
|
87
88
|
return sql`lower(${column}) = ${email}`;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Real `org_members` membership, independent of the caller's currently
|
|
93
|
+
* active org (`ctx.orgId`). `org`-visibility access must key off actual
|
|
94
|
+
* membership — a user's active-org selection is a UI convenience, not a
|
|
95
|
+
* statement of which orgs they belong to.
|
|
96
|
+
*
|
|
97
|
+
* Queries through the resource's own `reg.getDb()` — the same connection
|
|
98
|
+
* every other lookup in this file uses — not the ambient `getDbExec()`,
|
|
99
|
+
* since `org_members` lives in that same app database.
|
|
100
|
+
*/
|
|
101
|
+
async function isOrgMember(
|
|
102
|
+
reg: ShareableResourceRegistration,
|
|
103
|
+
memberOrgId: string,
|
|
104
|
+
email: string,
|
|
105
|
+
): Promise<boolean> {
|
|
106
|
+
const db = reg.getDb() as any;
|
|
107
|
+
const rows = await db
|
|
108
|
+
.select({ id: orgMembers.id })
|
|
109
|
+
.from(orgMembers)
|
|
110
|
+
.where(
|
|
111
|
+
and(
|
|
112
|
+
eq(orgMembers.orgId, memberOrgId),
|
|
113
|
+
emailColumnMatches(orgMembers.email, email),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
.limit(1);
|
|
117
|
+
return rows.length > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
/**
|
|
91
121
|
* Build a Drizzle `WHERE` clause that admits rows the current user can see.
|
|
92
122
|
* Pass the ownable resource table and its shares table; optional min role
|
|
@@ -470,7 +500,7 @@ async function resolveAccessImpl(
|
|
|
470
500
|
const resource = await loadResourceForAccess(reg, resourceId, options);
|
|
471
501
|
if (!resource) return null;
|
|
472
502
|
|
|
473
|
-
const { userEmail
|
|
503
|
+
const { userEmail } = ctx;
|
|
474
504
|
const normalizedUserEmail = normalizeEmailForAccess(userEmail);
|
|
475
505
|
|
|
476
506
|
if (
|
|
@@ -491,7 +521,19 @@ async function resolveAccessImpl(
|
|
|
491
521
|
// `visibility === "public"` on an `allowPublic: false` resource is treated
|
|
492
522
|
// as private: only owner + explicit shares grant access. Falls through to
|
|
493
523
|
// the explicit-share lookup below.
|
|
494
|
-
|
|
524
|
+
//
|
|
525
|
+
// Membership in the resource's own org, not equality with the caller's
|
|
526
|
+
// currently active org: a caller can be a genuine member of the
|
|
527
|
+
// resource's org while a *different* org is their active selection, and
|
|
528
|
+
// `org` visibility should still admit them. Still requires some active
|
|
529
|
+
// org to be set at all (`orgId`), matching the pre-existing behavior for
|
|
530
|
+
// a caller with no active org.
|
|
531
|
+
if (
|
|
532
|
+
resource.visibility === "org" &&
|
|
533
|
+
resource.orgId &&
|
|
534
|
+
normalizedUserEmail &&
|
|
535
|
+
(await isOrgMember(reg, resource.orgId, normalizedUserEmail))
|
|
536
|
+
) {
|
|
495
537
|
const role = await highestShareRole(reg, resourceId, ctx, resource);
|
|
496
538
|
return { role: role ?? "viewer", resource };
|
|
497
539
|
}
|