@agent-native/core 0.161.23 → 0.163.0

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 (206) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
  3. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +27 -2
  4. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
  5. package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
  6. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
  7. package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
  8. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
  9. package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
  10. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
  11. package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
  12. package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  13. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  14. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
  15. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
  16. package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
  17. package/corpus/templates/clips/app/lib/camera-composite.ts +15 -1
  18. package/corpus/templates/clips/changelog/2026-08-18-clips-no-longer-crashes-when-a-camera-bubble-starts-at-a-tin.md +6 -0
  19. package/corpus/templates/clips/desktop/src/app.tsx +30 -40
  20. package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
  21. package/corpus/templates/clips/desktop/src/styles.css +110 -0
  22. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
  23. package/corpus/templates/clips/shared/recording-capture.ts +18 -13
  24. package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
  25. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +27 -2
  26. package/corpus/templates/content/actions/create-content-database.ts +1 -1
  27. package/corpus/templates/content/actions/create-document.ts +1 -1
  28. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
  29. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
  30. package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
  31. package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
  32. package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
  33. package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
  34. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +27 -2
  35. package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
  36. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +27 -2
  37. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +6 -1
  38. package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
  39. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +27 -2
  40. package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
  41. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
  42. package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
  43. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
  44. package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
  45. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
  46. package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
  47. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
  48. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
  49. package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
  50. package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
  51. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
  52. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
  53. package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
  54. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
  55. package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
  56. package/corpus/templates/slides/actions/add-slide.ts +6 -2
  57. package/corpus/templates/slides/actions/create-deck.ts +2 -1
  58. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +3 -3
  59. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +3 -3
  60. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -1
  61. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  62. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +1 -1
  63. package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
  64. package/corpus/templates/slides/app/pages/DeckEditor.tsx +35 -2
  65. package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
  66. package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
  67. package/corpus/templates/slides/server/handlers/assets.ts +23 -12
  68. package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
  69. package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
  70. package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
  71. package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
  72. package/corpus/templates/slides/server/handlers/share.ts +1 -1
  73. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  74. package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
  75. package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
  76. package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
  77. package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
  78. package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
  79. package/corpus/templates/slides/shared/api.ts +7 -1
  80. package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
  81. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
  82. package/dist/agent/engine/translate-anthropic.js +27 -2
  83. package/dist/agent/production-agent.d.ts +2 -2
  84. package/dist/agent/production-agent.js +16 -8
  85. package/dist/agent/thread-data-builder.d.ts +1 -0
  86. package/dist/agent/thread-data-builder.js +6 -4
  87. package/dist/agent/tool-approval-store.d.ts +10 -1
  88. package/dist/agent/tool-approval-store.js +19 -2
  89. package/dist/agent/types.d.ts +9 -0
  90. package/dist/app-config/schema.d.ts +1 -0
  91. package/dist/app-config/workspace.d.ts +1 -0
  92. package/dist/app-config/workspace.js +8 -0
  93. package/dist/chat-threads/schema.d.ts +2 -2
  94. package/dist/client/AssistantChat.js +14 -6
  95. package/dist/client/EnvironmentBadge.d.ts +15 -0
  96. package/dist/client/EnvironmentBadge.js +79 -0
  97. package/dist/client/MultiTabAssistantChat.js +73 -17
  98. package/dist/client/agent-chat-adapter.js +11 -1
  99. package/dist/client/api-path.js +14 -1
  100. package/dist/client/app-providers.js +2 -1
  101. package/dist/client/chat/tool-call-display.d.ts +12 -2
  102. package/dist/client/chat/tool-call-display.js +17 -7
  103. package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
  104. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
  105. package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
  106. package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
  107. package/dist/client/onboarding/use-onboarding.d.ts +8 -1
  108. package/dist/client/onboarding/use-onboarding.js +22 -7
  109. package/dist/client/org/TeamPage.js +15 -2
  110. package/dist/client/org/hooks.d.ts +2 -0
  111. package/dist/client/org/hooks.js +12 -0
  112. package/dist/client/org/index.d.ts +2 -2
  113. package/dist/client/org/index.js +1 -1
  114. package/dist/client/session-replay.js +58 -15
  115. package/dist/client/settings/SettingsTabsPage.js +19 -6
  116. package/dist/client/sharing/ShareButton.js +43 -18
  117. package/dist/client/sharing/useShareButtonController.d.ts +12 -2
  118. package/dist/client/sharing/useShareButtonController.js +38 -8
  119. package/dist/client/sse-event-processor.d.ts +7 -0
  120. package/dist/client/sse-event-processor.js +4 -1
  121. package/dist/client/ui/index.d.ts +1 -0
  122. package/dist/client/ui/index.js +1 -0
  123. package/dist/client/use-action.js +26 -0
  124. package/dist/collab/struct-routes.d.ts +1 -1
  125. package/dist/config.d.ts +15 -0
  126. package/dist/config.js +53 -0
  127. package/dist/dashboard-storage/schema.d.ts +2 -2
  128. package/dist/data-programs/schema.d.ts +2 -2
  129. package/dist/extensions/fetch-tool.js +8 -2
  130. package/dist/extensions/schema.d.ts +2 -2
  131. package/dist/extensions/store.js +3 -0
  132. package/dist/localization/core-messages/ar-SA.js +1 -0
  133. package/dist/localization/core-messages/de-DE.js +1 -0
  134. package/dist/localization/core-messages/en-US.d.ts +1 -0
  135. package/dist/localization/core-messages/en-US.js +1 -0
  136. package/dist/localization/core-messages/es-ES.js +1 -0
  137. package/dist/localization/core-messages/fr-FR.js +1 -0
  138. package/dist/localization/core-messages/hi-IN.js +1 -0
  139. package/dist/localization/core-messages/ja-JP.js +1 -0
  140. package/dist/localization/core-messages/ko-KR.js +1 -0
  141. package/dist/localization/core-messages/pt-BR.js +1 -0
  142. package/dist/localization/core-messages/zh-CN.js +1 -0
  143. package/dist/localization/core-messages/zh-TW.js +1 -0
  144. package/dist/localization/core-messages.d.ts +1 -0
  145. package/dist/localization/default-messages.d.ts +4 -0
  146. package/dist/localization/default-messages.js +4 -0
  147. package/dist/mcp/embed-app.js +1 -1
  148. package/dist/notifications/routes.d.ts +1 -1
  149. package/dist/org/handlers.d.ts +6 -1
  150. package/dist/org/handlers.js +43 -0
  151. package/dist/org/index.d.ts +3 -3
  152. package/dist/org/index.js +2 -2
  153. package/dist/org/migrations.js +33 -0
  154. package/dist/org/plugin.js +10 -1
  155. package/dist/org/schema.d.ts +315 -0
  156. package/dist/org/schema.js +20 -1
  157. package/dist/org/types.d.ts +3 -0
  158. package/dist/org/workspace-app-access.d.ts +10 -0
  159. package/dist/org/workspace-app-access.js +179 -0
  160. package/dist/progress/routes.d.ts +1 -1
  161. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  162. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  163. package/dist/provider-api/index.d.ts +10 -0
  164. package/dist/provider-api/index.js +45 -9
  165. package/dist/resources/handlers.d.ts +1 -1
  166. package/dist/secrets/routes.d.ts +9 -9
  167. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  168. package/dist/server/auth.js +24 -4
  169. package/dist/server/poll.js +13 -3
  170. package/dist/server/sentry.d.ts +1 -1
  171. package/dist/sharing/access.d.ts +2 -2
  172. package/dist/sharing/access.js +56 -3
  173. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  174. package/dist/sharing/actions/list-resource-shares.js +26 -1
  175. package/dist/sharing/actions/share-resource.d.ts +1 -1
  176. package/dist/sharing/actions/share-resource.js +23 -4
  177. package/dist/sharing/actions/unshare-resource.d.ts +1 -1
  178. package/dist/sharing/actions/unshare-resource.js +1 -1
  179. package/dist/sharing/registry.d.ts +12 -0
  180. package/dist/sharing/schema.d.ts +4 -4
  181. package/dist/sharing/schema.js +2 -2
  182. package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  183. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  184. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  185. package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  186. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  187. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
  188. package/dist/vite/client.d.ts +5 -0
  189. package/dist/vite/client.js +64 -2
  190. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +9 -1
  191. package/dist/workspace-connections/actions/list-workspace-user-groups.js +9 -4
  192. package/dist/workspace-connections/groups.d.ts +2 -0
  193. package/dist/workspace-connections/groups.js +16 -5
  194. package/dist/workspace-files/actions.d.ts +1 -8
  195. package/dist/workspace-files/actions.js +5 -21
  196. package/dist/workspace-files/index.d.ts +1 -1
  197. package/dist/workspace-files/index.js +1 -1
  198. package/dist/workspace-files/store.d.ts +18 -0
  199. package/dist/workspace-files/store.js +24 -3
  200. package/package.json +3 -3
  201. package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
  202. package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  203. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
  204. package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  205. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
  206. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
@@ -991,6 +991,11 @@ const PROVIDER_CONFIGS = {
991
991
  id: "slack",
992
992
  label: "Slack Web API",
993
993
  defaultBaseUrl: "https://slack.com/api",
994
+ // Slack answers every call with HTTP 200, success or failure — see
995
+ // bodyOkField's doc comment. Without this, a rejected chat.postMessage
996
+ // (not_in_channel, channel_not_found, msg_too_long, …) looks identical to
997
+ // a delivered one at the transport level.
998
+ bodyOkField: "ok",
994
999
  auth: {
995
1000
  type: "bearer",
996
1001
  keys: ["SLACK_BOT_TOKEN"],
@@ -1301,7 +1306,7 @@ export async function executeProviderApiRequest(args, runtime) {
1301
1306
  body: pageBody,
1302
1307
  headers,
1303
1308
  });
1304
- const resp = await fetchWithTimeout(pageUrl.href, {
1309
+ const resp = applyBodyEnvelopeOutcome(await fetchWithTimeout(pageUrl.href, {
1305
1310
  method,
1306
1311
  headers,
1307
1312
  body: pageBody,
@@ -1315,7 +1320,7 @@ export async function executeProviderApiRequest(args, runtime) {
1315
1320
  target: describeProviderRequestTarget(pageUrl.href, auth.secretValues),
1316
1321
  requestKey,
1317
1322
  },
1318
- });
1323
+ }), config);
1319
1324
  return {
1320
1325
  text: resp.text ??
1321
1326
  (resp.json !== undefined ? JSON.stringify(resp.json) : ""),
@@ -1347,7 +1352,7 @@ export async function executeProviderApiRequest(args, runtime) {
1347
1352
  body,
1348
1353
  headers,
1349
1354
  });
1350
- const response = await fetchWithTimeout(url.href, {
1355
+ const response = applyBodyEnvelopeOutcome(await fetchWithTimeout(url.href, {
1351
1356
  method,
1352
1357
  headers,
1353
1358
  body,
@@ -1361,12 +1366,12 @@ export async function executeProviderApiRequest(args, runtime) {
1361
1366
  target: describeProviderRequestTarget(url.href, auth.secretValues),
1362
1367
  requestKey,
1363
1368
  },
1364
- });
1369
+ }), config);
1365
1370
  // saveToFile: write full body to workspace file and return compact summary.
1366
1371
  if (args.saveToFile) {
1367
1372
  const rawText = response.text ??
1368
1373
  (response.json !== undefined ? JSON.stringify(response.json) : "");
1369
- const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status));
1374
+ const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status, response.ok));
1370
1375
  return {
1371
1376
  provider: { id: config.id, label: config.label },
1372
1377
  request: {
@@ -2119,7 +2124,7 @@ async function executeCustomProviderApiRequest(args, customConfig, runtime) {
2119
2124
  if (args.saveToFile) {
2120
2125
  const rawText = response.text ??
2121
2126
  (response.json !== undefined ? JSON.stringify(response.json) : "");
2122
- const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status));
2127
+ const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status, response.ok));
2123
2128
  return {
2124
2129
  provider: {
2125
2130
  id: customConfig.id,
@@ -3817,6 +3822,31 @@ function providerQuotaExhaustedResponse(detail) {
3817
3822
  },
3818
3823
  };
3819
3824
  }
3825
+ /**
3826
+ * Flip transport-level `ok` to `false` when the provider's own success field
3827
+ * (config.bodyOkField) says the call failed. See ProviderApiConfig's
3828
+ * bodyOkField doc comment: Slack answers every request with HTTP 200, so
3829
+ * without this a rejected send is indistinguishable from a delivered one to
3830
+ * any caller — including the agent — that trusts `response.ok`.
3831
+ */
3832
+ function applyBodyEnvelopeOutcome(response, config) {
3833
+ const field = config.bodyOkField;
3834
+ if (!field || !response.ok)
3835
+ return response;
3836
+ const body = response.json;
3837
+ if (!body || typeof body !== "object" || Array.isArray(body))
3838
+ return response;
3839
+ if (body[field] !== false)
3840
+ return response;
3841
+ const error = stringOrNull(body.error);
3842
+ return {
3843
+ ...response,
3844
+ ok: false,
3845
+ statusText: error
3846
+ ? `${response.statusText} (${config.label} reported ${field}: false — ${error})`
3847
+ : `${response.statusText} (${config.label} reported ${field}: false)`,
3848
+ };
3849
+ }
3820
3850
  async function fetchProviderResponse(url, fetchOptions, options) {
3821
3851
  const startedAt = Date.now();
3822
3852
  const res = await fetch(url, fetchOptions);
@@ -4000,8 +4030,8 @@ function dotGet(obj, path) {
4000
4030
  * Handle saveToFile: write the full provider-api response body to a workspace
4001
4031
  * file and return a compact summary.
4002
4032
  */
4003
- async function handleSaveToFile(filePath, responseText, contentType, status) {
4004
- const { writeWorkspaceFile, SAVE_TO_FILE_MAX_BYTES: maxSaveBytes } = await import("../workspace-files/store.js");
4033
+ async function handleSaveToFile(filePath, responseText, contentType, status, ok = true) {
4034
+ const { writeWorkspaceFile, SAVE_TO_FILE_MAX_BYTES: maxSaveBytes, isScratchWorkspacePath, toWorkspaceFileCard, } = await import("../workspace-files/store.js");
4005
4035
  const { getRequestOrgId, getRequestUserEmail } = await import("../server/request-context.js");
4006
4036
  const orgId = getRequestOrgId();
4007
4037
  const email = getRequestUserEmail();
@@ -4014,7 +4044,7 @@ async function handleSaveToFile(filePath, responseText, contentType, status) {
4014
4044
  throw new Error("saveToFile requires an authenticated request context (no user email or orgId found).");
4015
4045
  }
4016
4046
  const mimeType = contentType?.split(";")[0].trim() ?? "text/plain";
4017
- await writeWorkspaceFile(scope, filePath, responseText, mimeType, {
4047
+ const meta = await writeWorkspaceFile(scope, filePath, responseText, mimeType, {
4018
4048
  maxFileBytes: maxSaveBytes,
4019
4049
  });
4020
4050
  const bytes = Buffer.byteLength(responseText, "utf8");
@@ -4023,9 +4053,15 @@ async function handleSaveToFile(filePath, responseText, contentType, status) {
4023
4053
  savedToFile: true,
4024
4054
  savedTo: filePath,
4025
4055
  status,
4056
+ ok,
4026
4057
  bytes,
4027
4058
  contentType: mimeType,
4028
4059
  preview: preview.length < responseText.length ? `${preview}…` : preview,
4060
+ // A durable (non-scratch) file renders a download card the moment it's
4061
+ // created — no separate show-workspace-file call needed to get a link.
4062
+ ...(isScratchWorkspacePath(filePath)
4063
+ ? {}
4064
+ : { file: toWorkspaceFileCard(meta) }),
4029
4065
  };
4030
4066
  }
4031
4067
  /**
@@ -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;
@@ -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";
@@ -636,9 +637,16 @@ async function ensureEmailVerifiedForRedirect(request, response) {
636
637
  args: [email],
637
638
  });
638
639
  }
639
- catch {
640
- // Better Auth already handled the verification route. This repair is
641
- // best-effort so response cookies/redirects are never lost to DB noise.
640
+ catch (error) {
641
+ // Better Auth already handled the verification route, so this repair
642
+ // staying best-effort (never blocking the response) is correct. But a
643
+ // failure here must never be silent: "repair didn't run" and "there was
644
+ // nothing to repair" are the same observable outcome to a caller unless
645
+ // this is logged, which is exactly the shape of "clicked the verify
646
+ // link, login still says not verified" (Slack C0ATH3CCZT4, reported
647
+ // 2026-07-31 and 2026-08-05) — this UPDATE is the only place that would
648
+ // show whether Better Auth's own write is failing too.
649
+ captureAuthError(error, { route: "verify-email", email });
642
650
  }
643
651
  }
644
652
  async function emailFromVerificationResponseSession(response) {
@@ -2163,8 +2171,20 @@ function createAuthGuardFn(app) {
2163
2171
  return;
2164
2172
  }
2165
2173
  const session = await getSession(event);
2166
- 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
+ }
2167
2186
  return;
2187
+ }
2168
2188
  if (p.startsWith("/api/") || p.startsWith("/_agent-native/")) {
2169
2189
  setResponseStatus(event, 401);
2170
2190
  return { error: "Unauthorized" };
@@ -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
  }
@@ -1506,11 +1512,15 @@ export class AppSyncState {
1506
1512
  // marker rows back into extension-source events for targeted client
1507
1513
  // invalidation while preserving user/org scope.
1508
1514
  if (extensionMarkerTs > this.lastExtensionMarkerTs) {
1515
+ // Bound the scan by the watermark instead of reading every marker row
1516
+ // ever written and filtering in memory: this table keeps one row per
1517
+ // session that has ever mutated an extension, so the unbounded read
1518
+ // grows with lifetime users rather than with pending work.
1509
1519
  const extensionMarkerResult = await db.execute({
1510
- sql: "SELECT session_id, value, updated_at FROM application_state WHERE key = ? ORDER BY updated_at ASC",
1511
- args: [EXTENSION_CHANGE_MARKER_KEY],
1520
+ sql: "SELECT session_id, value, updated_at FROM application_state WHERE key = ? AND updated_at > ? ORDER BY updated_at ASC",
1521
+ args: [EXTENSION_CHANGE_MARKER_KEY, this.lastExtensionMarkerTs],
1512
1522
  });
1513
- const changedExtensionMarkers = extensionMarkerResult.rows.filter((row) => timestampValue(row.updated_at) > this.lastExtensionMarkerTs);
1523
+ const changedExtensionMarkers = extensionMarkerResult.rows;
1514
1524
  if (this.lastExtensionMarkerTs > 0) {
1515
1525
  this.recordExtensionChanges(changedExtensionMarkers
1516
1526
  .map((row) => parseExtensionChangeMarker(row.session_id, row.value))
@@ -57,7 +57,7 @@ export declare function setSentryRequestContext(ctx: {
57
57
  * this just records the error for observability.
58
58
  */
59
59
  export declare function captureAuthError(error: unknown, context: {
60
- route: "login" | "signup" | "logout" | "magic-link";
60
+ route: "login" | "signup" | "logout" | "magic-link" | "verify-email";
61
61
  email?: string;
62
62
  }): string | undefined;
63
63
  export interface RouteErrorContext {
@@ -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
@@ -42,7 +42,7 @@ export declare function ownableColumns(): {
42
42
  * Columns:
43
43
  * - `id` — share row id (generated by the share action)
44
44
  * - `resourceId` — id of the parent resource
45
- * - `principalType` — `'user' | 'org'`
45
+ * - `principalType` — `'user' | 'group' | 'org'`
46
46
  * - `principalId` — email (for user) or org id (for org)
47
47
  * - `role` — `'viewer' | 'commenter' | 'editor' | 'admin'`
48
48
  * - `createdBy` — email of the user who created the share
@@ -100,14 +100,14 @@ export declare function createSharesTable(tableName: string): import("drizzle-or
100
100
  tableName: string;
101
101
  dataType: "string";
102
102
  columnType: "SQLiteText";
103
- data: "org" | "user";
103
+ data: "group" | "org" | "user";
104
104
  driverParam: string;
105
105
  notNull: true;
106
106
  hasDefault: false;
107
107
  isPrimaryKey: false;
108
108
  isAutoincrement: false;
109
109
  hasRuntimeDefault: false;
110
- enumValues: ["user", "org"];
110
+ enumValues: ["user", "group", "org"];
111
111
  baseColumn: never;
112
112
  identity: undefined;
113
113
  generated: undefined;
@@ -195,7 +195,7 @@ export declare function createSharesTable(tableName: string): import("drizzle-or
195
195
  }>;
196
196
  export type Visibility = "private" | "org" | "public";
197
197
  export type ShareRole = "viewer" | "commenter" | "editor" | "admin";
198
- export type PrincipalType = "user" | "org";
198
+ export type PrincipalType = "user" | "group" | "org";
199
199
  /** Role precedence — higher number = stronger capability. */
200
200
  export declare const ROLE_RANK: Record<ShareRole | "owner", number>;
201
201
  export declare function roleSatisfies(actual: ShareRole | "owner", minimum: ShareRole | "owner"): boolean;