@agent-native/core-corpus 0.201.0-nightly-20261005153421 → 0.201.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 (235) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/security/SKILL.md +9 -2
  3. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +14 -4
  4. package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +12 -7
  5. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  6. package/corpus/templates/analytics/README.md +2 -2
  7. package/corpus/templates/analytics/changelog/2026-10-02-events-with-a-very-long-name-app-or-page-path-no-longer-caus.md +6 -0
  8. package/corpus/templates/analytics/package.json +1 -0
  9. package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +9 -19
  10. package/corpus/templates/analytics/server/handlers/session-replay.ts +34 -47
  11. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -6
  12. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -36
  13. package/corpus/templates/analytics/server/lib/indexed-text.ts +53 -0
  14. package/corpus/templates/analytics/server/lib/request-errors.ts +73 -0
  15. package/corpus/templates/analytics/server/lib/session-event-index.ts +6 -16
  16. package/corpus/templates/analytics/server/lib/session-replay.ts +12 -2
  17. package/corpus/templates/analytics/server/plugins/auth.ts +2 -1
  18. package/corpus/templates/analytics/server/plugins/otel.ts +6 -0
  19. package/corpus/templates/assets/.agents/skills/security/SKILL.md +9 -2
  20. package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +12 -7
  21. package/corpus/templates/assets/CHANGELOG.md +7 -0
  22. package/corpus/templates/assets/README.md +2 -2
  23. package/corpus/templates/assets/package.json +1 -0
  24. package/corpus/templates/assets/server/plugins/auth.ts +2 -1
  25. package/corpus/templates/assets/server/plugins/otel.ts +6 -0
  26. package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +2 -1
  27. package/corpus/templates/brain/.agents/skills/brain/SKILL.md +5 -4
  28. package/corpus/templates/brain/.agents/skills/security/SKILL.md +9 -2
  29. package/corpus/templates/brain/.agents/skills/storing-data/SKILL.md +12 -7
  30. package/corpus/templates/brain/CHANGELOG.md +6 -0
  31. package/corpus/templates/brain/README.md +12 -8
  32. package/corpus/templates/brain/changelog/2026-10-05-private-slack-channels-are-searchable-org-wide.md +6 -0
  33. package/corpus/templates/brain/package.json +1 -0
  34. package/corpus/templates/brain/server/lib/audiences.ts +1 -98
  35. package/corpus/templates/brain/server/lib/connectors.ts +2 -166
  36. package/corpus/templates/brain/server/plugins/otel.ts +6 -0
  37. package/corpus/templates/calendar/.agents/skills/security/SKILL.md +9 -2
  38. package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +12 -7
  39. package/corpus/templates/calendar/CHANGELOG.md +7 -0
  40. package/corpus/templates/calendar/README.md +2 -2
  41. package/corpus/templates/calendar/package.json +1 -0
  42. package/corpus/templates/calendar/server/plugins/auth.ts +2 -1
  43. package/corpus/templates/calendar/server/plugins/otel.ts +6 -0
  44. package/corpus/templates/chat/.agents/skills/security/SKILL.md +9 -2
  45. package/corpus/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
  46. package/corpus/templates/chat/README.md +2 -2
  47. package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +9 -0
  48. package/corpus/templates/clips/.agents/skills/security/SKILL.md +9 -2
  49. package/corpus/templates/clips/CHANGELOG.md +24 -0
  50. package/corpus/templates/clips/README.md +2 -2
  51. package/corpus/templates/clips/actions/lib/audio-chunk-list.ts +23 -0
  52. package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +187 -292
  53. package/corpus/templates/clips/actions/lib/chunked-transcription.ts +151 -0
  54. package/corpus/templates/clips/actions/lib/recording-audio-source.ts +191 -0
  55. package/corpus/templates/clips/actions/request-transcript.ts +81 -135
  56. package/corpus/templates/clips/actions/search-recordings.ts +19 -5
  57. package/corpus/templates/clips/app/components/clips-command-menu.tsx +5 -0
  58. package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -1
  59. package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -0
  60. package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
  61. package/corpus/templates/clips/app/lib/local-recording-upload.ts +13 -6
  62. package/corpus/templates/clips/app/lib/recording-section.ts +1 -1
  63. package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +7 -5
  64. package/corpus/templates/clips/app/routes/record.tsx +16 -6
  65. package/corpus/templates/clips/changelog/2026-10-02-long-uploads-an-hour-or-more-now-transcribe-reliably-instead.md +6 -0
  66. package/corpus/templates/clips/changelog/2026-10-05-confirming-discard-recording-now-returns-you-to-your-library.md +6 -0
  67. package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recording-now-removes-its-failed-upload-card-fr.md +6 -0
  68. package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recovered-recording-no-longer-warns-that-it-s-o.md +6 -0
  69. package/corpus/templates/clips/changelog/2026-10-05-search-results-identify-recordings-in-trash-and-open-them-wi.md +6 -0
  70. package/corpus/templates/clips/package.json +1 -0
  71. package/corpus/templates/clips/server/lib/builder-cdn.ts +5 -0
  72. package/corpus/templates/clips/server/lib/builder-media-compression.ts +2 -2
  73. package/corpus/templates/clips/server/plugins/auth.ts +2 -1
  74. package/corpus/templates/clips/server/plugins/otel.ts +6 -0
  75. package/corpus/templates/clips/shared/transcript-failure.ts +5 -0
  76. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
  77. package/corpus/templates/content/.agents/skills/security/SKILL.md +9 -2
  78. package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +12 -7
  79. package/corpus/templates/content/CHANGELOG.md +41 -0
  80. package/corpus/templates/content/README.md +2 -2
  81. package/corpus/templates/content/actions/_database-navigation.ts +523 -344
  82. package/corpus/templates/content/actions/_database-row-mutation.ts +44 -10
  83. package/corpus/templates/content/actions/_database-setup-discovery.ts +9 -3
  84. package/corpus/templates/content/actions/_database-utils.ts +299 -271
  85. package/corpus/templates/content/actions/_property-utils.ts +293 -190
  86. package/corpus/templates/content/actions/_relation-values.ts +3 -1
  87. package/corpus/templates/content/actions/get-content-database.ts +1 -0
  88. package/corpus/templates/content/actions/query-content-database-items.ts +32 -5
  89. package/corpus/templates/content/actions/set-document-property.ts +17 -11
  90. package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +21 -6
  91. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -43
  92. package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +159 -50
  93. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +12 -28
  94. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +6 -1
  95. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +139 -70
  96. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +16 -1
  97. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +180 -75
  98. package/corpus/templates/content/app/components/editor/database/DatabaseViewSkeleton.tsx +76 -0
  99. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +60 -2
  100. package/corpus/templates/content/app/components/editor/database/sidebar.tsx +41 -42
  101. package/corpus/templates/content/app/components/editor/document-editor-layout.ts +18 -0
  102. package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +11 -3
  103. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +15 -7
  104. package/corpus/templates/content/app/components/editor/suggestions/model.ts +13 -0
  105. package/corpus/templates/content/app/components/editor/suggestions/read-only-blocks.ts +136 -0
  106. package/corpus/templates/content/app/components/editor/table-view.ts +73 -0
  107. package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +1 -1
  108. package/corpus/templates/content/app/components/layout/Layout.tsx +5 -1
  109. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +57 -16
  110. package/corpus/templates/content/app/components/sidebar/MovePageDialog.tsx +4 -4
  111. package/corpus/templates/content/app/components/sidebar/SidebarNavigationRow.tsx +34 -10
  112. package/corpus/templates/content/app/global.css +16 -8
  113. package/corpus/templates/content/app/hooks/content-action-refresh.ts +14 -13
  114. package/corpus/templates/content/app/hooks/use-content-database.ts +72 -9
  115. package/corpus/templates/content/app/hooks/use-content-personal-navigation.ts +7 -3
  116. package/corpus/templates/content/app/hooks/use-documents.ts +27 -0
  117. package/corpus/templates/content/app/i18n-data.ts +3 -2
  118. package/corpus/templates/content/app/lib/content-landing.ts +3 -31
  119. package/corpus/templates/content/app/lib/files-navigation.ts +182 -10
  120. package/corpus/templates/content/app/lib/files-root-hint.ts +20 -32
  121. package/corpus/templates/content/app/lib/page-startup-hints.ts +108 -0
  122. package/corpus/templates/content/app/lib/private-link-redirect.ts +25 -0
  123. package/corpus/templates/content/app/lib/sidebar-layout-hint.ts +34 -1
  124. package/corpus/templates/content/app/lib/startup-timing.ts +2 -0
  125. package/corpus/templates/content/app/root.tsx +3 -1
  126. package/corpus/templates/content/app/routes/_app.home.tsx +10 -19
  127. package/corpus/templates/content/app/routes/p.$id.tsx +23 -5
  128. package/corpus/templates/content/changelog/2026-10-02-a-page-link-you-cant-open-now-says-so-instead-of-opening-an.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-02-after-a-refresh-the-files-sidebar-shows-the-open-page-s-fold.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-02-large-collections-open-faster.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-02-pages-stay-inside-the-window-at-every-width-long-inline-code.md +6 -0
  132. package/corpus/templates/content/changelog/2026-10-02-pages-with-open-comments-and-collections-with-sorted-or-filt.md +6 -0
  133. package/corpus/templates/content/changelog/2026-10-02-suggest-edits-works-on-pages-written-by-agents-including-pag.md +6 -0
  134. package/corpus/templates/content/changelog/2026-10-02-while-you-suggest-edits-tables-and-other-blocks-that-suggest.md +6 -0
  135. package/corpus/templates/content/changelog/2026-10-05-collection-view-context-now-survives-quick-page-changes-when.md +6 -0
  136. package/corpus/templates/content/changelog/2026-10-05-collection-views-keep-their-saved-rows-accurate-during-refre.md +6 -0
  137. package/corpus/templates/content/changelog/2026-10-05-exact-saved-view-links-keep-collection-retry-available.md +6 -0
  138. package/corpus/templates/content/changelog/2026-10-05-expanding-a-files-folder-now-shares-one-navigation-read-acro.md +6 -0
  139. package/corpus/templates/content/changelog/2026-10-05-files-navigation-retries-temporary-failures-consistently.md +6 -0
  140. package/corpus/templates/content/changelog/2026-10-05-shared-page-owners-keep-edit-and-manage-controls-in-files-an.md +6 -0
  141. package/corpus/templates/content/changelog/2026-10-05-tagging-an-ai-in-a-comment-works-again-instead-of-failing-ri.md +6 -0
  142. package/corpus/templates/content/changelog/2026-10-05-the-home-loading-shell-keeps-the-layout-of-your-last-opened-.md +6 -0
  143. package/corpus/templates/content/changelog/2026-10-05-typing-and-a-space-or-inserting-a-toggle-or-callout-no-longe.md +6 -0
  144. package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +4 -1
  145. package/corpus/templates/content/e2e/playwright.config.ts +1 -1
  146. package/corpus/templates/content/package.json +2 -1
  147. package/corpus/templates/content/scripts/seed-perf-database.mjs +600 -0
  148. package/corpus/templates/content/scripts/trace-startup.mjs +25 -3
  149. package/corpus/templates/content/server/db/index.ts +1 -1
  150. package/corpus/templates/content/server/lib/suggested-edits.ts +23 -26
  151. package/corpus/templates/content/server/plugins/auth.ts +2 -1
  152. package/corpus/templates/content/server/plugins/otel.ts +6 -0
  153. package/corpus/templates/content/shared/api.ts +17 -0
  154. package/corpus/templates/content/shared/suggestion-diff.ts +124 -38
  155. package/corpus/templates/content/shared/suggestion-formatting.ts +134 -3
  156. package/corpus/templates/crm/.agents/skills/security/SKILL.md +9 -2
  157. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +12 -7
  158. package/corpus/templates/crm/package.json +1 -0
  159. package/corpus/templates/crm/server/plugins/otel.ts +6 -0
  160. package/corpus/templates/design/.agents/skills/security/SKILL.md +9 -2
  161. package/corpus/templates/design/.agents/skills/storing-data/SKILL.md +12 -7
  162. package/corpus/templates/design/CHANGELOG.md +41 -0
  163. package/corpus/templates/design/README.md +2 -2
  164. package/corpus/templates/design/actions/list-designs.ts +11 -4
  165. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +110 -32
  166. package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +28 -0
  167. package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -0
  168. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +79 -0
  169. package/corpus/templates/design/app/components/layout/Header.tsx +2 -4
  170. package/corpus/templates/design/app/components/ui/tabs.tsx +59 -1
  171. package/corpus/templates/design/app/global.css +1 -34
  172. package/corpus/templates/design/app/i18n/en-US.ts +4 -0
  173. package/corpus/templates/design/app/lib/design-filter.ts +4 -2
  174. package/corpus/templates/design/app/pages/Index.tsx +91 -59
  175. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +58 -17
  176. package/corpus/templates/design/changelog/2026-10-01-filter-designs-by-owner.md +6 -0
  177. package/corpus/templates/design/changelog/2026-10-03-layers-dropped-into-visible-canvas-space-beyond-a-screen-now.md +6 -0
  178. package/corpus/templates/design/changelog/2026-10-03-visible-screen-overflow-no-longer-routes-drops-to-the-canvas.md +6 -0
  179. package/corpus/templates/design/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-designs.md +6 -0
  180. package/corpus/templates/design/package.json +1 -0
  181. package/corpus/templates/design/server/plugins/auth.ts +2 -1
  182. package/corpus/templates/design/server/plugins/otel.ts +6 -0
  183. package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +9 -2
  184. package/corpus/templates/dispatch/.agents/skills/storing-data/SKILL.md +12 -7
  185. package/corpus/templates/dispatch/README.md +2 -2
  186. package/corpus/templates/dispatch/package.json +1 -0
  187. package/corpus/templates/dispatch/server/plugins/otel.ts +6 -0
  188. package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +2 -1
  189. package/corpus/templates/factory/.agents/skills/security/SKILL.md +9 -2
  190. package/corpus/templates/factory/.agents/skills/storing-data/SKILL.md +12 -7
  191. package/corpus/templates/factory/package.json +1 -0
  192. package/corpus/templates/factory/server/plugins/otel.ts +6 -0
  193. package/corpus/templates/forms/.agents/skills/security/SKILL.md +9 -2
  194. package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +12 -7
  195. package/corpus/templates/forms/README.md +2 -2
  196. package/corpus/templates/forms/package.json +1 -0
  197. package/corpus/templates/forms/server/plugins/auth.ts +2 -1
  198. package/corpus/templates/forms/server/plugins/otel.ts +6 -0
  199. package/corpus/templates/mail/.agents/skills/security/SKILL.md +9 -2
  200. package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +12 -7
  201. package/corpus/templates/mail/CHANGELOG.md +6 -0
  202. package/corpus/templates/mail/README.md +2 -2
  203. package/corpus/templates/mail/package.json +1 -0
  204. package/corpus/templates/mail/server/plugins/auth.ts +2 -1
  205. package/corpus/templates/mail/server/plugins/otel.ts +6 -0
  206. package/corpus/templates/plan/.agents/skills/security/SKILL.md +9 -2
  207. package/corpus/templates/plan/.agents/skills/storing-data/SKILL.md +12 -7
  208. package/corpus/templates/plan/CHANGELOG.md +1 -0
  209. package/corpus/templates/plan/README.md +2 -2
  210. package/corpus/templates/plan/package.json +1 -0
  211. package/corpus/templates/plan/server/plugins/auth.ts +2 -1
  212. package/corpus/templates/plan/server/plugins/otel.ts +6 -0
  213. package/corpus/templates/slides/.agents/skills/security/SKILL.md +9 -2
  214. package/corpus/templates/slides/.agents/skills/storing-data/SKILL.md +12 -7
  215. package/corpus/templates/slides/CHANGELOG.md +23 -0
  216. package/corpus/templates/slides/README.md +2 -2
  217. package/corpus/templates/slides/actions/list-decks.ts +11 -4
  218. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +44 -36
  219. package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +79 -0
  220. package/corpus/templates/slides/app/components/layout/Header.tsx +2 -15
  221. package/corpus/templates/slides/app/components/ui/tabs.tsx +59 -0
  222. package/corpus/templates/slides/app/global.css +3 -54
  223. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  224. package/corpus/templates/slides/app/lib/deck-filter.ts +5 -2
  225. package/corpus/templates/slides/app/pages/Index.tsx +102 -88
  226. package/corpus/templates/slides/changelog/2026-10-01-filter-decks-by-owner.md +6 -0
  227. package/corpus/templates/slides/changelog/2026-10-02-hide-recent-until-decks-available.md +6 -0
  228. package/corpus/templates/slides/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-decks-a.md +6 -0
  229. package/corpus/templates/slides/package.json +1 -0
  230. package/corpus/templates/slides/server/handlers/request-auth-context.ts +8 -0
  231. package/corpus/templates/slides/server/plugins/auth.ts +2 -1
  232. package/corpus/templates/slides/server/plugins/otel.ts +6 -0
  233. package/corpus/templates/tasks/.agents/skills/security/SKILL.md +9 -2
  234. package/corpus/templates/tasks/.agents/skills/storing-data/SKILL.md +12 -7
  235. package/package.json +1 -1
@@ -0,0 +1,53 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ /**
4
+ * Postgres rejects an index entry over about 2.7 KB, and one rejected row fails
5
+ * its whole insert, so caller text stored in an indexed column is cut first.
6
+ * Lengths count UTF-16 code units, each at most three UTF-8 bytes, and are sized
7
+ * so the widest index, (org_id, path, event_name), stays under that limit.
8
+ */
9
+ export const MAX_EVENT_NAME_LENGTH = 200;
10
+ /** App and template names. */
11
+ export const MAX_APP_LENGTH = 100;
12
+ export const MAX_PATH_LENGTH = 500;
13
+ export const MAX_USER_KEY_LENGTH = 256;
14
+ /**
15
+ * Session and recording ids are rejected or skipped past this length, never
16
+ * cut, because a cut id could merge two sessions.
17
+ */
18
+ export const MAX_SESSION_ID_LENGTH = 256;
19
+ /**
20
+ * Row ids are primary keys, and percent-encoding can triple already bounded
21
+ * text, so a longer id is replaced by a hash of its parts.
22
+ */
23
+ const MAX_ROW_ID_LENGTH = 512;
24
+
25
+ /**
26
+ * Never ends on half of a surrogate pair, which Postgres would store as a
27
+ * replacement character.
28
+ */
29
+ export function boundedText(
30
+ value: string | null | undefined,
31
+ maxLength: number,
32
+ ): string {
33
+ const text = value?.trim().slice(0, maxLength) ?? "";
34
+ return /[\uD800-\uDBFF]$/.test(text) ? text.slice(0, -1) : text;
35
+ }
36
+
37
+ /**
38
+ * For a value that identifies something, such as a user. A plain cut would
39
+ * merge every value sharing the kept prefix, so a value past the limit keeps a
40
+ * shorter prefix plus a hash of the whole value.
41
+ */
42
+ export function boundedIdentity(value: string, maxLength: number): string {
43
+ const text = value.trim();
44
+ if (text.length <= maxLength) return text;
45
+ const digest = createHash("sha256").update(text).digest("hex").slice(0, 16);
46
+ return `${boundedText(text, maxLength - digest.length - 1)}~${digest}`;
47
+ }
48
+
49
+ export function indexedRowId(prefix: string, parts: readonly string[]): string {
50
+ const id = `${prefix}_${parts.map((part) => encodeURIComponent(part)).join("|")}`;
51
+ if (id.length <= MAX_ROW_ID_LENGTH) return id;
52
+ return `${prefix}_h_${createHash("sha256").update(JSON.stringify(parts)).digest("hex")}`;
53
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * An error raised for the caller carries a numeric statusCode and a message
3
+ * written to be returned. Any other error is unexpected, and its message can
4
+ * quote internal database details, so it is logged and the caller gets a
5
+ * generic reply.
6
+ */
7
+ export function requestError(
8
+ message: string,
9
+ statusCode: number,
10
+ ): Error & { statusCode: number } {
11
+ return Object.assign(new Error(message), { statusCode });
12
+ }
13
+
14
+ export function parseJsonBody(raw: string): unknown {
15
+ try {
16
+ return JSON.parse(raw);
17
+ } catch {
18
+ throw requestError("Request body is not valid JSON", 400);
19
+ }
20
+ }
21
+
22
+ const NUL = /\u0000/g;
23
+ const LONE_SURROGATE =
24
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
25
+
26
+ /**
27
+ * Postgres rejects U+0000 in text and jsonb, and a lone surrogate in jsonb;
28
+ * stored JSON is read back with ::jsonb, and row ids encode values with
29
+ * encodeURIComponent, which throws on a lone surrogate. One such character
30
+ * would fail its write on every retry, or every query over it later, so an
31
+ * ingest body has every string and key cleaned once, as it is parsed.
32
+ */
33
+ export function parseIngestBody(raw: unknown): unknown {
34
+ return postgresSafe(
35
+ typeof raw === "string" && raw.trim() ? parseJsonBody(raw) : raw,
36
+ );
37
+ }
38
+
39
+ function postgresSafe(value: unknown): unknown {
40
+ if (typeof value === "string") return postgresSafeString(value);
41
+ if (Array.isArray(value)) return value.map(postgresSafe);
42
+ if (value && typeof value === "object") {
43
+ const entries = Object.entries(value).map(([key, entry]) => [
44
+ postgresSafeString(key),
45
+ postgresSafe(entry),
46
+ ]);
47
+ // Two keys that clean to one would silently keep only one value.
48
+ if (new Set(entries.map(([key]) => key)).size < entries.length) {
49
+ throw requestError(
50
+ "Request body has keys that differ only by characters Postgres cannot store",
51
+ 400,
52
+ );
53
+ }
54
+ return Object.fromEntries(entries);
55
+ }
56
+ return value;
57
+ }
58
+
59
+ function postgresSafeString(value: string): string {
60
+ return value.replace(NUL, "").replace(LONE_SURROGATE, "�");
61
+ }
62
+
63
+ export function errorReply(
64
+ error: unknown,
65
+ logPrefix: string,
66
+ ): { statusCode: number; error: string } {
67
+ const statusCode = (error as { statusCode?: unknown } | null)?.statusCode;
68
+ if (error instanceof Error && typeof statusCode === "number") {
69
+ return { statusCode, error: error.message };
70
+ }
71
+ console.error(`${logPrefix} Unexpected request failure:`, error);
72
+ return { statusCode: 500, error: "Internal server error" };
73
+ }
@@ -23,6 +23,12 @@ import {
23
23
  type SessionEventNameCount,
24
24
  } from "../../shared/session-events.js";
25
25
  import { getDb, schema } from "../db/index.js";
26
+ import {
27
+ MAX_APP_LENGTH,
28
+ MAX_EVENT_NAME_LENGTH,
29
+ MAX_SESSION_ID_LENGTH,
30
+ boundedText,
31
+ } from "./indexed-text.js";
26
32
 
27
33
  /**
28
34
  * Session event index.
@@ -54,9 +60,6 @@ export interface SessionEventScope {
54
60
 
55
61
  const MAX_PROPERTY_KEYS = 30;
56
62
  const PROPERTY_KEY_PATTERN = /^[A-Za-z0-9_.$:-]{1,64}$/;
57
- const MAX_EVENT_NAME_LENGTH = 200;
58
- const MAX_APP_LENGTH = 100;
59
- const MAX_SESSION_ID_LENGTH = 256;
60
63
  const STOPPED_FIRING_DAYS = 7;
61
64
  const SESSION_EVENT_INDEX_RETENTION_BUFFER_DAYS = 2;
62
65
  export const EVENT_CATALOG_RETENTION_DAYS = 180;
@@ -82,19 +85,6 @@ function viewerTenantKeys(scope: SessionEventScope): string[] {
82
85
  : [sessionEventTenantKey(scope.userEmail, null)];
83
86
  }
84
87
 
85
- /**
86
- * Unique indexes hold event names and apps raw, so both are cut to a length
87
- * that fits an index entry. Never ends on half of a surrogate pair, which
88
- * Postgres would store as a replacement character.
89
- */
90
- function boundedText(
91
- value: string | null | undefined,
92
- maxLength: number,
93
- ): string {
94
- const text = value?.trim().slice(0, maxLength) ?? "";
95
- return /[\uD800-\uDBFF]$/.test(text) ? text.slice(0, -1) : text;
96
- }
97
-
98
88
  /**
99
89
  * An id too long for a unique index entry would fail the gap marker too, and
100
90
  * with it the batch. Such a session never gets an index row, so "didn't"
@@ -49,6 +49,8 @@ import {
49
49
  resolveAnalyticsEventDimensions,
50
50
  touchPublicKeyLastUsedAt,
51
51
  } from "./first-party-analytics.js";
52
+ import { MAX_SESSION_ID_LENGTH } from "./indexed-text.js";
53
+ import { parseIngestBody } from "./request-errors.js";
52
54
  import {
53
55
  pruneSessionEventIndex,
54
56
  sessionEventFilterConditions,
@@ -997,8 +999,7 @@ function deriveReplaySignals({
997
999
  export function parseSessionReplayIngestPayload(
998
1000
  raw: unknown,
999
1001
  ): ParsedSessionReplayIngest {
1000
- const body =
1001
- typeof raw === "string" && raw.trim() ? JSON.parse(raw) : replayRecord(raw);
1002
+ const body = replayRecord(parseIngestBody(raw));
1002
1003
  const publicKey =
1003
1004
  replayString(body.publicKey) ||
1004
1005
  replayString(body.writeKey) ||
@@ -1026,6 +1027,15 @@ export function parseSessionReplayIngestPayload(
1026
1027
  replayString(body.recording_id) ||
1027
1028
  replayString(body.replayId) ||
1028
1029
  sessionId;
1030
+ if (
1031
+ sessionId.length > MAX_SESSION_ID_LENGTH ||
1032
+ clientRecordingId.length > MAX_SESSION_ID_LENGTH
1033
+ ) {
1034
+ throw replayError(
1035
+ `Replay session and recording ids must be at most ${MAX_SESSION_ID_LENGTH} characters`,
1036
+ 400,
1037
+ );
1038
+ }
1029
1039
  const metadata = replayRecord(body.metadata);
1030
1040
  assertReplayMetadataCap(metadata);
1031
1041
 
@@ -34,7 +34,8 @@ const authPlugin = createAuthPlugin({
34
34
  publicCorsPaths: ["/track", "/api/analytics/track", "/api/analytics/replay"],
35
35
  marketing: {
36
36
  appName: "Analytics",
37
- learnMoreUrl: "https://agent-native.com/apps/analytics",
37
+ learnMoreUrl:
38
+ "https://agent-native.com/apps/analytics?utm_source=app&utm_medium=product&utm_content=onboarding-learn-more",
38
39
  tagline:
39
40
  "Your AI agent queries your data sources, builds dashboards, and answers business questions alongside you.",
40
41
  features: [
@@ -0,0 +1,6 @@
1
+ import { defineNitroPlugin } from "@agent-native/core/server";
2
+ import { startAgentNativeOtel } from "@agent-native/otel";
3
+
4
+ export default defineNitroPlugin(() => {
5
+ startAgentNativeOtel();
6
+ });
@@ -290,9 +290,16 @@ export default defineEventHandler(async (event) => {
290
290
 
291
291
  ## Data Scoping
292
292
 
293
- In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views. This is enforced at the SQL level — the agent cannot bypass it.
293
+ In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views, and checks every statement before it runs so it can only reach those views.
294
294
 
295
- The `db-query` / `db-exec` tools (and the extension SQL bridge, which shares the same path) reject schema-qualified table references like `public.<table>` — a qualified name resolves to the base table and would skip the temp view. Use bare table names; scoping is applied automatically.
295
+ The `db-query` / `db-exec` / `db-patch` tools (and the extension SQL bridge, which shares the same path) read each statement the way Postgres does and refuse anything that could reach data around the temp views:
296
+
297
+ - schema- or database-qualified names like `public.<table>`, which resolve to the base table;
298
+ - names that resolve to anything other than the current user's temp views, such as materialized views or tables the scoping does not cover;
299
+ - app-defined SQL or PL/pgSQL functions and operators, whose bodies read tables without the views;
300
+ - built-ins that run SQL text, change session settings, or reach outside the database (`query_to_xml`, `set_config`, `dblink`, most `pg_*` functions).
301
+
302
+ `db-query` runs in a read-only transaction, and an `INSERT` must list its columns when the table has access-control columns. Use bare table names and plain expressions; scoping is applied automatically. When the agent needs data the views do not expose, add an app action with its own access checks.
296
303
 
297
304
  ### Per-User Scoping (`owner_email`)
298
305
 
@@ -75,10 +75,11 @@ state their PostgreSQL types directly.
75
75
 
76
76
  Member offboarding and email changes refuse to run while any column named
77
77
  `email`, `*_email`, `*scope_id`, `created_by`, `updated_by`, `invited_by`,
78
- `owner`, `principal_id`, `session_id`, or `user_id` has no policy. `owner_email`
79
- and `createSharesTable()` tables are handled for you. Declare every other one,
80
- including columns that are not member identities, from the app's database
81
- plugin graph (Clips does it in `server/db/index.ts`):
78
+ `owner`, `principal_id`, `session_id`, or `user_id` has no policy.
79
+ `createSharesTable()` tables are handled for you, and undeclared `owner_email`
80
+ columns transfer to the successor, which is right only for owned content.
81
+ Declare every other one, including columns that are not member identities, from
82
+ the app's database plugin graph (Clips does it in `server/db/index.ts`):
82
83
 
83
84
  ```ts
84
85
  import { registerIdentityColumns } from "@agent-native/core/org";
@@ -88,13 +89,17 @@ registerIdentityColumns([
88
89
  { table: "space_members", column: "email", emailChange: "rekey", offboard: "delete", orgScope: { column: "space_id", references: { table: "spaces", column: "id", orgColumn: "org_id" } }, reason: "Space membership grants access." },
89
90
  // Someone else's address: never rewritten.
90
91
  { table: "meeting_participants", column: "email", emailChange: "retain", offboard: "retain", reason: "Attendee address from the calendar provider." },
92
+ // A credential in an owner_email table: never handed to the successor.
93
+ { table: "api_keys", column: "owner_email", emailChange: "rekey", offboard: "revoke", reason: "Bearer keys act as their owner." },
91
94
  ]);
92
95
  ```
93
96
 
94
- Choose `delete` for grants, credentials, and pending tokens; `retain` for
97
+ Choose `delete` for grants, credentials, and pending tokens, or `revoke` (sets
98
+ `revoked_at`) when a reader treats a missing row as "not revoked"; `retain` for
95
99
  attribution, history, and third-party addresses; `transfer` only for owned
96
- data. A table without `org_id` needs `orgScope` or an organization-scoped
97
- removal leaves its rows alone.
100
+ data. Declare `owner_email` tables that hold credentials or grants, or
101
+ offboarding hands them to the successor. A table without `org_id` needs
102
+ `orgScope` or an organization-scoped removal leaves its rows alone.
98
103
 
99
104
  | Template | Tables |
100
105
  | ------------ | --------------------------------------------- |
@@ -3,6 +3,13 @@
3
3
  All notable user-facing changes to Assets are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-02
7
+
8
+ ### Improved
9
+
10
+ - Builder.io setup copy now clarifies account use and available credits.
11
+ - Owners and admins can save image generation keys for the whole organization, which is now the default.
12
+
6
13
  ## 2026-10-01
7
14
 
8
15
  ### Fixed
@@ -4,7 +4,7 @@ An open-source, agent-native digital asset manager (DAM) and brand-image/video
4
4
  generation workspace — a self-hostable alternative to tools like Brandfolder and
5
5
  Air, with generation built in.
6
6
 
7
- **Live app: [assets.agent-native.com](https://assets.agent-native.com)**
7
+ **Live app: [assets.agent-native.com](https://assets.agent-native.com?utm_source=github&utm_medium=referral&utm_content=assets-readme)**
8
8
 
9
9
  Organize uploaded and generated media into libraries and folders, then route
10
10
  image and video generation through the agent chat so every asset can be reviewed,
@@ -31,4 +31,4 @@ pnpm install
31
31
  pnpm dev
32
32
  ```
33
33
 
34
- Full docs: [agent-native.com/docs/template-assets](https://agent-native.com/docs/template-assets).
34
+ Full docs: [agent-native.com/docs/template-assets](https://agent-native.com/docs/template-assets?utm_source=github&utm_medium=referral&utm_content=assets-readme).
@@ -16,6 +16,7 @@
16
16
  "dependencies": {
17
17
  "@agent-native/core": "workspace:*",
18
18
  "@agent-native/creative-context": "workspace:*",
19
+ "@agent-native/otel": "workspace:*",
19
20
  "@agent-native/toolkit": "workspace:*",
20
21
  "@electric-sql/pglite": "^0.5.8",
21
22
  "@fontsource-variable/inter": "^5.2.8",
@@ -4,7 +4,8 @@ export default createAuthPlugin({
4
4
  workspaceAppPublicPaths: ["/"],
5
5
  marketing: {
6
6
  appName: "Assets",
7
- learnMoreUrl: "https://agent-native.com/apps/assets",
7
+ learnMoreUrl:
8
+ "https://agent-native.com/apps/assets?utm_source=app&utm_medium=product&utm_content=onboarding-learn-more",
8
9
  tagline:
9
10
  "Your AI agent creates, refines, and organizes on-brand assets alongside you.",
10
11
  features: [
@@ -0,0 +1,6 @@
1
+ import { defineNitroPlugin } from "@agent-native/core/server";
2
+ import { startAgentNativeOtel } from "@agent-native/otel";
3
+
4
+ export default defineNitroPlugin(() => {
5
+ startAgentNativeOtel();
6
+ });
@@ -113,7 +113,8 @@ pnpm --filter brain action create-source \
113
113
  The connector verifies each configured conversation before reading history and
114
114
  rejects DMs and MPIMs. Public discovery is opt-in and exclusion patterns are
115
115
  applied before capture. Private channels are never silently joined: an operator
116
- must manually invite the Slack app before validation and membership sync. Cursor
116
+ must manually invite the Slack app before validation. Once invited, the
117
+ channel's screened content is searchable org-wide. Cursor
117
118
  state is stored on the source so each sync can pick up where the last one
118
119
  stopped, including after Slack rate limiting.
119
120
 
@@ -112,10 +112,11 @@ general model knowledge.
112
112
  `lanes.semantic.status` is `failed`, semantic matches are missing — say so.
113
113
  Use it as the default first search; narrow with `type: "knowledge" |
114
114
  "capture" | "source"` when you already know which record type you need.
115
- - Audience filtering happens before ranking. Public and organization sources
116
- use the cheap organization audience; private channels and meetings use their
117
- restricted audience. A multi-source answer must use the intersection of the
118
- cited evidence audiences.
115
+ - Audience filtering happens before ranking. Slack (public and invited private
116
+ channels) and Zoom use the organization audience; attendee-scoped meetings
117
+ (Granola) use their restricted audience; personal sources are additionally
118
+ limited to their owner and shares. A multi-source answer must use the
119
+ intersection of the cited evidence audiences.
119
120
 
120
121
  Follow `sourcePolicy` for how much of `search-everything`'s output an answer
121
122
  may lean on: `strict` means distilled knowledge only; `balanced` and
@@ -290,9 +290,16 @@ export default defineEventHandler(async (event) => {
290
290
 
291
291
  ## Data Scoping
292
292
 
293
- In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views. This is enforced at the SQL level — the agent cannot bypass it.
293
+ In production, the framework automatically restricts all agent SQL queries to the current user's data using temporary views, and checks every statement before it runs so it can only reach those views.
294
294
 
295
- The `db-query` / `db-exec` tools (and the extension SQL bridge, which shares the same path) reject schema-qualified table references like `public.<table>` — a qualified name resolves to the base table and would skip the temp view. Use bare table names; scoping is applied automatically.
295
+ The `db-query` / `db-exec` / `db-patch` tools (and the extension SQL bridge, which shares the same path) read each statement the way Postgres does and refuse anything that could reach data around the temp views:
296
+
297
+ - schema- or database-qualified names like `public.<table>`, which resolve to the base table;
298
+ - names that resolve to anything other than the current user's temp views, such as materialized views or tables the scoping does not cover;
299
+ - app-defined SQL or PL/pgSQL functions and operators, whose bodies read tables without the views;
300
+ - built-ins that run SQL text, change session settings, or reach outside the database (`query_to_xml`, `set_config`, `dblink`, most `pg_*` functions).
301
+
302
+ `db-query` runs in a read-only transaction, and an `INSERT` must list its columns when the table has access-control columns. Use bare table names and plain expressions; scoping is applied automatically. When the agent needs data the views do not expose, add an app action with its own access checks.
296
303
 
297
304
  ### Per-User Scoping (`owner_email`)
298
305
 
@@ -75,10 +75,11 @@ state their PostgreSQL types directly.
75
75
 
76
76
  Member offboarding and email changes refuse to run while any column named
77
77
  `email`, `*_email`, `*scope_id`, `created_by`, `updated_by`, `invited_by`,
78
- `owner`, `principal_id`, `session_id`, or `user_id` has no policy. `owner_email`
79
- and `createSharesTable()` tables are handled for you. Declare every other one,
80
- including columns that are not member identities, from the app's database
81
- plugin graph (Clips does it in `server/db/index.ts`):
78
+ `owner`, `principal_id`, `session_id`, or `user_id` has no policy.
79
+ `createSharesTable()` tables are handled for you, and undeclared `owner_email`
80
+ columns transfer to the successor, which is right only for owned content.
81
+ Declare every other one, including columns that are not member identities, from
82
+ the app's database plugin graph (Clips does it in `server/db/index.ts`):
82
83
 
83
84
  ```ts
84
85
  import { registerIdentityColumns } from "@agent-native/core/org";
@@ -88,13 +89,17 @@ registerIdentityColumns([
88
89
  { table: "space_members", column: "email", emailChange: "rekey", offboard: "delete", orgScope: { column: "space_id", references: { table: "spaces", column: "id", orgColumn: "org_id" } }, reason: "Space membership grants access." },
89
90
  // Someone else's address: never rewritten.
90
91
  { table: "meeting_participants", column: "email", emailChange: "retain", offboard: "retain", reason: "Attendee address from the calendar provider." },
92
+ // A credential in an owner_email table: never handed to the successor.
93
+ { table: "api_keys", column: "owner_email", emailChange: "rekey", offboard: "revoke", reason: "Bearer keys act as their owner." },
91
94
  ]);
92
95
  ```
93
96
 
94
- Choose `delete` for grants, credentials, and pending tokens; `retain` for
97
+ Choose `delete` for grants, credentials, and pending tokens, or `revoke` (sets
98
+ `revoked_at`) when a reader treats a missing row as "not revoked"; `retain` for
95
99
  attribution, history, and third-party addresses; `transfer` only for owned
96
- data. A table without `org_id` needs `orgScope` or an organization-scoped
97
- removal leaves its rows alone.
100
+ data. Declare `owner_email` tables that hold credentials or grants, or
101
+ offboarding hands them to the successor. A table without `org_id` needs
102
+ `orgScope` or an organization-scoped removal leaves its rows alone.
98
103
 
99
104
  | Template | Tables |
100
105
  | ------------ | --------------------------------------------- |
@@ -3,6 +3,12 @@
3
3
  All notable user-facing changes to Agent-Native Brain are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-05
7
+
8
+ ### Improved
9
+
10
+ - Messages from private Slack channels the Brain app is invited to are now searchable by everyone in the org once they pass sensitivity screening. Syncs no longer look up channel members, so a member without a visible email no longer stops a channel from syncing. Personal sources stay visible only to their owner and the people they are shared with.
11
+
6
12
  ## 2026-10-02
7
13
 
8
14
  ### Improved
@@ -4,13 +4,14 @@ An open-source, agent-native alternative to Glean — clean company chat backed
4
4
  cited institutional knowledge. Ask a plain-English question and get an answer
5
5
  from approved company knowledge, with links back to the source.
6
6
 
7
- **Live app: [brain.agent-native.com](https://brain.agent-native.com)**
7
+ **Live app: [brain.agent-native.com](https://brain.agent-native.com?utm_source=github&utm_medium=referral&utm_content=brain-readme)**
8
8
 
9
9
  Brain ingests approved Slack channels, meetings, transcripts, GitHub issues/PRs,
10
10
  and webhook captures, distills them into reviewable knowledge, and answers with
11
- exact evidence quotes and source links instead of guesses. It can search both
12
- organization-wide material and explicitly invited private channels or
13
- attendee-scoped meetings without widening their audience.
11
+ exact evidence quotes and source links instead of guesses. Messages from public
12
+ channels and explicitly invited private channels are searchable org-wide once
13
+ they pass sensitivity screening; attendee-scoped meetings keep their audience,
14
+ and personal sources stay limited to their owner and shares.
14
15
 
15
16
  ## Features
16
17
 
@@ -21,8 +22,8 @@ attendee-scoped meetings without widening their audience.
21
22
  privacy classifier for richer review decisions.
22
23
  - Hybrid full-text and semantic search over allowed captures and knowledge,
23
24
  filtered by the requesting user's evidence audience before ranking.
24
- - Private-channel and meeting evidence stays audience-scoped; multi-source
25
- answers use only the intersection of their evidence audiences.
25
+ - Attendee-scoped meeting evidence and personal sources stay restricted;
26
+ multi-source answers use only the intersection of their evidence audiences.
26
27
  - Sensitive sources are suppressed or quarantined before search, citations,
27
28
  distillation, and source editors can see their content.
28
29
  - Read-only, citation-backed retrieval exposed to other apps over A2A.
@@ -33,7 +34,10 @@ attendee-scoped meetings without widening their audience.
33
34
 
34
35
  Brain never treats a connected provider as permission to index everything.
35
36
  Slack public-channel discovery is configurable, while private channels require a
36
- manual app invite and membership sync. Meetings use their attendee audience.
37
+ manual app invite. Content from an invited private channel that passes
38
+ sensitivity screening is searchable by everyone in the org. Meetings from
39
+ attendee-scoped connectors use their attendee audience, and personal sources
40
+ stay limited to their owner and shares.
37
41
 
38
42
  Hard privacy categories such as performance discussions, layoffs, compensation,
39
43
  recruiting, health accommodations, investigations, privileged legal material,
@@ -63,4 +67,4 @@ pnpm install
63
67
  pnpm dev
64
68
  ```
65
69
 
66
- Full docs: [agent-native.com/docs/template-brain](https://agent-native.com/docs/template-brain).
70
+ Full docs: [agent-native.com/docs/template-brain](https://agent-native.com/docs/template-brain?utm_source=github&utm_medium=referral&utm_content=brain-readme).
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-05
4
+ ---
5
+
6
+ Messages from private Slack channels the Brain app is invited to are now searchable by everyone in the org once they pass sensitivity screening. Syncs no longer look up channel members, so a member without a visible email no longer stops a channel from syncing. Personal sources stay visible only to their owner and the people they are shared with.
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@agent-native/core": "workspace:*",
24
+ "@agent-native/otel": "workspace:*",
24
25
  "@agent-native/toolkit": "workspace:*",
25
26
  "@electric-sql/pglite": "^0.5.8",
26
27
  "@fontsource-variable/inter": "^5.2.8",
@@ -3,25 +3,12 @@ import {
3
3
  getRequestUserEmail,
4
4
  } from "@agent-native/core/server/request-context";
5
5
  import { accessFilter } from "@agent-native/core/sharing";
6
- import { and, eq, gte, inArray, ne, or } from "drizzle-orm";
6
+ import { and, eq, inArray, or } from "drizzle-orm";
7
7
 
8
8
  import { getDb, schema } from "../db/index.js";
9
9
  import { nanoid, nowIso } from "./brain.js";
10
10
  import type { BrainAudienceAssignment } from "./search-index-contracts.js";
11
11
 
12
- export const SLACK_PRIVATE_AUDIENCE_TTL_MS = 15 * 60 * 1000;
13
-
14
- export function isAudienceMembershipFresh(
15
- kind: BrainAudienceAssignment["kind"],
16
- lastSyncedAt: string,
17
- nowMs = Date.now(),
18
- ) {
19
- return (
20
- kind !== "slack-private-channel" ||
21
- Date.parse(lastSyncedAt) >= nowMs - SLACK_PRIVATE_AUDIENCE_TTL_MS
22
- );
23
- }
24
-
25
12
  async function sha256(value: string) {
26
13
  const digest = await crypto.subtle.digest(
27
14
  "SHA-256",
@@ -465,83 +452,6 @@ export function assertSingleEvidenceTenant(
465
452
  return Array.from(tenantKeys)[0]!;
466
453
  }
467
454
 
468
- export async function refreshSlackPrivateChannelAudience(input: {
469
- source: typeof schema.brainSources.$inferSelect;
470
- channelId: string;
471
- memberEmails: string[];
472
- }) {
473
- const members =
474
- input.source.visibility === "private" && input.memberEmails.length
475
- ? [normalizeEmail(input.source.ownerEmail)]
476
- : Array.from(new Set(input.memberEmails.map(normalizeEmail))).sort();
477
- const upstreamRefHash = await sha256(input.channelId);
478
- const aclHash = await computeAudienceAclHash(
479
- "slack-private-channel",
480
- members,
481
- );
482
- const audienceId = await computeCaptureAudienceId({
483
- sourceId: input.source.id,
484
- kind: "slack-private-channel",
485
- upstreamRefHash,
486
- aclHash,
487
- });
488
- const db = getDb();
489
- const [audience] = await db
490
- .select({
491
- aclHash: schema.brainAudiences.aclHash,
492
- membershipState: schema.brainAudiences.membershipState,
493
- })
494
- .from(schema.brainAudiences)
495
- .where(
496
- and(
497
- eq(schema.brainAudiences.id, audienceId),
498
- eq(schema.brainAudiences.sourceId, input.source.id),
499
- eq(schema.brainAudiences.kind, "slack-private-channel"),
500
- eq(schema.brainAudiences.upstreamRefHash, upstreamRefHash),
501
- ),
502
- )
503
- .limit(1);
504
- if (!audience) return;
505
-
506
- if (audience.aclHash !== aclHash || audience.membershipState !== "current") {
507
- await replaceAudienceUserMembers(
508
- audienceId,
509
- members.map((email) => ({ email })),
510
- );
511
- return;
512
- }
513
- const now = nowIso();
514
- await db
515
- .update(schema.brainAudiences)
516
- .set({ lastSyncedAt: now, updatedAt: now })
517
- .where(
518
- and(
519
- eq(schema.brainAudiences.id, audienceId),
520
- eq(schema.brainAudiences.aclHash, aclHash),
521
- ),
522
- );
523
- }
524
-
525
- export async function replaceAudienceUserMembers(
526
- audienceId: string,
527
- members: Array<{ email: string; upstreamPrincipalHash?: string }>,
528
- options: { previousAclHash?: string } = {},
529
- ) {
530
- const db = getDb();
531
- const invalidations: DeferredAudienceInvalidation[] = [];
532
- const result = await db.transaction((tx) =>
533
- replaceAudienceUserMembersMutation(
534
- audienceId,
535
- members,
536
- options,
537
- tx as unknown as BrainDb,
538
- invalidations,
539
- ),
540
- );
541
- await flushAudienceInvalidations(invalidations);
542
- return result;
543
- }
544
-
545
455
  async function replaceAudienceUserMembersMutation(
546
456
  audienceId: string,
547
457
  members: Array<{ email: string; upstreamPrincipalHash?: string }>,
@@ -648,9 +558,6 @@ export async function listAccessibleAudienceIds(sourceIds?: string[]) {
648
558
  : undefined,
649
559
  );
650
560
  if (!principalFilter) return [];
651
- const slackFreshnessCutoff = new Date(
652
- Date.now() - SLACK_PRIVATE_AUDIENCE_TTL_MS,
653
- ).toISOString();
654
561
  const rows = await getDb()
655
562
  .selectDistinct({
656
563
  id: schema.brainAudiences.id,
@@ -664,10 +571,6 @@ export async function listAccessibleAudienceIds(sourceIds?: string[]) {
664
571
  .where(
665
572
  and(
666
573
  eq(schema.brainAudiences.membershipState, "current"),
667
- or(
668
- ne(schema.brainAudiences.kind, "slack-private-channel"),
669
- gte(schema.brainAudiences.lastSyncedAt, slackFreshnessCutoff),
670
- ),
671
574
  eq(schema.brainAudienceMembers.status, "active"),
672
575
  principalFilter,
673
576
  ),