@agent-native/core 0.162.0 → 0.163.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 (132) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +27 -2
  3. package/corpus/templates/analytics/server/plugins/collab.ts +1 -0
  4. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
  5. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
  6. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
  7. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  8. package/corpus/templates/clips/app/lib/camera-composite.ts +15 -1
  9. package/corpus/templates/clips/changelog/2026-08-18-clips-no-longer-crashes-when-a-camera-bubble-starts-at-a-tin.md +6 -0
  10. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +27 -2
  11. package/corpus/templates/content/actions/create-content-database.ts +1 -1
  12. package/corpus/templates/content/actions/create-document.ts +1 -1
  13. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +27 -2
  14. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +27 -2
  15. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +6 -1
  16. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +27 -2
  17. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
  18. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
  19. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
  20. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
  21. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
  22. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
  23. package/corpus/templates/slides/DEVELOPING.md +17 -0
  24. package/corpus/templates/slides/actions/get-deck-access-status.ts +67 -0
  25. package/corpus/templates/slides/actions/request-deck-access.ts +569 -0
  26. package/corpus/templates/slides/agent-native.json +3 -0
  27. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +3 -3
  28. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +3 -3
  29. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -1
  30. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  31. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +1 -1
  32. package/corpus/templates/slides/app/components/layout/Header.tsx +2 -0
  33. package/corpus/templates/slides/app/hooks/use-deck-access.ts +51 -0
  34. package/corpus/templates/slides/app/i18n/en-US.ts +12 -0
  35. package/corpus/templates/slides/app/lib/deck-editor-loading.ts +7 -0
  36. package/corpus/templates/slides/app/pages/DeckEditor.tsx +173 -33
  37. package/corpus/templates/slides/changelog/2026-08-18-private-deck-links-now-explain-access-and-let-viewers-notify.md +6 -0
  38. package/corpus/templates/slides/server/db/schema.ts +10 -0
  39. package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
  40. package/corpus/templates/slides/server/plugins/db.ts +17 -0
  41. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
  42. package/dist/app-config/schema.d.ts +1 -0
  43. package/dist/app-config/workspace.d.ts +1 -0
  44. package/dist/app-config/workspace.js +8 -0
  45. package/dist/chat-threads/schema.d.ts +2 -2
  46. package/dist/client/analytics.d.ts +1 -0
  47. package/dist/client/analytics.js +19 -5
  48. package/dist/client/org/TeamPage.js +15 -2
  49. package/dist/client/org/hooks.d.ts +2 -0
  50. package/dist/client/org/hooks.js +12 -0
  51. package/dist/client/org/index.d.ts +2 -2
  52. package/dist/client/org/index.js +1 -1
  53. package/dist/client/sharing/ShareButton.js +43 -18
  54. package/dist/client/sharing/useShareButtonController.d.ts +12 -2
  55. package/dist/client/sharing/useShareButtonController.js +38 -8
  56. package/dist/collab/awareness.d.ts +2 -2
  57. package/dist/collab/routes.d.ts +1 -1
  58. package/dist/collab/storage.d.ts +2 -0
  59. package/dist/collab/storage.js +7 -0
  60. package/dist/config.js +11 -1
  61. package/dist/dashboard-storage/schema.d.ts +2 -2
  62. package/dist/data-programs/schema.d.ts +2 -2
  63. package/dist/deploy/build.d.ts +1 -1
  64. package/dist/deploy/build.js +89 -12
  65. package/dist/extensions/schema.d.ts +2 -2
  66. package/dist/extensions/store.js +3 -0
  67. package/dist/localization/core-messages/ar-SA.js +1 -0
  68. package/dist/localization/core-messages/de-DE.js +1 -0
  69. package/dist/localization/core-messages/en-US.d.ts +1 -0
  70. package/dist/localization/core-messages/en-US.js +1 -0
  71. package/dist/localization/core-messages/es-ES.js +1 -0
  72. package/dist/localization/core-messages/fr-FR.js +1 -0
  73. package/dist/localization/core-messages/hi-IN.js +1 -0
  74. package/dist/localization/core-messages/ja-JP.js +1 -0
  75. package/dist/localization/core-messages/ko-KR.js +1 -0
  76. package/dist/localization/core-messages/pt-BR.js +1 -0
  77. package/dist/localization/core-messages/zh-CN.js +1 -0
  78. package/dist/localization/core-messages/zh-TW.js +1 -0
  79. package/dist/localization/core-messages.d.ts +1 -0
  80. package/dist/localization/default-messages.d.ts +4 -0
  81. package/dist/localization/default-messages.js +4 -0
  82. package/dist/observability/routes.d.ts +1 -1
  83. package/dist/observability/tracking-identity.js +2 -0
  84. package/dist/org/handlers.d.ts +6 -1
  85. package/dist/org/handlers.js +43 -0
  86. package/dist/org/index.d.ts +3 -3
  87. package/dist/org/index.js +2 -2
  88. package/dist/org/migrations.js +33 -0
  89. package/dist/org/plugin.js +10 -1
  90. package/dist/org/schema.d.ts +315 -0
  91. package/dist/org/schema.js +20 -1
  92. package/dist/org/types.d.ts +3 -0
  93. package/dist/org/workspace-app-access.d.ts +10 -0
  94. package/dist/org/workspace-app-access.js +179 -0
  95. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  96. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  97. package/dist/secrets/routes.d.ts +9 -9
  98. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  99. package/dist/server/auth.js +14 -1
  100. package/dist/server/collab-plugin.d.ts +6 -0
  101. package/dist/server/collab-plugin.js +27 -6
  102. package/dist/server/deploy-environment.d.ts +1 -1
  103. package/dist/server/deploy-environment.js +40 -2
  104. package/dist/server/poll.js +6 -0
  105. package/dist/server/realtime-token.d.ts +1 -1
  106. package/dist/server/sentry-config.js +8 -4
  107. package/dist/server/sentry.js +4 -0
  108. package/dist/sharing/access.d.ts +2 -2
  109. package/dist/sharing/access.js +56 -3
  110. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  111. package/dist/sharing/actions/list-resource-shares.js +26 -1
  112. package/dist/sharing/actions/share-resource.d.ts +1 -1
  113. package/dist/sharing/actions/share-resource.js +23 -4
  114. package/dist/sharing/actions/unshare-resource.d.ts +1 -1
  115. package/dist/sharing/actions/unshare-resource.js +1 -1
  116. package/dist/sharing/registry.d.ts +12 -0
  117. package/dist/sharing/schema.d.ts +4 -4
  118. package/dist/sharing/schema.js +2 -2
  119. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  120. package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  121. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
  122. package/dist/tracking/registry.js +8 -6
  123. package/dist/vite/client.js +5 -0
  124. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +9 -1
  125. package/dist/workspace-connections/actions/list-workspace-user-groups.js +9 -4
  126. package/dist/workspace-connections/groups.d.ts +2 -0
  127. package/dist/workspace-connections/groups.js +16 -5
  128. package/docs/content/deployment.mdx +19 -5
  129. package/package.json +1 -1
  130. package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  131. package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  132. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- status?: undefined;
38
37
  ok?: undefined;
38
+ status?: undefined;
39
39
  } | {
40
+ error?: undefined;
40
41
  ok: boolean;
41
42
  status: string;
42
- error?: undefined;
43
43
  } | {
44
+ ok?: undefined;
44
45
  error: string;
45
46
  removed?: undefined;
46
- ok?: undefined;
47
47
  } | {
48
+ error?: undefined;
48
49
  ok: boolean;
49
50
  removed: boolean;
50
- error?: undefined;
51
51
  }>>;
52
52
  /**
53
53
  * POST /_agent-native/secrets/:key/test — validate an optional candidate value
54
54
  * or the current stored value without changing anything.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ ok?: undefined;
57
58
  error: string;
58
59
  note?: undefined;
59
- ok?: undefined;
60
60
  } | {
61
+ error?: undefined;
61
62
  ok: boolean;
62
63
  note?: undefined;
63
- error?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  ok: boolean;
66
67
  note: string;
67
- error?: undefined;
68
68
  } | {
69
69
  note?: undefined;
70
70
  ok: boolean;
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
95
95
  export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
96
96
  error: string;
97
97
  } | {
98
+ error?: undefined;
98
99
  ok: boolean;
99
100
  key: string;
100
- error?: undefined;
101
101
  } | {
102
+ error?: undefined;
102
103
  ok: boolean;
103
104
  removed: boolean;
104
- error?: undefined;
105
105
  }>>;
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { defineEventHandler, getMethod, getQuery, getRequestIP, setResponseHeader, setResponseStatus, getCookie, setCookie, deleteCookie, getHeader, } from "h3";
3
3
  import { getAppConfig } from "../app-config/index.js";
4
+ import { isWorkspaceAppAccessAllowed } from "../org/workspace-app-access.js";
4
5
  import { EMBED_START_PATH } from "../shared/embed-auth.js";
5
6
  import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
6
7
  import { FIRST_RUN_ONBOARDING_COOKIE, FIRST_RUN_ONBOARDING_MAX_AGE, } from "../shared/first-run-onboarding.js";
@@ -2170,8 +2171,20 @@ function createAuthGuardFn(app) {
2170
2171
  return;
2171
2172
  }
2172
2173
  const session = await getSession(event);
2173
- if (session)
2174
+ if (session) {
2175
+ const workspaceAppId = getAppConfig().app.workspaceId?.trim() || "";
2176
+ if (workspaceAppId &&
2177
+ workspaceAppId !== "dispatch" &&
2178
+ (p.startsWith("/api/") || p.startsWith("/_agent-native/")) &&
2179
+ !(await isWorkspaceAppAccessAllowed(workspaceAppId, {
2180
+ email: session.email,
2181
+ orgId: session.orgId,
2182
+ }))) {
2183
+ setResponseStatus(event, 403);
2184
+ return { error: "You do not have access to this workspace app." };
2185
+ }
2174
2186
  return;
2187
+ }
2175
2188
  if (p.startsWith("/api/") || p.startsWith("/_agent-native/")) {
2176
2189
  setResponseStatus(event, 401);
2177
2190
  return { error: "Unauthorized" };
@@ -41,6 +41,8 @@ export interface CollabPluginOptions {
41
41
  idColumn?: string;
42
42
  /** Whether to auto-seed existing documents on startup. Default: true */
43
43
  autoSeed?: boolean;
44
+ /** Map a source-table id to the id used by the collab document store. */
45
+ resolveCollabDocumentId?: (sourceId: string) => string;
44
46
  /**
45
47
  * Callback invoked after a collab update to sync the content column.
46
48
  * If not provided, the plugin auto-syncs using table/contentColumn/idColumn.
@@ -78,4 +80,8 @@ export interface CollabPluginOptions {
78
80
  }
79
81
  export declare function normalizeCollabAccess(options: Pick<CollabPluginOptions, "access" | "resourceType" | "resolveResourceId">): NormalizedCollabAccess;
80
82
  export declare function createCollabPlugin(options?: CollabPluginOptions): NitroPluginDef;
83
+ export declare function selectUnseededCollabRows(rows: ReadonlyArray<Record<string, unknown>>, idColumn: string, existingDocIds: ReadonlySet<string>, resolveCollabDocumentId?: (sourceId: string) => string): Array<{
84
+ row: Record<string, unknown>;
85
+ docId: string;
86
+ }>;
81
87
  export {};
@@ -16,7 +16,7 @@ import { defineEventHandler, getMethod, setResponseStatus, } from "h3";
16
16
  import { postAwareness, getActiveUsers } from "../collab/awareness.js";
17
17
  import { getCollabEmitter } from "../collab/emitter.js";
18
18
  import { getCollabState, postCollabUpdate, postCollabText, postCollabSearchReplace, } from "../collab/routes.js";
19
- import { hasCollabState } from "../collab/storage.js";
19
+ import { listCollabDocIds } from "../collab/storage.js";
20
20
  import { postCollabJson, getCollabJson, postCollabPatch, } from "../collab/struct-routes.js";
21
21
  import { seedFromText, seedFromJson } from "../collab/ydoc-manager.js";
22
22
  import { getDbExec } from "../db/client.js";
@@ -82,6 +82,7 @@ function warnForImplicitAllAuthenticatedAccess(table) {
82
82
  export function createCollabPlugin(options = {}) {
83
83
  const normalizedAccess = normalizeCollabAccess(options);
84
84
  const { table = "documents", contentColumn = "content", idColumn = "id", autoSeed = true, maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES, } = options;
85
+ const resolveCollabDocumentId = options.resolveCollabDocumentId ?? ((sourceId) => sourceId);
85
86
  const resourceType = normalizedAccess.mode === "resource"
86
87
  ? normalizedAccess.resourceType
87
88
  : undefined;
@@ -308,13 +309,14 @@ export function createCollabPlugin(options = {}) {
308
309
  // Run in background so it doesn't block startup
309
310
  setTimeout(async () => {
310
311
  try {
312
+ // Read existing ids once, then apply the plugin's document-id
313
+ // mapping before filtering. A source-table SQL predicate can use
314
+ // the wrong id for integrations such as Analytics' dash-* docs.
315
+ const existingDocIds = await listCollabDocIds();
311
316
  const client = getDbExec();
312
317
  const { rows } = await client.execute(`SELECT ${idColumn}, ${seedColumn} FROM ${table}`);
313
- for (const row of rows) {
314
- const docId = row[idColumn];
315
- const exists = await hasCollabState(docId);
316
- if (exists)
317
- continue;
318
+ for (const { row, docId } of selectUnseededCollabRows(rows, idColumn, existingDocIds, resolveCollabDocumentId)) {
319
+ let seeded = false;
318
320
  if (isJson) {
319
321
  const raw = row[seedColumn] ?? "{}";
320
322
  try {
@@ -323,6 +325,7 @@ export function createCollabPlugin(options = {}) {
323
325
  ? "array"
324
326
  : "map";
325
327
  await seedFromJson(docId, parsed, "data", inferredType);
328
+ seeded = true;
326
329
  }
327
330
  catch {
328
331
  // Invalid JSON — skip
@@ -331,7 +334,10 @@ export function createCollabPlugin(options = {}) {
331
334
  else {
332
335
  const content = row[seedColumn] ?? "";
333
336
  await seedFromText(docId, content);
337
+ seeded = true;
334
338
  }
339
+ if (seeded)
340
+ existingDocIds.add(docId);
335
341
  }
336
342
  }
337
343
  catch {
@@ -341,3 +347,18 @@ export function createCollabPlugin(options = {}) {
341
347
  }
342
348
  };
343
349
  }
350
+ export function selectUnseededCollabRows(rows, idColumn, existingDocIds, resolveCollabDocumentId = (sourceId) => sourceId) {
351
+ const seenDocIds = new Set(existingDocIds);
352
+ const unseeded = [];
353
+ for (const row of rows) {
354
+ const sourceId = String(row[idColumn] ?? "");
355
+ if (!sourceId)
356
+ continue;
357
+ const docId = resolveCollabDocumentId(sourceId);
358
+ if (seenDocIds.has(docId))
359
+ continue;
360
+ seenDocIds.add(docId);
361
+ unseeded.push({ row, docId });
362
+ }
363
+ return unseeded;
364
+ }
@@ -5,7 +5,7 @@
5
5
  * not the vendor, and a second backend that reports errors without them
6
6
  * produces issues nobody can tie to a release.
7
7
  */
8
- /** The deploy environment name, e.g. `production`, `deploy-preview`. */
8
+ /** The deploy environment name, e.g. `production`, `beta`, or `preview`. */
9
9
  export declare function resolveDeployEnvironment(): string;
10
10
  /**
11
11
  * Resolve the agent-native version baked into core's package.json so the
@@ -8,6 +8,7 @@
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { isAgentNativeDeploymentEnvironment } from "../config.js";
11
12
  function firstNonEmpty(...values) {
12
13
  for (const value of values) {
13
14
  const trimmed = value?.trim();
@@ -16,9 +17,46 @@ function firstNonEmpty(...values) {
16
17
  }
17
18
  return undefined;
18
19
  }
19
- /** The deploy environment name, e.g. `production`, `deploy-preview`. */
20
+ function normalizeFallbackEnvironment(value) {
21
+ const normalized = firstNonEmpty(value)?.toLowerCase();
22
+ if (normalized === "development" || normalized === "test")
23
+ return "local";
24
+ return isAgentNativeDeploymentEnvironment(normalized)
25
+ ? normalized
26
+ : undefined;
27
+ }
28
+ /** The deploy environment name, e.g. `production`, `beta`, or `preview`. */
20
29
  export function resolveDeployEnvironment() {
21
- return (firstNonEmpty(process.env.SENTRY_ENVIRONMENT, process.env.NETLIFY_CONTEXT, process.env.VERCEL_ENV, process.env.NODE_ENV) ?? "production");
30
+ const explicit = firstNonEmpty(process.env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT)?.toLowerCase();
31
+ if (explicit) {
32
+ if (!isAgentNativeDeploymentEnvironment(explicit)) {
33
+ throw new Error('AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT must be "local", "beta", "production", or "preview"');
34
+ }
35
+ return explicit;
36
+ }
37
+ const context = firstNonEmpty(process.env.CONTEXT, process.env.NETLIFY_CONTEXT, process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT)?.toLowerCase();
38
+ const branch = process.env.BRANCH?.trim().toLowerCase();
39
+ const vercelEnv = process.env.VERCEL_ENV?.trim().toLowerCase();
40
+ if (branch === "beta")
41
+ return "beta";
42
+ if (branch === "production" ||
43
+ (context === "production" && branch !== "beta")) {
44
+ return "production";
45
+ }
46
+ if (context === "branch-deploy" && branch === "main")
47
+ return "beta";
48
+ if (context === "deploy-preview" ||
49
+ context === "branch-deploy" ||
50
+ branch?.startsWith("deploy-preview") ||
51
+ vercelEnv === "preview") {
52
+ return "preview";
53
+ }
54
+ if (!context && !branch && !vercelEnv) {
55
+ return (normalizeFallbackEnvironment(firstNonEmpty(process.env.SENTRY_ENVIRONMENT, process.env.NODE_ENV)) ?? "production");
56
+ }
57
+ return (normalizeFallbackEnvironment(firstNonEmpty(context, vercelEnv)) ??
58
+ normalizeFallbackEnvironment(process.env.NODE_ENV) ??
59
+ "production");
22
60
  }
23
61
  /**
24
62
  * Resolve the agent-native version baked into core's package.json so the
@@ -231,6 +231,12 @@ function extensionTargetsForRow(row, shareRows) {
231
231
  else if (principalType === "org" && principalId) {
232
232
  addExtensionTarget(targets, { orgId: principalId });
233
233
  }
234
+ else if (principalType === "group" && orgId) {
235
+ // Group membership is org-scoped. Invalidating the org target is a
236
+ // conservative fallback that keeps group-share changes fresh for every
237
+ // member without making the poll layer own group-directory queries.
238
+ addExtensionTarget(targets, { orgId });
239
+ }
234
240
  }
235
241
  return Array.from(targets.values());
236
242
  }
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -28,11 +28,15 @@ export function resolvePublicSentryDsn() {
28
28
  }
29
29
  export function getSentryClientConfigScript() {
30
30
  const dsn = resolvePublicSentryDsn();
31
- if (!dsn)
32
- return null;
31
+ const deploymentEnvironment = resolveDeployEnvironment();
33
32
  const config = {
34
- sentryDsn: dsn,
35
- sentryEnvironment: resolveSentryEnvironment(),
33
+ ...(dsn
34
+ ? {
35
+ sentryDsn: dsn,
36
+ sentryEnvironment: resolveSentryEnvironment(),
37
+ }
38
+ : {}),
39
+ deploymentEnvironment,
36
40
  };
37
41
  return [
38
42
  "<script data-agent-native-sentry-config>",
@@ -62,6 +62,10 @@ export function initServerSentry() {
62
62
  // cookies, or process.env contents to Sentry without explicit consent.
63
63
  sendDefaultPii: false,
64
64
  beforeSend(event) {
65
+ event.tags = {
66
+ ...event.tags,
67
+ deployment_environment: resolveDeployEnvironment(),
68
+ };
65
69
  // Drop rules live in `error-noise-filter.ts` so every backend applies
66
70
  // the same ones — they describe which server errors are non-bugs in
67
71
  // this framework, not a Sentry preference.
@@ -5,8 +5,8 @@
5
5
  * 1. Direct ownership — `owner_email = currentUser`.
6
6
  * 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone
7
7
  * in the same org; `public` grants read to any authenticated user.
8
- * 3. Share rows — per-user or per-org grants in the `{type}_shares` table
9
- * with a role (`viewer | commenter | editor | admin`).
8
+ * 3. Share rows — per-user, per-group, or per-org grants in the
9
+ * `{type}_shares` table with a role (`viewer | commenter | editor | admin`).
10
10
  *
11
11
  * Use `applyAccessFilter()` on list/read queries to filter rows the current
12
12
  * user can see. Use `assertAccess()` at the top of write actions to reject
@@ -5,16 +5,18 @@
5
5
  * 1. Direct ownership — `owner_email = currentUser`.
6
6
  * 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone
7
7
  * in the same org; `public` grants read to any authenticated user.
8
- * 3. Share rows — per-user or per-org grants in the `{type}_shares` table
9
- * with a role (`viewer | commenter | editor | admin`).
8
+ * 3. Share rows — per-user, per-group, or per-org grants in the
9
+ * `{type}_shares` table with a role (`viewer | commenter | editor | admin`).
10
10
  *
11
11
  * Use `applyAccessFilter()` on list/read queries to filter rows the current
12
12
  * user can see. Use `assertAccess()` at the top of write actions to reject
13
13
  * callers who lack the required role.
14
14
  */
15
15
  import { and, eq, or, sql } from "drizzle-orm";
16
+ import { isPostgres } from "../db/client.js";
16
17
  import { orgMembers } from "../org/schema.js";
17
18
  import { getRequestAuthCapability, getRequestUserEmail, getRequestOrgId, } from "../server/request-context.js";
19
+ import { workspaceUserGroupsIncludeUser, workspaceUserGroupsTable, } from "../workspace-connections/groups.js";
18
20
  import { listShareableResources, requireShareableResource, } from "./registry.js";
19
21
  import { ROLE_RANK } from "./schema.js";
20
22
  /**
@@ -140,6 +142,38 @@ export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(
140
142
  and ${shareScope}
141
143
  and ${minRoleSql(minRole)})`);
142
144
  }
145
+ if (reg?.supportsGroupShares && normalizedUserEmail && orgId) {
146
+ const groupTable = sql.raw(workspaceUserGroupsTable());
147
+ const groupMemberPredicate = isPostgres()
148
+ ? sql `exists (
149
+ select 1
150
+ from jsonb_array_elements_text(
151
+ workspace_group.member_emails_json::jsonb
152
+ ) as group_member(email)
153
+ where lower(group_member.email) = ${normalizedUserEmail}
154
+ )`
155
+ : sql `exists (
156
+ select 1
157
+ from json_each(workspace_group.member_emails_json) as group_member
158
+ where lower(group_member.value) = ${normalizedUserEmail}
159
+ )`;
160
+ clauses.push(sql `exists (select 1 from ${sharesTable}
161
+ where ${sharesTable.resourceId} = ${resourceTable.id}
162
+ and ${sharesTable.principalType} = 'group'
163
+ and ${minRoleSql(minRole)}
164
+ and exists (
165
+ select 1 from ${groupTable} as workspace_group
166
+ where workspace_group.id = ${sharesTable.principalId}
167
+ and workspace_group.org_id = ${resourceTable.orgId}
168
+ and workspace_group.org_id = ${orgId}
169
+ and exists (
170
+ select 1 from ${orgMembers} as workspace_member
171
+ where workspace_member.org_id = workspace_group.org_id
172
+ and lower(workspace_member.email) = ${normalizedUserEmail}
173
+ )
174
+ and ${groupMemberPredicate}
175
+ ))`);
176
+ }
143
177
  return or(...clauses) ?? sql `1=0`;
144
178
  }
145
179
  function ownerScopeFilter(reg, resourceTable, ctx) {
@@ -314,6 +348,9 @@ async function resolveAccessImpl(resourceType, resourceId, rawCtx = currentAcces
314
348
  ownerMatchesActiveScope(reg, resource, ctx)) {
315
349
  return { role: "owner", resource };
316
350
  }
351
+ if (reg.canManageAccess && (await reg.canManageAccess(resource, ctx))) {
352
+ return { role: "admin", resource };
353
+ }
317
354
  if (resource.visibility === "public" && reg.allowPublic !== false) {
318
355
  // No share row needed; default viewer unless the resource registration
319
356
  // deliberately grants a stronger public-by-link role or explicit shares
@@ -359,12 +396,28 @@ async function highestShareRole(reg, resourceId, ctx, resource) {
359
396
  if (orgId) {
360
397
  principalClauses.push(and(eq(reg.sharesTable.principalType, "org"), eq(reg.sharesTable.principalId, orgId)));
361
398
  }
399
+ let best = null;
400
+ if (reg.supportsGroupShares && normalizedUserEmail && resource.orgId) {
401
+ const groupRows = await db
402
+ .select({
403
+ principalId: reg.sharesTable.principalId,
404
+ role: reg.sharesTable.role,
405
+ })
406
+ .from(reg.sharesTable)
407
+ .where(and(eq(reg.sharesTable.resourceId, resourceId), eq(reg.sharesTable.principalType, "group")));
408
+ for (const row of groupRows) {
409
+ if (await workspaceUserGroupsIncludeUser(resource.orgId, [row.principalId], normalizedUserEmail)) {
410
+ if (!best || ROLE_RANK[row.role] > ROLE_RANK[best]) {
411
+ best = row.role;
412
+ }
413
+ }
414
+ }
415
+ }
362
416
  const rows = await db
363
417
  .select({ role: reg.sharesTable.role })
364
418
  .from(reg.sharesTable)
365
419
  .where(and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)))
366
420
  .limit(10);
367
- let best = null;
368
421
  for (const r of rows) {
369
422
  if (!best || ROLE_RANK[r.role] > ROLE_RANK[best])
370
423
  best = r.role;
@@ -8,6 +8,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
8
8
  policy: {
9
9
  allowPublic: boolean;
10
10
  requireOrgMemberForUserShares: boolean;
11
+ supportsGroupShares?: boolean;
11
12
  };
12
13
  orgId?: undefined;
13
14
  role?: undefined;
@@ -22,6 +23,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
22
23
  policy: {
23
24
  allowPublic: boolean;
24
25
  requireOrgMemberForUserShares: boolean;
26
+ supportsGroupShares?: boolean;
25
27
  };
26
28
  }>;
27
29
  export default _default;
@@ -2,6 +2,7 @@ import { eq, inArray } from "drizzle-orm";
2
2
  import { z } from "zod";
3
3
  import { defineAction } from "../../action.js";
4
4
  import { organizations } from "../../org/schema.js";
5
+ import { listWorkspaceUserGroupsForOrg } from "../../workspace-connections/groups.js";
5
6
  import { resolveAccess } from "../access.js";
6
7
  import { requireShareableResource } from "../registry.js";
7
8
  async function loadOrgDisplayNames(db, shares) {
@@ -28,6 +29,24 @@ async function loadOrgDisplayNames(db, shares) {
28
29
  return new Map();
29
30
  }
30
31
  }
32
+ async function loadGroupDisplayNames(orgId, shares) {
33
+ const groupIds = Array.from(new Set(shares
34
+ .filter((share) => share.principalType === "group" && share.principalId)
35
+ .map((share) => share.principalId)));
36
+ if (!groupIds.length || !orgId)
37
+ return new Map();
38
+ try {
39
+ const groups = await listWorkspaceUserGroupsForOrg(orgId, groupIds);
40
+ return new Map(groups.flatMap((group) => {
41
+ const id = group.id;
42
+ const name = group.name.trim();
43
+ return id && name ? [[id, name]] : [];
44
+ }));
45
+ }
46
+ catch {
47
+ return new Map();
48
+ }
49
+ }
31
50
  export default defineAction({
32
51
  description: "List the current visibility and share grants on a shareable resource. Any read access is sufficient.",
33
52
  schema: z.object({
@@ -42,6 +61,9 @@ export default defineAction({
42
61
  // resources that haven't opted into restrictions.
43
62
  allowPublic: reg.allowPublic !== false,
44
63
  requireOrgMemberForUserShares: reg.requireOrgMemberForUserShares === true,
64
+ ...(reg.supportsGroupShares === true
65
+ ? { supportsGroupShares: true }
66
+ : {}),
45
67
  };
46
68
  // Only `ownerEmail`/`orgId`/`visibility` are read below, all of which the
47
69
  // projected load carries — so the share dialog never pulls a resource's
@@ -55,6 +77,7 @@ export default defineAction({
55
77
  .from(reg.sharesTable)
56
78
  .where(eq(reg.sharesTable.resourceId, args.resourceId));
57
79
  const orgDisplayNames = await loadOrgDisplayNames(db, shares);
80
+ const groupDisplayNames = await loadGroupDisplayNames(access.resource.orgId, shares);
58
81
  return {
59
82
  ownerEmail: access.resource.ownerEmail ?? null,
60
83
  orgId: access.resource.orgId ?? null,
@@ -67,7 +90,9 @@ export default defineAction({
67
90
  principalId: s.principalId,
68
91
  displayName: s.principalType === "org"
69
92
  ? orgDisplayNames.get(s.principalId)
70
- : undefined,
93
+ : s.principalType === "group"
94
+ ? groupDisplayNames.get(s.principalId)
95
+ : undefined,
71
96
  role: s.role,
72
97
  createdAt: s.createdAt,
73
98
  })),
@@ -3,7 +3,7 @@ export declare function resolveShareNotificationUrl(explicitUrl: string | undefi
3
3
  declare const _default: import("../../action.js").ActionDefinition<{
4
4
  resourceType: string;
5
5
  resourceId: string;
6
- principalType: "org" | "user";
6
+ principalType: "group" | "org" | "user";
7
7
  principalId: string;
8
8
  role?: "admin" | "commenter" | "editor" | "viewer";
9
9
  notify?: boolean;
@@ -8,6 +8,7 @@ import { sendEmail, isEmailConfigured } from "../../server/email.js";
8
8
  import { invalidateCollabAccessCache } from "../../server/poll.js";
9
9
  import { getRequestUserEmail } from "../../server/request-context.js";
10
10
  import { getUserProfile } from "../../user-profile/store.js";
11
+ import { assertWorkspaceUserGroupIds } from "../../workspace-connections/groups.js";
11
12
  import { assertAccess, ForbiddenError } from "../access.js";
12
13
  import { requireShareableResource } from "../registry.js";
13
14
  import { getExtensionShareChangeTargets, notifyExtensionShareChanged, } from "./extension-change.js";
@@ -122,7 +123,7 @@ async function isOrgMemberOrInvited(orgId, email) {
122
123
  return invited.rows.length > 0;
123
124
  }
124
125
  export default defineAction({
125
- description: "Grant a user or org access to a shareable resource. Owner or admin role required.",
126
+ description: "Grant a user, group, or org access to a shareable resource. Owner or admin role required.",
126
127
  // (audit H5) Sharing-grant operations are admin-tier and let a caller
127
128
  // expand who can read/write a resource. Refuse from the tools iframe
128
129
  // bridge so a malicious shared tool can't silently re-share its
@@ -134,11 +135,11 @@ export default defineAction({
134
135
  .describe("Registered resource type, e.g. 'document', 'form'."),
135
136
  resourceId: z.string().describe("Id of the resource to share."),
136
137
  principalType: z
137
- .enum(["user", "org"])
138
- .describe("'user' for an individual, 'org' for a whole organization."),
138
+ .enum(["user", "group", "org"])
139
+ .describe("'user' for an individual, 'group' for an organization group, or 'org' for the whole organization."),
139
140
  principalId: z
140
141
  .string()
141
- .describe("Email (user) or org id (org) of the principal."),
142
+ .describe("Email (user), group id (group), or org id (org) of the principal."),
142
143
  role: z
143
144
  .enum(["viewer", "commenter", "editor", "admin"])
144
145
  .default("viewer")
@@ -165,9 +166,24 @@ export default defineAction({
165
166
  if (!actor)
166
167
  throw new ForbiddenError("Not signed in");
167
168
  const principalId = normalizePrincipalId(args.principalType, args.principalId);
169
+ if (args.principalType === "group" && reg.supportsGroupShares !== true) {
170
+ throw new ForbiddenError(`${reg.displayName} does not support organization groups yet.`);
171
+ }
168
172
  if (args.principalType === "user" && !isEmailPrincipalId(principalId)) {
169
173
  throw new Error("User shares must use an email address, not an internal user id.");
170
174
  }
175
+ if (args.principalType === "group") {
176
+ const resourceOrgId = access.resource?.orgId;
177
+ if (!resourceOrgId) {
178
+ throw new ForbiddenError(`${reg.displayName} can only be shared with a group from within an organization.`);
179
+ }
180
+ try {
181
+ await assertWorkspaceUserGroupIds([principalId], resourceOrgId);
182
+ }
183
+ catch {
184
+ throw new ForbiddenError(`${reg.displayName} can only be shared with a group from its own organization.`);
185
+ }
186
+ }
171
187
  const beforeExtensionTargets = await getExtensionShareChangeTargets(args.resourceType, args.resourceId);
172
188
  if (reg.requireOrgMemberForUserShares) {
173
189
  const resourceOrgId = access.resource?.orgId;
@@ -189,6 +205,9 @@ export default defineAction({
189
205
  throw new ForbiddenError(`${reg.displayName} can only be shared with its own organization, not a different one.`);
190
206
  }
191
207
  }
208
+ else if (args.principalType === "group") {
209
+ // Group ownership was validated above against the resource org.
210
+ }
192
211
  }
193
212
  const db = reg.getDb();
194
213
  const [existing] = await db
@@ -1,7 +1,7 @@
1
1
  declare const _default: import("../../action.js").ActionDefinition<{
2
2
  resourceType: string;
3
3
  resourceId: string;
4
- principalType: "org" | "user";
4
+ principalType: "group" | "org" | "user";
5
5
  principalId: string;
6
6
  }, {
7
7
  ok: boolean;
@@ -22,7 +22,7 @@ export default defineAction({
22
22
  schema: z.object({
23
23
  resourceType: z.string(),
24
24
  resourceId: z.string(),
25
- principalType: z.enum(["user", "org"]),
25
+ principalType: z.enum(["user", "group", "org"]),
26
26
  principalId: z.string(),
27
27
  }),
28
28
  run: async (args) => {
@@ -168,6 +168,18 @@ export interface ShareableResourceRegistration {
168
168
  * Default: `false` (matches the historical behavior — any email can be granted).
169
169
  */
170
170
  requireOrgMemberForUserShares?: boolean;
171
+ /** Set true when this resource's DB is mounted with the org group tables. */
172
+ supportsGroupShares?: boolean;
173
+ /**
174
+ * Optional resource-specific manager check. Returning true grants the
175
+ * caller admin capability even when the row is private or they have no
176
+ * explicit share. Workspace apps use this for organization owners/admins;
177
+ * the owner is still recognized by the normal owner_email check.
178
+ */
179
+ canManageAccess?: (resource: any, ctx: {
180
+ userEmail?: string;
181
+ orgId?: string;
182
+ }) => boolean | Promise<boolean>;
171
183
  /**
172
184
  * Optional per-resource access-context adapter. Most resources should use the
173
185
  * request user/org unchanged. Templates with an intentional alternate local