@agent-native/core 0.114.0 → 0.114.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.
Files changed (162) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +9 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/build-compatibility.ts +77 -0
  5. package/corpus/core/src/client/route-chunk-recovery/index.ts +1 -0
  6. package/corpus/core/src/client/use-action.ts +32 -1
  7. package/corpus/core/src/deploy/build.ts +4 -1
  8. package/corpus/core/src/org/context.ts +56 -30
  9. package/corpus/core/src/server/action-routes.ts +52 -2
  10. package/corpus/core/src/server/auth-plugin.ts +9 -4
  11. package/corpus/core/src/server/better-auth-instance.ts +5 -1
  12. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  13. package/corpus/core/src/vite/client.ts +17 -0
  14. package/corpus/templates/content/AGENTS.md +2 -0
  15. package/corpus/templates/content/actions/_content-database-personal-view.ts +49 -5
  16. package/corpus/templates/content/actions/_content-document-access.ts +21 -0
  17. package/corpus/templates/content/actions/_content-favorites.ts +91 -0
  18. package/corpus/templates/content/actions/_content-files.ts +79 -23
  19. package/corpus/templates/content/actions/_content-sidebar-state.ts +24 -0
  20. package/corpus/templates/content/actions/_content-space-access.ts +15 -17
  21. package/corpus/templates/content/actions/_content-space-catalog-guards.ts +20 -0
  22. package/corpus/templates/content/actions/_content-spaces.ts +364 -167
  23. package/corpus/templates/content/actions/_database-source-utils.ts +8 -0
  24. package/corpus/templates/content/actions/_database-utils.ts +249 -15
  25. package/corpus/templates/content/actions/_delete-content-space.ts +84 -0
  26. package/corpus/templates/content/actions/_files-system-properties.ts +499 -0
  27. package/corpus/templates/content/actions/_local-file-documents.ts +0 -1
  28. package/corpus/templates/content/actions/_property-utils.ts +18 -9
  29. package/corpus/templates/content/actions/bind-content-database-source-field.ts +3 -0
  30. package/corpus/templates/content/actions/configure-document-property.ts +3 -0
  31. package/corpus/templates/content/actions/create-content-database.ts +2 -2
  32. package/corpus/templates/content/actions/create-content-space.ts +34 -0
  33. package/corpus/templates/content/actions/create-document.ts +4 -1
  34. package/corpus/templates/content/actions/delete-content-space.ts +19 -0
  35. package/corpus/templates/content/actions/delete-database-items.ts +28 -1
  36. package/corpus/templates/content/actions/delete-document-property.ts +3 -0
  37. package/corpus/templates/content/actions/delete-document.ts +39 -2
  38. package/corpus/templates/content/actions/duplicate-database-item.ts +6 -0
  39. package/corpus/templates/content/actions/duplicate-database-items.ts +6 -0
  40. package/corpus/templates/content/actions/duplicate-document-property.ts +3 -0
  41. package/corpus/templates/content/actions/get-content-database.ts +11 -2
  42. package/corpus/templates/content/actions/get-content-sidebar-state.ts +23 -0
  43. package/corpus/templates/content/actions/get-document.ts +32 -7
  44. package/corpus/templates/content/actions/import-content-source.ts +33 -1
  45. package/corpus/templates/content/actions/list-content-spaces.ts +34 -2
  46. package/corpus/templates/content/actions/list-documents.ts +39 -7
  47. package/corpus/templates/content/actions/reorder-document-property.ts +9 -0
  48. package/corpus/templates/content/actions/set-document-property.ts +41 -23
  49. package/corpus/templates/content/actions/share-local-file-document.ts +15 -0
  50. package/corpus/templates/content/actions/submit-content-database-form.ts +7 -0
  51. package/corpus/templates/content/actions/sync-local-folder-source.ts +14 -0
  52. package/corpus/templates/content/actions/update-content-database-view.ts +1 -0
  53. package/corpus/templates/content/actions/update-content-sidebar-state.ts +27 -0
  54. package/corpus/templates/content/actions/update-document.ts +158 -66
  55. package/corpus/templates/content/app/components/EmptyState.tsx +4 -57
  56. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +8 -1
  57. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +519 -178
  58. package/corpus/templates/content/app/components/editor/DocumentInfoPanel.tsx +39 -0
  59. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
  60. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +266 -7
  61. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +430 -152
  62. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +467 -66
  63. package/corpus/templates/content/app/components/editor/database/view-config.ts +9 -3
  64. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +720 -196
  65. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +173 -1
  66. package/corpus/templates/content/app/components/sidebar/document-sidebar-sections.ts +1 -1
  67. package/corpus/templates/content/app/components/sidebar/select-content-space.ts +91 -16
  68. package/corpus/templates/content/app/entry.client.tsx +5 -1
  69. package/corpus/templates/content/app/hooks/use-content-database.ts +84 -3
  70. package/corpus/templates/content/app/hooks/use-content-spaces.ts +60 -2
  71. package/corpus/templates/content/app/hooks/use-create-page.ts +30 -0
  72. package/corpus/templates/content/app/hooks/use-documents.ts +270 -19
  73. package/corpus/templates/content/app/i18n-data.ts +243 -20
  74. package/corpus/templates/content/app/lib/local-folder-picker-safety.ts +54 -0
  75. package/corpus/templates/content/app/lib/local-folder-picker-support.ts +18 -0
  76. package/corpus/templates/content/app/routes/_app.favorites.tsx +30 -0
  77. package/corpus/templates/content/app/routes/_app.local-files.tsx +18 -6
  78. package/corpus/templates/content/changelog/2026-07-17-workspace-files-now-stay-in-a-loading-state-until-automatic-.md +6 -0
  79. package/corpus/templates/content/changelog/2026-07-17-workspace-switching-now-opens-the-selected-files-database-pr.md +6 -0
  80. package/corpus/templates/content/changelog/2026-07-17-workspaces-now-expand-above-their-own-files-list-without-dup.md +6 -0
  81. package/corpus/templates/content/changelog/2026-07-18-create-named-workspaces-each-with-its-own-private-files-data.md +6 -0
  82. package/corpus/templates/content/changelog/2026-07-18-favorites-align-with-workspace-and-file-rows-in-the-sidebar.md +6 -0
  83. package/corpus/templates/content/changelog/2026-07-18-local-folder-selection-no-longer-invokes-the-unsafe-native-p.md +6 -0
  84. package/corpus/templates/content/changelog/2026-07-18-made-content-workspaces-independently-expandable-restored-or.md +6 -0
  85. package/corpus/templates/content/changelog/2026-07-18-page-details-now-live-in-a-shared-info-and-comments-rail-dat.md +6 -0
  86. package/corpus/templates/content/changelog/2026-07-18-workspace-database-views-now-control-sidebar-workspace-navig.md +6 -0
  87. package/corpus/templates/content/changelog/2026-07-18-workspace-navigation-now-aligns-favorites-with-workspace-pag.md +6 -0
  88. package/corpus/templates/content/changelog/2026-07-19-favorites-now-open-as-a-table-and-compact-breadcrumbs-make-w.md +6 -0
  89. package/corpus/templates/content/changelog/2026-07-19-files-databases-now-start-unfiltered-sidebar-rows-exactly-fo.md +6 -0
  90. package/corpus/templates/content/changelog/2026-07-19-sidebar-page-trees-now-open-only-when-expanded-remember-thei.md +6 -0
  91. package/corpus/templates/content/changelog/2026-07-19-workspace-file-databases-now-use-the-workspace-name-open-as-.md +6 -0
  92. package/corpus/templates/content/changelog/2026-07-19-workspace-navigation-now-stays-stable-on-hover-uses-folder-i.md +6 -0
  93. package/corpus/templates/content/changelog/2026-07-19-workspace-rows-now-use-folder-icons-and-deleting-a-user-crea.md +6 -0
  94. package/corpus/templates/content/changelog/2026-07-19-workspaces-can-be-created-without-leaving-the-workspaces-dat.md +6 -0
  95. package/corpus/templates/content/e2e/global-setup.ts +3 -2
  96. package/corpus/templates/content/parity/matrix.md +28 -28
  97. package/corpus/templates/content/parity/matrix.ts +8 -2
  98. package/corpus/templates/content/server/db/index.ts +1 -0
  99. package/corpus/templates/content/server/db/schema.ts +10 -0
  100. package/corpus/templates/content/server/lib/documents.ts +12 -7
  101. package/corpus/templates/content/server/plugins/db.ts +17 -0
  102. package/corpus/templates/content/shared/api.ts +9 -0
  103. package/corpus/templates/content/shared/database-form.ts +11 -2
  104. package/corpus/templates/content/vite.config.ts +1 -0
  105. package/dist/client/build-compatibility.d.ts +11 -0
  106. package/dist/client/build-compatibility.d.ts.map +1 -0
  107. package/dist/client/build-compatibility.js +56 -0
  108. package/dist/client/build-compatibility.js.map +1 -0
  109. package/dist/client/route-chunk-recovery/index.d.ts +1 -0
  110. package/dist/client/route-chunk-recovery/index.d.ts.map +1 -1
  111. package/dist/client/route-chunk-recovery/index.js +1 -0
  112. package/dist/client/route-chunk-recovery/index.js.map +1 -1
  113. package/dist/client/use-action.d.ts.map +1 -1
  114. package/dist/client/use-action.js +19 -1
  115. package/dist/client/use-action.js.map +1 -1
  116. package/dist/collab/routes.d.ts +1 -1
  117. package/dist/collab/struct-routes.d.ts +1 -1
  118. package/dist/deploy/build.js +2 -1
  119. package/dist/deploy/build.js.map +1 -1
  120. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  121. package/dist/notifications/routes.d.ts +1 -1
  122. package/dist/observability/routes.d.ts +5 -5
  123. package/dist/org/context.d.ts +4 -2
  124. package/dist/org/context.d.ts.map +1 -1
  125. package/dist/org/context.js +44 -22
  126. package/dist/org/context.js.map +1 -1
  127. package/dist/progress/routes.d.ts +1 -1
  128. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  129. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  130. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  131. package/dist/resources/handlers.d.ts +1 -1
  132. package/dist/secrets/routes.d.ts +9 -9
  133. package/dist/server/action-routes.d.ts.map +1 -1
  134. package/dist/server/action-routes.js +32 -2
  135. package/dist/server/action-routes.js.map +1 -1
  136. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  137. package/dist/server/auth-plugin.d.ts.map +1 -1
  138. package/dist/server/auth-plugin.js +9 -5
  139. package/dist/server/auth-plugin.js.map +1 -1
  140. package/dist/server/better-auth-instance.d.ts.map +1 -1
  141. package/dist/server/better-auth-instance.js +4 -1
  142. package/dist/server/better-auth-instance.js.map +1 -1
  143. package/dist/server/transcribe-voice.d.ts +1 -1
  144. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  145. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  146. package/dist/shared/mcp-embed-headers.js +1 -1
  147. package/dist/shared/mcp-embed-headers.js.map +1 -1
  148. package/dist/vite/client.d.ts +6 -0
  149. package/dist/vite/client.d.ts.map +1 -1
  150. package/dist/vite/client.js +8 -0
  151. package/dist/vite/client.js.map +1 -1
  152. package/package.json +1 -1
  153. package/src/client/build-compatibility.ts +77 -0
  154. package/src/client/route-chunk-recovery/index.ts +1 -0
  155. package/src/client/use-action.ts +32 -1
  156. package/src/deploy/build.ts +4 -1
  157. package/src/org/context.ts +56 -30
  158. package/src/server/action-routes.ts +52 -2
  159. package/src/server/auth-plugin.ts +9 -4
  160. package/src/server/better-auth-instance.ts +5 -1
  161. package/src/shared/mcp-embed-headers.ts +1 -1
  162. package/src/vite/client.ts +17 -0
@@ -0,0 +1,91 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { and, eq, inArray, sql } from "drizzle-orm";
4
+
5
+ import { schema } from "../server/db/index.js";
6
+ import { normalizeContentSpaceEmail } from "./_content-space-access.js";
7
+ import {
8
+ personalContentSpaceId,
9
+ systemIdsForContentSpace,
10
+ } from "./_content-spaces.js";
11
+
12
+ type Db = any;
13
+
14
+ export function favoritesSystemIds(userEmail: string) {
15
+ return systemIdsForContentSpace(
16
+ personalContentSpaceId(normalizeContentSpaceEmail(userEmail)),
17
+ "favorites",
18
+ );
19
+ }
20
+
21
+ export async function favoriteDocumentIds(
22
+ db: Db,
23
+ userEmail: string,
24
+ documentIds: string[],
25
+ ) {
26
+ if (documentIds.length === 0) return new Set<string>();
27
+ const favoritesDatabaseId = favoritesSystemIds(userEmail).databaseId;
28
+ const rows = await db
29
+ .select({ documentId: schema.contentDatabaseItems.documentId })
30
+ .from(schema.contentDatabaseItems)
31
+ .where(
32
+ and(
33
+ eq(schema.contentDatabaseItems.databaseId, favoritesDatabaseId),
34
+ inArray(schema.contentDatabaseItems.documentId, documentIds),
35
+ ),
36
+ );
37
+ return new Set(rows.map((row: { documentId: string }) => row.documentId));
38
+ }
39
+
40
+ export async function setFavoriteMembership(args: {
41
+ db: Db;
42
+ userEmail: string;
43
+ documentId: string;
44
+ favorite: boolean;
45
+ now: string;
46
+ }) {
47
+ const email = normalizeContentSpaceEmail(args.userEmail);
48
+ const favoritesDatabaseId = favoritesSystemIds(email).databaseId;
49
+ const [existing] = await args.db
50
+ .select({ id: schema.contentDatabaseItems.id })
51
+ .from(schema.contentDatabaseItems)
52
+ .where(
53
+ and(
54
+ eq(schema.contentDatabaseItems.databaseId, favoritesDatabaseId),
55
+ eq(schema.contentDatabaseItems.documentId, args.documentId),
56
+ ),
57
+ );
58
+
59
+ if (!args.favorite) {
60
+ if (existing) {
61
+ await args.db
62
+ .delete(schema.contentDatabaseItems)
63
+ .where(eq(schema.contentDatabaseItems.id, existing.id));
64
+ }
65
+ return false;
66
+ }
67
+ if (existing) return true;
68
+
69
+ const [position] = await args.db
70
+ .select({ max: sql<number>`COALESCE(MAX(position), -1)` })
71
+ .from(schema.contentDatabaseItems)
72
+ .where(eq(schema.contentDatabaseItems.databaseId, favoritesDatabaseId));
73
+ const id = `content_database_item_${createHash("sha256")
74
+ .update(`${favoritesDatabaseId}:${args.documentId}`)
75
+ .digest("hex")
76
+ .slice(0, 32)}`;
77
+ await args.db
78
+ .insert(schema.contentDatabaseItems)
79
+ .values({
80
+ id,
81
+ ownerEmail: email,
82
+ orgId: null,
83
+ databaseId: favoritesDatabaseId,
84
+ documentId: args.documentId,
85
+ position: Number(position?.max ?? -1) + 1,
86
+ createdAt: args.now,
87
+ updatedAt: args.now,
88
+ })
89
+ .onConflictDoNothing();
90
+ return true;
91
+ }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  import { accessFilter } from "@agent-native/core/sharing";
4
- import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
4
+ import { and, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
5
5
 
6
6
  import { schema } from "../server/db/index.js";
7
7
  import {
@@ -62,9 +62,44 @@ async function reconcileDocuments(args: {
62
62
  args.filesDatabases.map((database) => [database.spaceId, database]),
63
63
  );
64
64
  const filesDatabaseIds = args.filesDatabases.map((database) => database.id);
65
- const filesBackingDocumentIds = new Set(
66
- args.filesDatabases.map((database) => database.documentId),
67
- );
65
+ const documentIds = args.documents.map((document) => document.id);
66
+ const [systemDatabaseDocuments, workspaceReferenceDocuments] =
67
+ documentIds.length > 0
68
+ ? await Promise.all([
69
+ args.db
70
+ .select({ id: schema.contentDatabases.documentId })
71
+ .from(schema.contentDatabases)
72
+ .where(
73
+ and(
74
+ inArray(schema.contentDatabases.documentId, documentIds),
75
+ isNotNull(schema.contentDatabases.systemRole),
76
+ ),
77
+ ),
78
+ args.db
79
+ .select({ id: schema.contentSpaceCatalogItems.documentId })
80
+ .from(schema.contentSpaceCatalogItems)
81
+ .innerJoin(
82
+ schema.contentDatabases,
83
+ eq(
84
+ schema.contentDatabases.id,
85
+ schema.contentSpaceCatalogItems.catalogDatabaseId,
86
+ ),
87
+ )
88
+ .where(
89
+ and(
90
+ inArray(
91
+ schema.contentSpaceCatalogItems.documentId,
92
+ documentIds,
93
+ ),
94
+ eq(schema.contentDatabases.systemRole, "workspaces"),
95
+ ),
96
+ ),
97
+ ])
98
+ : [[], []];
99
+ const excludedDocumentIds = new Set([
100
+ ...systemDatabaseDocuments.map((document: any) => document.id),
101
+ ...workspaceReferenceDocuments.map((document: any) => document.id),
102
+ ]);
68
103
  const existingItems = filesDatabaseIds.length
69
104
  ? await args.db
70
105
  .select()
@@ -93,7 +128,7 @@ async function reconcileDocuments(args: {
93
128
  const inserts: Array<typeof schema.contentDatabaseItems.$inferInsert> = [];
94
129
  for (const document of args.documents) {
95
130
  const existing = itemsByDocument.get(document.id) ?? [];
96
- if (filesBackingDocumentIds.has(document.id)) {
131
+ if (excludedDocumentIds.has(document.id)) {
97
132
  for (const item of existing) deleteIds.add(item.id);
98
133
  continue;
99
134
  }
@@ -239,24 +274,23 @@ export async function reconcileContentFilesMemberships(
239
274
  const email = normalizeContentSpaceEmail(userEmail);
240
275
  const memberships = await listContentOrganizationMemberships(email);
241
276
  const personalSpaceId = personalContentSpaceId(email);
242
- const orgSpaceIds = new Map<string, string>();
277
+ const orgSpaces = new Map<
278
+ string,
279
+ { spaceId: string; canReconcilePrivateDocuments: boolean }
280
+ >();
243
281
  for (const membership of memberships) {
244
282
  const spaceId = organizationContentSpaceId(membership.orgId);
245
- try {
246
- await resolveContentSpaceAccess(spaceId, "editor");
247
- orgSpaceIds.set(membership.orgId, spaceId);
248
- } catch (error) {
249
- if (
250
- error instanceof Error &&
251
- error.message ===
252
- `Editor access is required for Content space "${spaceId}"`
253
- ) {
254
- continue;
255
- }
256
- throw error;
257
- }
283
+ await resolveContentSpaceAccess(spaceId);
284
+ orgSpaces.set(membership.orgId, {
285
+ spaceId,
286
+ canReconcilePrivateDocuments:
287
+ membership.role === "owner" || membership.role === "admin",
288
+ });
258
289
  }
259
- const accessibleSpaceIds = [personalSpaceId, ...orgSpaceIds.values()];
290
+ const accessibleSpaceIds = [
291
+ personalSpaceId,
292
+ ...[...orgSpaces.values()].map(({ spaceId }) => spaceId),
293
+ ];
260
294
  const now = new Date().toISOString();
261
295
  const result: ContentFilesReconciliation = {
262
296
  assignedSpaces: 0,
@@ -288,7 +322,7 @@ export async function reconcileContentFilesMemberships(
288
322
  );
289
323
  result.assignedSpaces += personalLegacyDocuments.length;
290
324
  }
291
- for (const [orgId, spaceId] of orgSpaceIds) {
325
+ for (const [orgId, orgSpace] of orgSpaces) {
292
326
  const legacyDocuments = await tx
293
327
  .select({ id: schema.documents.id })
294
328
  .from(schema.documents)
@@ -296,12 +330,19 @@ export async function reconcileContentFilesMemberships(
296
330
  and(
297
331
  eq(schema.documents.orgId, orgId),
298
332
  isNull(schema.documents.spaceId),
333
+ orgSpace.canReconcilePrivateDocuments
334
+ ? undefined
335
+ : or(
336
+ eq(schema.documents.ownerEmail, email),
337
+ eq(schema.documents.visibility, "org"),
338
+ eq(schema.documents.visibility, "public"),
339
+ ),
299
340
  ),
300
341
  );
301
342
  if (!legacyDocuments.length) continue;
302
343
  await tx
303
344
  .update(schema.documents)
304
- .set({ spaceId, updatedAt: now })
345
+ .set({ spaceId: orgSpace.spaceId, updatedAt: now })
305
346
  .where(
306
347
  inArray(
307
348
  schema.documents.id,
@@ -311,10 +352,25 @@ export async function reconcileContentFilesMemberships(
311
352
  result.assignedSpaces += legacyDocuments.length;
312
353
  }
313
354
 
314
- const documents = await tx
355
+ const accessibleDocuments = await tx
315
356
  .select()
316
357
  .from(schema.documents)
317
358
  .where(inArray(schema.documents.spaceId, accessibleSpaceIds));
359
+ const orgSpaceById = new Map(
360
+ [...orgSpaces.values()].map((space) => [space.spaceId, space]),
361
+ );
362
+ const documents = accessibleDocuments.filter((document: any) => {
363
+ if (document.spaceId === personalSpaceId) {
364
+ return normalizeContentSpaceEmail(document.ownerEmail) === email;
365
+ }
366
+ const orgSpace = orgSpaceById.get(document.spaceId);
367
+ return (
368
+ orgSpace?.canReconcilePrivateDocuments === true ||
369
+ normalizeContentSpaceEmail(document.ownerEmail) === email ||
370
+ document.visibility === "org" ||
371
+ document.visibility === "public"
372
+ );
373
+ });
318
374
  const filesDatabases = await tx
319
375
  .select()
320
376
  .from(schema.contentDatabases)
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+
3
+ export const CONTENT_SIDEBAR_STATE_VERSION = 1;
4
+ export const CONTENT_SIDEBAR_STATE_SETTING_KEY = "content-sidebar-state";
5
+
6
+ const expandedIdSchema = z.string().min(1).max(256);
7
+
8
+ export const contentSidebarStateSchema = z.object({
9
+ version: z.literal(CONTENT_SIDEBAR_STATE_VERSION),
10
+ expandedWorkspaceIds: z.array(expandedIdSchema).max(1_000),
11
+ expandedDocumentIds: z.array(expandedIdSchema).max(5_000),
12
+ });
13
+
14
+ export type ContentSidebarState = z.infer<typeof contentSidebarStateSchema>;
15
+
16
+ export function normalizeContentSidebarState(value: unknown) {
17
+ const parsed = contentSidebarStateSchema.safeParse(value);
18
+ if (!parsed.success) return null;
19
+ return {
20
+ ...parsed.data,
21
+ expandedWorkspaceIds: [...new Set(parsed.data.expandedWorkspaceIds)],
22
+ expandedDocumentIds: [...new Set(parsed.data.expandedDocumentIds)],
23
+ };
24
+ }
@@ -1,25 +1,9 @@
1
1
  import { getDbExec } from "@agent-native/core/db";
2
- import { table, text } from "@agent-native/core/db/schema";
3
2
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
4
3
  import { and, eq, sql } from "drizzle-orm";
5
4
 
6
5
  import { getDb, schema } from "../server/db/index.js";
7
6
 
8
- // Keep these lightweight table references local. Importing the public org
9
- // barrel also initializes auth's long-lived cleanup timer, which breaks test
10
- // suites that intentionally drain fake timers.
11
- const organizations = table("organizations", {
12
- id: text("id").primaryKey(),
13
- name: text("name").notNull(),
14
- createdBy: text("created_by").notNull(),
15
- });
16
-
17
- const orgMembers = table("org_members", {
18
- orgId: text("org_id").notNull(),
19
- email: text("email").notNull(),
20
- role: text("role").notNull(),
21
- });
22
-
23
7
  export type ContentSpaceRole = "viewer" | "editor" | "owner";
24
8
 
25
9
  export type ContentSpaceAccess = {
@@ -40,6 +24,10 @@ export async function getContentOrganizationMembership(
40
24
  options: { db?: any } = {},
41
25
  ): Promise<{ role: string; name: string; createdBy: string } | null> {
42
26
  if (options.db) {
27
+ // The canonical tables carry the active database dialect. Loading them only
28
+ // for transaction-scoped checks avoids initializing auth timers elsewhere.
29
+ const { organizations, orgMembers } =
30
+ await import("@agent-native/core/org");
43
31
  const [row] = await options.db
44
32
  .select({
45
33
  role: orgMembers.role,
@@ -130,7 +118,7 @@ export async function listContentOrganizationMemberships(userEmail: string) {
130
118
 
131
119
  export async function resolveContentSpaceAccess(
132
120
  spaceId: string,
133
- requiredRole: "viewer" | "editor" = "viewer",
121
+ requiredRole: "viewer" | "contributor" | "editor" = "viewer",
134
122
  options: { db?: any } = {},
135
123
  ): Promise<ContentSpaceAccess> {
136
124
  const userEmail = getRequestUserEmail();
@@ -167,6 +155,16 @@ export async function resolveContentSpaceAccess(
167
155
  : membership.role === "admin"
168
156
  ? "editor"
169
157
  : "viewer";
158
+ if (
159
+ requiredRole === "contributor" &&
160
+ membership.role !== "owner" &&
161
+ membership.role !== "admin" &&
162
+ membership.role !== "member"
163
+ ) {
164
+ throw new Error(
165
+ `Contributor access is required for Content space "${spaceId}"`,
166
+ );
167
+ }
170
168
  if (requiredRole === "editor" && role === "viewer") {
171
169
  throw new Error(`Editor access is required for Content space "${spaceId}"`);
172
170
  }
@@ -0,0 +1,20 @@
1
+ import { inArray } from "drizzle-orm";
2
+
3
+ import { schema } from "../server/db/index.js";
4
+
5
+ type Db = any;
6
+
7
+ export async function assertNotWorkspaceCatalogDocuments(
8
+ db: Db,
9
+ documentIds: string[],
10
+ operation: string,
11
+ ) {
12
+ if (documentIds.length === 0) return;
13
+ const [workspaceReference] = await db
14
+ .select({ id: schema.contentSpaceCatalogItems.id })
15
+ .from(schema.contentSpaceCatalogItems)
16
+ .where(inArray(schema.contentSpaceCatalogItems.documentId, documentIds));
17
+ if (workspaceReference) {
18
+ throw new Error(`Workspace references cannot be ${operation} as pages`);
19
+ }
20
+ }