@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +14 -4
- package/corpus/templates/analytics/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/analytics/CHANGELOG.md +6 -0
- package/corpus/templates/analytics/README.md +2 -2
- package/corpus/templates/analytics/changelog/2026-10-02-events-with-a-very-long-name-app-or-page-path-no-longer-caus.md +6 -0
- package/corpus/templates/analytics/package.json +1 -0
- package/corpus/templates/analytics/server/handlers/first-party-analytics.ts +9 -19
- package/corpus/templates/analytics/server/handlers/session-replay.ts +34 -47
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +9 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -36
- package/corpus/templates/analytics/server/lib/indexed-text.ts +53 -0
- package/corpus/templates/analytics/server/lib/request-errors.ts +73 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +6 -16
- package/corpus/templates/analytics/server/lib/session-replay.ts +12 -2
- package/corpus/templates/analytics/server/plugins/auth.ts +2 -1
- package/corpus/templates/analytics/server/plugins/otel.ts +6 -0
- package/corpus/templates/assets/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/assets/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/assets/CHANGELOG.md +7 -0
- package/corpus/templates/assets/README.md +2 -2
- package/corpus/templates/assets/package.json +1 -0
- package/corpus/templates/assets/server/plugins/auth.ts +2 -1
- package/corpus/templates/assets/server/plugins/otel.ts +6 -0
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +2 -1
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +5 -4
- package/corpus/templates/brain/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/brain/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/brain/CHANGELOG.md +6 -0
- package/corpus/templates/brain/README.md +12 -8
- package/corpus/templates/brain/changelog/2026-10-05-private-slack-channels-are-searchable-org-wide.md +6 -0
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/lib/audiences.ts +1 -98
- package/corpus/templates/brain/server/lib/connectors.ts +2 -166
- package/corpus/templates/brain/server/plugins/otel.ts +6 -0
- package/corpus/templates/calendar/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/calendar/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/calendar/CHANGELOG.md +7 -0
- package/corpus/templates/calendar/README.md +2 -2
- package/corpus/templates/calendar/package.json +1 -0
- package/corpus/templates/calendar/server/plugins/auth.ts +2 -1
- package/corpus/templates/calendar/server/plugins/otel.ts +6 -0
- package/corpus/templates/chat/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/chat/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/chat/README.md +2 -2
- package/corpus/templates/clips/.agents/skills/ai-video-tools/SKILL.md +9 -0
- package/corpus/templates/clips/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/clips/CHANGELOG.md +24 -0
- package/corpus/templates/clips/README.md +2 -2
- package/corpus/templates/clips/actions/lib/audio-chunk-list.ts +23 -0
- package/corpus/templates/clips/actions/lib/audio-only-transcription.ts +187 -292
- package/corpus/templates/clips/actions/lib/chunked-transcription.ts +151 -0
- package/corpus/templates/clips/actions/lib/recording-audio-source.ts +191 -0
- package/corpus/templates/clips/actions/request-transcript.ts +81 -135
- package/corpus/templates/clips/actions/search-recordings.ts +19 -5
- package/corpus/templates/clips/app/components/clips-command-menu.tsx +5 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +3 -1
- package/corpus/templates/clips/app/components/library/search-bar.tsx +5 -0
- package/corpus/templates/clips/app/hooks/use-library.ts +1 -0
- package/corpus/templates/clips/app/lib/local-recording-upload.ts +13 -6
- package/corpus/templates/clips/app/lib/recording-section.ts +1 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +7 -5
- package/corpus/templates/clips/app/routes/record.tsx +16 -6
- package/corpus/templates/clips/changelog/2026-10-02-long-uploads-an-hour-or-more-now-transcribe-reliably-instead.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-confirming-discard-recording-now-returns-you-to-your-library.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recording-now-removes-its-failed-upload-card-fr.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-discarding-a-recovered-recording-no-longer-warns-that-it-s-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-05-search-results-identify-recordings-in-trash-and-open-them-wi.md +6 -0
- package/corpus/templates/clips/package.json +1 -0
- package/corpus/templates/clips/server/lib/builder-cdn.ts +5 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +2 -2
- package/corpus/templates/clips/server/plugins/auth.ts +2 -1
- package/corpus/templates/clips/server/plugins/otel.ts +6 -0
- package/corpus/templates/clips/shared/transcript-failure.ts +5 -0
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +8 -2
- package/corpus/templates/content/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/content/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/content/CHANGELOG.md +41 -0
- package/corpus/templates/content/README.md +2 -2
- package/corpus/templates/content/actions/_database-navigation.ts +523 -344
- package/corpus/templates/content/actions/_database-row-mutation.ts +44 -10
- package/corpus/templates/content/actions/_database-setup-discovery.ts +9 -3
- package/corpus/templates/content/actions/_database-utils.ts +299 -271
- package/corpus/templates/content/actions/_property-utils.ts +293 -190
- package/corpus/templates/content/actions/_relation-values.ts +3 -1
- package/corpus/templates/content/actions/get-content-database.ts +1 -0
- package/corpus/templates/content/actions/query-content-database-items.ts +32 -5
- package/corpus/templates/content/actions/set-document-property.ts +17 -11
- package/corpus/templates/content/app/components/editor/DocumentBlockFields.tsx +21 -6
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +127 -43
- package/corpus/templates/content/app/components/editor/DocumentEditorSkeleton.tsx +159 -50
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +12 -28
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +6 -1
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +139 -70
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +16 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +180 -75
- package/corpus/templates/content/app/components/editor/database/DatabaseViewSkeleton.tsx +76 -0
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +60 -2
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +41 -42
- package/corpus/templates/content/app/components/editor/document-editor-layout.ts +18 -0
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +11 -3
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +15 -7
- package/corpus/templates/content/app/components/editor/suggestions/model.ts +13 -0
- package/corpus/templates/content/app/components/editor/suggestions/read-only-blocks.ts +136 -0
- package/corpus/templates/content/app/components/editor/table-view.ts +73 -0
- package/corpus/templates/content/app/components/layout/ContentStartupShell.tsx +1 -1
- package/corpus/templates/content/app/components/layout/Layout.tsx +5 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +57 -16
- package/corpus/templates/content/app/components/sidebar/MovePageDialog.tsx +4 -4
- package/corpus/templates/content/app/components/sidebar/SidebarNavigationRow.tsx +34 -10
- package/corpus/templates/content/app/global.css +16 -8
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +14 -13
- package/corpus/templates/content/app/hooks/use-content-database.ts +72 -9
- package/corpus/templates/content/app/hooks/use-content-personal-navigation.ts +7 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +27 -0
- package/corpus/templates/content/app/i18n-data.ts +3 -2
- package/corpus/templates/content/app/lib/content-landing.ts +3 -31
- package/corpus/templates/content/app/lib/files-navigation.ts +182 -10
- package/corpus/templates/content/app/lib/files-root-hint.ts +20 -32
- package/corpus/templates/content/app/lib/page-startup-hints.ts +108 -0
- package/corpus/templates/content/app/lib/private-link-redirect.ts +25 -0
- package/corpus/templates/content/app/lib/sidebar-layout-hint.ts +34 -1
- package/corpus/templates/content/app/lib/startup-timing.ts +2 -0
- package/corpus/templates/content/app/root.tsx +3 -1
- package/corpus/templates/content/app/routes/_app.home.tsx +10 -19
- package/corpus/templates/content/app/routes/p.$id.tsx +23 -5
- package/corpus/templates/content/changelog/2026-10-02-a-page-link-you-cant-open-now-says-so-instead-of-opening-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-after-a-refresh-the-files-sidebar-shows-the-open-page-s-fold.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-large-collections-open-faster.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-stay-inside-the-window-at-every-width-long-inline-code.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-pages-with-open-comments-and-collections-with-sorted-or-filt.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-suggest-edits-works-on-pages-written-by-agents-including-pag.md +6 -0
- package/corpus/templates/content/changelog/2026-10-02-while-you-suggest-edits-tables-and-other-blocks-that-suggest.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-view-context-now-survives-quick-page-changes-when.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-collection-views-keep-their-saved-rows-accurate-during-refre.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-exact-saved-view-links-keep-collection-retry-available.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-expanding-a-files-folder-now-shares-one-navigation-read-acro.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-files-navigation-retries-temporary-failures-consistently.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-shared-page-owners-keep-edit-and-manage-controls-in-files-an.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-tagging-an-ai-in-a-comment-works-again-instead-of-failing-ri.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-the-home-loading-shell-keeps-the-layout-of-your-last-opened-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-05-typing-and-a-space-or-inserting-a-toggle-or-callout-no-longe.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +4 -1
- package/corpus/templates/content/e2e/playwright.config.ts +1 -1
- package/corpus/templates/content/package.json +2 -1
- package/corpus/templates/content/scripts/seed-perf-database.mjs +600 -0
- package/corpus/templates/content/scripts/trace-startup.mjs +25 -3
- package/corpus/templates/content/server/db/index.ts +1 -1
- package/corpus/templates/content/server/lib/suggested-edits.ts +23 -26
- package/corpus/templates/content/server/plugins/auth.ts +2 -1
- package/corpus/templates/content/server/plugins/otel.ts +6 -0
- package/corpus/templates/content/shared/api.ts +17 -0
- package/corpus/templates/content/shared/suggestion-diff.ts +124 -38
- package/corpus/templates/content/shared/suggestion-formatting.ts +134 -3
- package/corpus/templates/crm/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/crm/package.json +1 -0
- package/corpus/templates/crm/server/plugins/otel.ts +6 -0
- package/corpus/templates/design/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/design/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/design/CHANGELOG.md +41 -0
- package/corpus/templates/design/README.md +2 -2
- package/corpus/templates/design/actions/list-designs.ts +11 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +110 -32
- package/corpus/templates/design/app/components/design/multi-screen/cross-screen-drop.ts +28 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +1 -0
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +79 -0
- package/corpus/templates/design/app/components/layout/Header.tsx +2 -4
- package/corpus/templates/design/app/components/ui/tabs.tsx +59 -1
- package/corpus/templates/design/app/global.css +1 -34
- package/corpus/templates/design/app/i18n/en-US.ts +4 -0
- package/corpus/templates/design/app/lib/design-filter.ts +4 -2
- package/corpus/templates/design/app/pages/Index.tsx +91 -59
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +58 -17
- package/corpus/templates/design/changelog/2026-10-01-filter-designs-by-owner.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-layers-dropped-into-visible-canvas-space-beyond-a-screen-now.md +6 -0
- package/corpus/templates/design/changelog/2026-10-03-visible-screen-overflow-no-longer-routes-drops-to-the-canvas.md +6 -0
- package/corpus/templates/design/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-designs.md +6 -0
- package/corpus/templates/design/package.json +1 -0
- package/corpus/templates/design/server/plugins/auth.ts +2 -1
- package/corpus/templates/design/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/dispatch/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/dispatch/README.md +2 -2
- package/corpus/templates/dispatch/package.json +1 -0
- package/corpus/templates/dispatch/server/plugins/otel.ts +6 -0
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +2 -1
- package/corpus/templates/factory/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/factory/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/factory/package.json +1 -0
- package/corpus/templates/factory/server/plugins/otel.ts +6 -0
- package/corpus/templates/forms/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/forms/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/forms/README.md +2 -2
- package/corpus/templates/forms/package.json +1 -0
- package/corpus/templates/forms/server/plugins/auth.ts +2 -1
- package/corpus/templates/forms/server/plugins/otel.ts +6 -0
- package/corpus/templates/mail/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/mail/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/mail/CHANGELOG.md +6 -0
- package/corpus/templates/mail/README.md +2 -2
- package/corpus/templates/mail/package.json +1 -0
- package/corpus/templates/mail/server/plugins/auth.ts +2 -1
- package/corpus/templates/mail/server/plugins/otel.ts +6 -0
- package/corpus/templates/plan/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/plan/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/plan/CHANGELOG.md +1 -0
- package/corpus/templates/plan/README.md +2 -2
- package/corpus/templates/plan/package.json +1 -0
- package/corpus/templates/plan/server/plugins/auth.ts +2 -1
- package/corpus/templates/plan/server/plugins/otel.ts +6 -0
- package/corpus/templates/slides/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/slides/.agents/skills/storing-data/SKILL.md +12 -7
- package/corpus/templates/slides/CHANGELOG.md +23 -0
- package/corpus/templates/slides/README.md +2 -2
- package/corpus/templates/slides/actions/list-decks.ts +11 -4
- package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +44 -36
- package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +79 -0
- package/corpus/templates/slides/app/components/layout/Header.tsx +2 -15
- package/corpus/templates/slides/app/components/ui/tabs.tsx +59 -0
- package/corpus/templates/slides/app/global.css +3 -54
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/lib/deck-filter.ts +5 -2
- package/corpus/templates/slides/app/pages/Index.tsx +102 -88
- package/corpus/templates/slides/changelog/2026-10-01-filter-decks-by-owner.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-02-hide-recent-until-decks-available.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-05-the-recent-tab-shows-loading-placeholders-until-your-decks-a.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +8 -0
- package/corpus/templates/slides/server/plugins/auth.ts +2 -1
- package/corpus/templates/slides/server/plugins/otel.ts +6 -0
- package/corpus/templates/tasks/.agents/skills/security/SKILL.md +9 -2
- package/corpus/templates/tasks/.agents/skills/storing-data/SKILL.md +12 -7
- 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:
|
|
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: [
|
|
@@ -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
|
|
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)
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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.
|
|
97
|
-
|
|
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:
|
|
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: [
|
|
@@ -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
|
|
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.
|
|
116
|
-
use the
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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)
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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.
|
|
97
|
-
|
|
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.
|
|
12
|
-
|
|
13
|
-
attendee-scoped meetings
|
|
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
|
-
-
|
|
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
|
|
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.
|
|
@@ -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,
|
|
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
|
),
|