@agent-native/core-corpus 0.212.1 → 0.214.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/onboarding-journeys/SKILL.md +29 -14
- package/corpus/templates/analytics/CHANGELOG.md +11 -0
- package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
- package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
- package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
- package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
- package/corpus/templates/analytics/package.json +6 -0
- package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
- package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
- package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
- package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
- package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
- package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
- package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
- package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
- package/corpus/templates/clips/CHANGELOG.md +1 -0
- package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +7 -4
- package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
- package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
- package/corpus/templates/clips/server/lib/recordings.ts +4 -19
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +15 -1
- package/corpus/templates/content/CHANGELOG.md +13 -0
- package/corpus/templates/content/actions/_content-space-access.ts +95 -6
- package/corpus/templates/content/actions/_database-utils.ts +20 -16
- package/corpus/templates/content/actions/_database-view-setup.ts +305 -11
- package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
- package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
- package/corpus/templates/content/actions/_github-source.ts +141 -77
- package/corpus/templates/content/actions/_property-utils.ts +3 -0
- package/corpus/templates/content/actions/check-github-connection.ts +15 -0
- package/corpus/templates/content/actions/create-github-collection.ts +180 -0
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
- package/corpus/templates/content/actions/link-github-folder.ts +104 -36
- package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
- package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
- package/corpus/templates/content/actions/update-content-database-view.ts +18 -2
- package/corpus/templates/content/actions/view-screen.ts +12 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +263 -35
- package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
- package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
- package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
- package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
- package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
- package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
- package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
- package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
- package/corpus/templates/content/app/global.css +13 -0
- package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
- package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
- package/corpus/templates/content/app/i18n-data.ts +186 -1
- package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
- package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
- package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
- package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
- package/corpus/templates/content/docs/product/roadmap.md +1 -1
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +14 -0
- package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
- package/corpus/templates/content/shared/api.ts +21 -0
- package/corpus/templates/content/shared/database-item-layout.ts +591 -0
- package/corpus/templates/content/shared/github-source.ts +68 -0
- package/corpus/templates/design/CHANGELOG.md +1 -0
- package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
- package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
- package/corpus/templates/design/shared/journey-canvas.ts +6 -4
- package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
- package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
- package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
- package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
- package/package.json +1 -1
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readOrgMemberRole } from "@agent-native/core/server";
|
|
2
2
|
import {
|
|
3
|
+
getRequestContext,
|
|
3
4
|
getRequestUserEmail,
|
|
4
5
|
getRequestOrgId,
|
|
5
|
-
} from "@agent-native/core/server
|
|
6
|
-
import { and, eq, sql } from "drizzle-orm";
|
|
7
|
-
|
|
8
|
-
import { getDb } from "../db/index.js";
|
|
6
|
+
} from "@agent-native/core/server";
|
|
9
7
|
|
|
10
8
|
export class ForbiddenAuditError extends Error {
|
|
11
9
|
readonly statusCode = 403;
|
|
@@ -36,22 +34,28 @@ export async function assertOrgAdmin(): Promise<AdminScope> {
|
|
|
36
34
|
|
|
37
35
|
const orgId = getRequestOrgId();
|
|
38
36
|
if (!orgId) {
|
|
37
|
+
if (getRequestContext()?.a2aIdentityAssurance === "org-member") {
|
|
38
|
+
throw new ForbiddenAuditError(
|
|
39
|
+
"Could not verify the delegated organization scope; refusing audit access.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
39
42
|
return { ownerEmail: email };
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
const request = getRequestContext();
|
|
46
|
+
if (
|
|
47
|
+
request?.a2aIdentityAssurance === "org-member" &&
|
|
48
|
+
(request.orgId?.trim() !== orgId.trim() ||
|
|
49
|
+
request.userEmail?.trim().toLowerCase() !== email.trim().toLowerCase())
|
|
50
|
+
) {
|
|
51
|
+
throw new ForbiddenAuditError(
|
|
52
|
+
"Could not verify the delegated organization scope; refusing audit access.",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
let role: string | null = null;
|
|
43
57
|
try {
|
|
44
|
-
|
|
45
|
-
.select({ role: orgMembers.role })
|
|
46
|
-
.from(orgMembers)
|
|
47
|
-
.where(
|
|
48
|
-
and(
|
|
49
|
-
eq(orgMembers.orgId, orgId),
|
|
50
|
-
sql`lower(${orgMembers.email}) = ${email.toLowerCase()}`,
|
|
51
|
-
),
|
|
52
|
-
)
|
|
53
|
-
.limit(1);
|
|
54
|
-
role = row?.role?.toLowerCase() ?? null;
|
|
58
|
+
role = await readOrgMemberRole(orgId, email);
|
|
55
59
|
} catch {
|
|
56
60
|
throw new ForbiddenAuditError(
|
|
57
61
|
"Could not verify org admin role; refusing audit access.",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fail } from "@agent-native/core/action";
|
|
2
|
-
import {
|
|
2
|
+
import { readOrgMemberRole } from "@agent-native/core/server";
|
|
3
3
|
import {
|
|
4
|
+
getRequestContext,
|
|
4
5
|
getRequestOrgId,
|
|
5
6
|
getRequestUserEmail,
|
|
6
7
|
} from "@agent-native/core/server/request-context";
|
|
@@ -21,11 +22,15 @@ export async function isBrainAdmin(
|
|
|
21
22
|
const normalizedEmail = email?.trim();
|
|
22
23
|
const normalizedOrgId = orgId?.trim();
|
|
23
24
|
if (!normalizedEmail || !normalizedOrgId) return false;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const request = getRequestContext();
|
|
26
|
+
if (
|
|
27
|
+
request?.a2aIdentityAssurance === "org-member" &&
|
|
28
|
+
(request.orgId?.trim() !== normalizedOrgId ||
|
|
29
|
+
request.userEmail?.trim().toLowerCase() !== normalizedEmail.toLowerCase())
|
|
30
|
+
) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const role = await readOrgMemberRole(normalizedOrgId, normalizedEmail);
|
|
29
34
|
return role === "owner" || role === "admin";
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -59,6 +59,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
59
59
|
|
|
60
60
|
### Improved
|
|
61
61
|
|
|
62
|
+
- Rewind settings now show whether Rewind is remembering screen only or screen + audio.
|
|
62
63
|
- Clips detects browser meetings more reliably and keeps notes until the meeting stop and transcript save are confirmed.
|
|
63
64
|
- Clips detects browser meetings sooner, asks before starting when the active tab is unclear, and retries failed stops.
|
|
64
65
|
- Desktop dictation confirms when text is pasted or copied and keeps transcripts available when saving fails.
|
|
@@ -96,6 +96,7 @@ import {
|
|
|
96
96
|
RecordingRecoveryPage,
|
|
97
97
|
type RecordingRecoveryProps,
|
|
98
98
|
} from "./components/RecordingRecovery";
|
|
99
|
+
import { RewindCaptureStatus } from "./components/RewindCaptureStatus";
|
|
99
100
|
import { ShortcutKeycaps } from "./components/ShortcutKeycaps";
|
|
100
101
|
import { SourceRow, type CaptureSource } from "./components/SourceRow";
|
|
101
102
|
import { Switch } from "./components/Switch";
|
|
@@ -8151,10 +8152,12 @@ function Setup({
|
|
|
8151
8152
|
>
|
|
8152
8153
|
{screenMemoryMessage.text}
|
|
8153
8154
|
</p>
|
|
8154
|
-
) :
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8155
|
+
) : null}
|
|
8156
|
+
{rewindOn ? (
|
|
8157
|
+
<RewindCaptureStatus
|
|
8158
|
+
status={screenMemoryStatus}
|
|
8159
|
+
presentation={rewindStatusPresentation}
|
|
8160
|
+
/>
|
|
8158
8161
|
) : null}
|
|
8159
8162
|
</SettingsRow>
|
|
8160
8163
|
{rewindOn ? (
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RewindStatusPresentation } from "../lib/rewind-status";
|
|
2
|
+
import type { ScreenMemoryStatus } from "../shared/config";
|
|
3
|
+
|
|
4
|
+
export function RewindCaptureStatus({
|
|
5
|
+
status,
|
|
6
|
+
presentation,
|
|
7
|
+
}: {
|
|
8
|
+
status: ScreenMemoryStatus | null;
|
|
9
|
+
presentation: RewindStatusPresentation;
|
|
10
|
+
}) {
|
|
11
|
+
if (!status || presentation.kind === "off") return null;
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<p className="flex items-center gap-2" role="status">
|
|
15
|
+
{presentation.isLive ? (
|
|
16
|
+
<span className="rec-dot" aria-hidden="true" />
|
|
17
|
+
) : null}
|
|
18
|
+
{presentation.title}
|
|
19
|
+
</p>
|
|
20
|
+
{presentation.hasError ? (
|
|
21
|
+
<p className="text-xs text-destructive">{presentation.detail}</p>
|
|
22
|
+
) : null}
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -70,9 +70,14 @@ export function getRewindStatusPresentation({
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (status?.state === "recording") {
|
|
73
|
+
// The active segment, not the setting: a clip or meeting notes can add
|
|
74
|
+
// audio while Remember is set to Screen only.
|
|
75
|
+
const capturesAudio = status.activeSegment?.captureMode === "visuals-audio";
|
|
73
76
|
return {
|
|
74
77
|
kind: "recording",
|
|
75
|
-
title:
|
|
78
|
+
title: capturesAudio
|
|
79
|
+
? "Rewind is remembering screen + audio"
|
|
80
|
+
: "Rewind is remembering screen only",
|
|
76
81
|
detail:
|
|
77
82
|
status.lastError ||
|
|
78
83
|
status.coverage ||
|
|
@@ -70,6 +70,9 @@ export interface ScreenMemoryActiveSegment {
|
|
|
70
70
|
durationMs: number;
|
|
71
71
|
width?: number | null;
|
|
72
72
|
height?: number | null;
|
|
73
|
+
// What this segment captures. A clip or meeting can add audio while the
|
|
74
|
+
// configured mode is screen only.
|
|
75
|
+
captureMode: RewindCaptureMode;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
export interface ScreenMemoryStatus {
|
|
@@ -417,6 +417,7 @@ pub struct ScreenMemoryActiveSegment {
|
|
|
417
417
|
pub duration_ms: u128,
|
|
418
418
|
pub width: Option<u32>,
|
|
419
419
|
pub height: Option<u32>,
|
|
420
|
+
pub capture_mode: RewindCaptureMode,
|
|
420
421
|
}
|
|
421
422
|
|
|
422
423
|
#[derive(Debug, Clone, Serialize)]
|
|
@@ -1134,6 +1135,7 @@ fn build_status(app: &AppHandle) -> Result<ScreenMemoryStatus, String> {
|
|
|
1134
1135
|
duration_ms: active.started_at.elapsed().as_millis(),
|
|
1135
1136
|
width: active.width,
|
|
1136
1137
|
height: active.height,
|
|
1138
|
+
capture_mode: active.capture_mode,
|
|
1137
1139
|
}),
|
|
1138
1140
|
guard.last_error.clone(),
|
|
1139
1141
|
guard.exclusion_active,
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { readAppState } from "@agent-native/core/application-state";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
organizations,
|
|
5
|
-
orgMembers,
|
|
6
|
-
} from "@agent-native/core/org";
|
|
7
|
-
import { getSession } from "@agent-native/core/server";
|
|
2
|
+
import { implicitServiceOrgRole, organizations } from "@agent-native/core/org";
|
|
3
|
+
import { getSession, readOrgMemberRole } from "@agent-native/core/server";
|
|
8
4
|
import {
|
|
9
5
|
getRequestUserEmail,
|
|
10
6
|
getRequestOrgId,
|
|
@@ -175,20 +171,9 @@ export async function getOrganizationRoleForEmail(
|
|
|
175
171
|
organizationId: string,
|
|
176
172
|
email: string,
|
|
177
173
|
): Promise<OrganizationAccessRole | null> {
|
|
178
|
-
const lowerEmail = email.toLowerCase();
|
|
179
|
-
|
|
180
174
|
try {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
.from(orgMembers)
|
|
184
|
-
.where(
|
|
185
|
-
and(
|
|
186
|
-
eq(orgMembers.orgId, organizationId),
|
|
187
|
-
sql`lower(${orgMembers.email}) = ${lowerEmail}`,
|
|
188
|
-
),
|
|
189
|
-
)
|
|
190
|
-
.limit(1);
|
|
191
|
-
if (row?.role) return normalizeOrganizationRole(row.role);
|
|
175
|
+
const role = await readOrgMemberRole(organizationId, email);
|
|
176
|
+
if (role) return normalizeOrganizationRole(role);
|
|
192
177
|
} catch {
|
|
193
178
|
// org_members table may not exist yet on first boot before migrations finish.
|
|
194
179
|
}
|
|
@@ -88,7 +88,7 @@ stable IDs identify edits. Sparse patches preserve omitted fields, and empty
|
|
|
88
88
|
lists explicitly clear supported settings.
|
|
89
89
|
|
|
90
90
|
Ordinary setup supports stored property types, additive options, metadata edits,
|
|
91
|
-
the existing text natural key, and
|
|
91
|
+
the existing text natural key, table presentation, and list row layouts. It refuses destructive
|
|
92
92
|
type/option changes, source-managed schema edits, and relationship/computed-field
|
|
93
93
|
authoring. Source status is readable, but this surface does not define joins,
|
|
94
94
|
row unions, source bindings, or write-mode changes. Discovery of a source-backed
|
|
@@ -230,6 +230,20 @@ all fields for the current view, and it includes a New property control for
|
|
|
230
230
|
adding fields without returning to the table header. The New property picker
|
|
231
231
|
supports searching property types by label or machine name.
|
|
232
232
|
|
|
233
|
+
List views can save an item layout (`itemLayout`) that decides what each row
|
|
234
|
+
shows: a start slot (page icon, a checkbox property, a status property, or
|
|
235
|
+
nothing), the title weight, line 2 (a text, URL, or non-body Blocks property,
|
|
236
|
+
clamped to 1-3 lines), and ordered property IDs for line 3 and the end of the
|
|
237
|
+
row; end values move under the title on narrow screens and empty values take
|
|
238
|
+
no space. Ticking a row's checkbox writes the property through
|
|
239
|
+
`set-document-property` and strikes the title through without moving the row.
|
|
240
|
+
A view without `itemLayout` keeps the default rows. Agents update it with an
|
|
241
|
+
`update-content-database-view` `update` patch on the list view: pass
|
|
242
|
+
`itemLayoutPreset` (`checklist`, `reminders`, `tracker`, `digest`, or
|
|
243
|
+
`default`), sparse `itemLayout` slots (omitted slots keep their value), or
|
|
244
|
+
`itemLayout: null` to restore the default rows. List views accept only name,
|
|
245
|
+
icon, and these layout fields on this path.
|
|
246
|
+
|
|
233
247
|
In unconstrained table views, row drag handles can reorder collection item
|
|
234
248
|
pages through `move-database-item`; pass both `databaseId` and `itemId` so the
|
|
235
249
|
membership target is exact, and clear search, sort, and filters before manual
|
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
All notable user-facing changes to Agent-Native Content are documented here. Open it any
|
|
4
4
|
time from the command menu (Cmd+K → "What's new").
|
|
5
5
|
|
|
6
|
+
## 2026-10-21
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Organization favorites include documents shared with the current member
|
|
11
|
+
|
|
6
12
|
## 2026-10-19
|
|
7
13
|
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- List rows can be customized: start from a preset like **Checklist**, then choose what shows before, under, and after each title
|
|
17
|
+
|
|
8
18
|
### Improved
|
|
9
19
|
|
|
10
20
|
- You can comment on text in any Blocks field tab; each tab shows its open comments, and the Comments panel groups and filters them by tab
|
|
@@ -12,6 +22,7 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
12
22
|
|
|
13
23
|
### Fixed
|
|
14
24
|
|
|
25
|
+
- List views no longer run off the side of the screen on phones
|
|
15
26
|
- Edits made from a block's menu or a table's row and column menus, such as deleting a table, adding a row or turning off the header row, are now saved instead of coming back after a reload
|
|
16
27
|
- Starting a comment puts the cursor in the comment box, ready to type.
|
|
17
28
|
- Table row and column options now appear for the row or column under the pointer, Escape selects the cell before leaving the editor, wide tables keep their add-column control on screen on phones, and a tap near a table's edge on a touch screen edits the cell there instead of adding a row or column
|
|
@@ -24,6 +35,8 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
24
35
|
|
|
25
36
|
### Added
|
|
26
37
|
|
|
38
|
+
- Connected AI assistants can make a collection from a GitHub folder, or link an empty one: each Markdown file becomes a view-only page, and a sync brings in the latest pushes
|
|
39
|
+
- Make a collection from a GitHub folder in the sidebar's New menu, or fill an empty one from its Sources settings, which say up front when GitHub can't link and what to do instead. A short preview comes before linking, the collection shows where its pages come from and when they last updated, and unlinking asks first
|
|
27
40
|
- Connected AI assistants can link a new collection to a GitHub folder: each Markdown file becomes a view-only page, and a sync brings in the latest pushes
|
|
28
41
|
|
|
29
42
|
### Improved
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ActionContractError } from "@agent-native/core/action";
|
|
2
2
|
import { getDbExec, type DbExec } from "@agent-native/core/db";
|
|
3
|
-
import {
|
|
3
|
+
import { readOrgMemberRole } from "@agent-native/core/server";
|
|
4
|
+
import {
|
|
5
|
+
getRequestContext,
|
|
6
|
+
getRequestUserEmail,
|
|
7
|
+
} from "@agent-native/core/server/request-context";
|
|
4
8
|
import { and, eq, isNull, sql } from "drizzle-orm";
|
|
5
9
|
|
|
6
10
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -13,6 +17,29 @@ export type ContentSpaceAccess = {
|
|
|
13
17
|
role: ContentSpaceRole;
|
|
14
18
|
};
|
|
15
19
|
|
|
20
|
+
function isDelegatedIdentityMismatch(orgId: string, email: string): boolean {
|
|
21
|
+
const request = getRequestContext();
|
|
22
|
+
return (
|
|
23
|
+
request?.a2aIdentityAssurance === "org-member" &&
|
|
24
|
+
(request.orgId?.trim() !== orgId.trim() ||
|
|
25
|
+
request.userEmail?.trim().toLowerCase() !== email.trim().toLowerCase())
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function preserveDelegatedMemberRole(
|
|
30
|
+
orgId: string,
|
|
31
|
+
email: string,
|
|
32
|
+
validatedRole: string,
|
|
33
|
+
verifiedRole: string,
|
|
34
|
+
): string {
|
|
35
|
+
const request = getRequestContext();
|
|
36
|
+
return request?.a2aIdentityAssurance === "org-member" &&
|
|
37
|
+
request.orgId?.trim() === orgId.trim() &&
|
|
38
|
+
request.userEmail?.trim().toLowerCase() === email.trim().toLowerCase()
|
|
39
|
+
? verifiedRole
|
|
40
|
+
: validatedRole;
|
|
41
|
+
}
|
|
42
|
+
|
|
16
43
|
type ContentOrganizationMembership = {
|
|
17
44
|
role: string;
|
|
18
45
|
name: string;
|
|
@@ -59,6 +86,11 @@ export async function getContentOrganizationMembership(
|
|
|
59
86
|
userEmail: string,
|
|
60
87
|
options: { db?: any } = {},
|
|
61
88
|
): Promise<{ role: string; name: string; createdBy: string } | null> {
|
|
89
|
+
if (isDelegatedIdentityMismatch(orgId, userEmail)) return null;
|
|
90
|
+
|
|
91
|
+
const verifiedRole = await readOrgMemberRole(orgId, userEmail);
|
|
92
|
+
if (!verifiedRole) return null;
|
|
93
|
+
|
|
62
94
|
if (options.db) {
|
|
63
95
|
const { organizations, orgMembers } =
|
|
64
96
|
await import("@agent-native/core/org");
|
|
@@ -81,7 +113,7 @@ export async function getContentOrganizationMembership(
|
|
|
81
113
|
)
|
|
82
114
|
.limit(1);
|
|
83
115
|
if (!row) return null;
|
|
84
|
-
|
|
116
|
+
const validated = await revalidateFederatedMembership(orgId, userEmail, {
|
|
85
117
|
role: (typeof row.role === "string"
|
|
86
118
|
? row.role
|
|
87
119
|
: (JSON.stringify(row.role) ?? "member")
|
|
@@ -91,6 +123,17 @@ export async function getContentOrganizationMembership(
|
|
|
91
123
|
identityAuthority: row.identityAuthority,
|
|
92
124
|
identityId: row.identityId,
|
|
93
125
|
});
|
|
126
|
+
return validated
|
|
127
|
+
? {
|
|
128
|
+
...validated,
|
|
129
|
+
role: preserveDelegatedMemberRole(
|
|
130
|
+
orgId,
|
|
131
|
+
userEmail,
|
|
132
|
+
validated.role,
|
|
133
|
+
verifiedRole,
|
|
134
|
+
),
|
|
135
|
+
}
|
|
136
|
+
: null;
|
|
94
137
|
}
|
|
95
138
|
const result = await getDbExec().execute({
|
|
96
139
|
sql: `SELECT m.role AS role, o.name AS name, o.created_by AS "createdBy",
|
|
@@ -119,7 +162,7 @@ export async function getContentOrganizationMembership(
|
|
|
119
162
|
) {
|
|
120
163
|
return null;
|
|
121
164
|
}
|
|
122
|
-
|
|
165
|
+
const validated = await revalidateFederatedMembership(orgId, userEmail, {
|
|
123
166
|
role: (typeof row.role === "string"
|
|
124
167
|
? row.role
|
|
125
168
|
: (JSON.stringify(row.role) ?? "member")
|
|
@@ -129,12 +172,34 @@ export async function getContentOrganizationMembership(
|
|
|
129
172
|
identityAuthority: row.identityAuthority,
|
|
130
173
|
identityId: row.identityId,
|
|
131
174
|
});
|
|
175
|
+
return validated
|
|
176
|
+
? {
|
|
177
|
+
...validated,
|
|
178
|
+
role: preserveDelegatedMemberRole(
|
|
179
|
+
orgId,
|
|
180
|
+
userEmail,
|
|
181
|
+
validated.role,
|
|
182
|
+
verifiedRole,
|
|
183
|
+
),
|
|
184
|
+
}
|
|
185
|
+
: null;
|
|
132
186
|
}
|
|
133
187
|
|
|
134
188
|
export async function listContentOrganizationMemberships(
|
|
135
189
|
userEmail: string,
|
|
136
190
|
transaction?: DbExec,
|
|
137
191
|
) {
|
|
192
|
+
const request = getRequestContext();
|
|
193
|
+
const normalizedEmail = normalizeContentSpaceEmail(userEmail);
|
|
194
|
+
const delegated = request?.a2aIdentityAssurance === "org-member";
|
|
195
|
+
if (
|
|
196
|
+
delegated &&
|
|
197
|
+
(!request.orgId?.trim() ||
|
|
198
|
+
request.userEmail?.trim().toLowerCase() !== normalizedEmail)
|
|
199
|
+
) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
138
203
|
if (transaction) {
|
|
139
204
|
const relation = await transaction.execute({
|
|
140
205
|
sql: "SELECT to_regclass('org_members') AS relation",
|
|
@@ -153,8 +218,11 @@ export async function listContentOrganizationMemberships(
|
|
|
153
218
|
INNER JOIN organizations o ON o.id = m.org_id
|
|
154
219
|
WHERE LOWER(m.email) = $1
|
|
155
220
|
AND m.federation_removal_pending_at IS NULL
|
|
221
|
+
${delegated ? "AND m.org_id = $2" : ""}
|
|
156
222
|
ORDER BY m.org_id ASC`,
|
|
157
|
-
args:
|
|
223
|
+
args: delegated
|
|
224
|
+
? [normalizedEmail, request!.orgId!.trim()]
|
|
225
|
+
: [normalizedEmail],
|
|
158
226
|
});
|
|
159
227
|
} catch (error) {
|
|
160
228
|
if (error instanceof Error && error.message.includes("does not exist")) {
|
|
@@ -174,11 +242,21 @@ export async function listContentOrganizationMemberships(
|
|
|
174
242
|
identityAuthority: row.identityAuthority ?? row.identity_authority,
|
|
175
243
|
identityId: row.identityId ?? row.identity_id,
|
|
176
244
|
}))
|
|
245
|
+
.map((row: ContentOrganizationMembership & { orgId?: string }) => ({
|
|
246
|
+
...row,
|
|
247
|
+
role:
|
|
248
|
+
row.role === "owner" || row.role === "admin" || row.role === "member"
|
|
249
|
+
? delegated
|
|
250
|
+
? "member"
|
|
251
|
+
: row.role
|
|
252
|
+
: null,
|
|
253
|
+
}))
|
|
177
254
|
.filter(
|
|
178
255
|
(row): row is ContentOrganizationMembership & { orgId: string } =>
|
|
179
256
|
typeof row.orgId === "string" &&
|
|
180
257
|
typeof row.name === "string" &&
|
|
181
|
-
typeof row.createdBy === "string"
|
|
258
|
+
typeof row.createdBy === "string" &&
|
|
259
|
+
row.role !== null,
|
|
182
260
|
);
|
|
183
261
|
return Promise.all(
|
|
184
262
|
memberships.map(async ({ orgId, ...membership }) => {
|
|
@@ -187,7 +265,18 @@ export async function listContentOrganizationMemberships(
|
|
|
187
265
|
userEmail,
|
|
188
266
|
membership,
|
|
189
267
|
);
|
|
190
|
-
return validated
|
|
268
|
+
return validated
|
|
269
|
+
? {
|
|
270
|
+
orgId,
|
|
271
|
+
...validated,
|
|
272
|
+
role: preserveDelegatedMemberRole(
|
|
273
|
+
orgId,
|
|
274
|
+
normalizedEmail,
|
|
275
|
+
validated.role,
|
|
276
|
+
membership.role,
|
|
277
|
+
),
|
|
278
|
+
}
|
|
279
|
+
: null;
|
|
191
280
|
}),
|
|
192
281
|
).then((validated) =>
|
|
193
282
|
validated.filter(
|
|
@@ -1018,23 +1018,27 @@ export async function getContentDatabasePageResponse(
|
|
|
1018
1018
|
items.map((item) => item.documentId),
|
|
1019
1019
|
),
|
|
1020
1020
|
isNull(schema.documents.trashedAt),
|
|
1021
|
-
database.systemRole === "
|
|
1022
|
-
?
|
|
1023
|
-
? inArray(schema.documents.id,
|
|
1021
|
+
database.systemRole === "favorites"
|
|
1022
|
+
? favoritesVisibleDocumentQuery
|
|
1023
|
+
? inArray(schema.documents.id, favoritesVisibleDocumentQuery)
|
|
1024
1024
|
: sql`1 = 0`
|
|
1025
|
-
: database.systemRole === "
|
|
1026
|
-
?
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1025
|
+
: database.systemRole === "workspaces"
|
|
1026
|
+
? workspacesVisibleDocumentIds?.length
|
|
1027
|
+
? inArray(schema.documents.id, workspacesVisibleDocumentIds)
|
|
1028
|
+
: sql`1 = 0`
|
|
1029
|
+
: database.systemRole === "files" && database.orgId
|
|
1030
|
+
? and(
|
|
1031
|
+
eq(schema.documents.orgId, database.orgId),
|
|
1032
|
+
or(
|
|
1033
|
+
eq(schema.documents.visibility, "org"),
|
|
1034
|
+
eq(schema.documents.visibility, "public"),
|
|
1035
|
+
),
|
|
1036
|
+
or(
|
|
1037
|
+
eq(schema.documents.hideFromSearch, 0),
|
|
1038
|
+
isNull(schema.documents.hideFromSearch),
|
|
1039
|
+
),
|
|
1040
|
+
)
|
|
1041
|
+
: eq(schema.documents.ownerEmail, database.ownerEmail),
|
|
1038
1042
|
),
|
|
1039
1043
|
)
|
|
1040
1044
|
: [];
|