@agent-native/core 0.161.23 → 0.163.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +18 -2
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/calendar/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +88 -35
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +36 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -3
- package/corpus/templates/clips/app/lib/camera-composite.ts +15 -1
- package/corpus/templates/clips/changelog/2026-08-18-clips-no-longer-crashes-when-a-camera-bubble-starts-at-a-tin.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +30 -40
- package/corpus/templates/clips/desktop/src/components/MicOffConfirmation.tsx +47 -0
- package/corpus/templates/clips/desktop/src/styles.css +110 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +114 -7
- package/corpus/templates/clips/shared/recording-capture.ts +18 -13
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/content/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/content/actions/create-content-database.ts +1 -1
- package/corpus/templates/content/actions/create-document.ts +1 -1
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +3 -5
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +0 -3
- package/corpus/templates/content/app/components/editor/NotionConflictBanner.tsx +0 -4
- package/corpus/templates/content/app/components/editor/NotionSyncBar.tsx +4 -1
- package/corpus/templates/content/app/hooks/use-notion.ts +17 -15
- package/corpus/templates/crm/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/design/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/design/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +6 -1
- package/corpus/templates/dispatch/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/factory/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/macros/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +18 -3
- package/corpus/templates/mail/server/handlers/google-auth.ts +1 -8
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +4 -0
- package/corpus/templates/slides/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/actions/_await-fit-check.ts +30 -5
- package/corpus/templates/slides/actions/add-slide.ts +6 -2
- package/corpus/templates/slides/actions/create-deck.ts +2 -1
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +3 -3
- package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +3 -3
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -1
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +1 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +35 -2
- package/corpus/templates/slides/app/routes/p.$id.tsx +3 -1
- package/corpus/templates/slides/changelog/2026-08-18-generated-slides-preserve-speaker-notes.md +6 -0
- package/corpus/templates/slides/server/handlers/assets.ts +23 -12
- package/corpus/templates/slides/server/handlers/design-system-decode-job-status.ts +15 -6
- package/corpus/templates/slides/server/handlers/design-system-upload-start.ts +15 -6
- package/corpus/templates/slides/server/handlers/index-design-system-sources.ts +34 -19
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
- package/corpus/templates/slides/server/handlers/share.ts +1 -1
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/server/routes/api/deck-agent-context.json.get.ts +1 -1
- package/corpus/templates/slides/server/routes/api/exports/[filename].get.ts +9 -3
- package/corpus/templates/slides/server/routes/api/exports/google-slides.post.ts +12 -5
- package/corpus/templates/slides/server/routes/api/exports/html.post.ts +9 -7
- package/corpus/templates/slides/server/routes/api/exports/pptx.post.ts +9 -7
- package/corpus/templates/slides/shared/api.ts +7 -1
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/agent/engine/translate-anthropic.js +27 -2
- package/dist/agent/production-agent.d.ts +2 -2
- package/dist/agent/production-agent.js +16 -8
- package/dist/agent/thread-data-builder.d.ts +1 -0
- package/dist/agent/thread-data-builder.js +6 -4
- package/dist/agent/tool-approval-store.d.ts +10 -1
- package/dist/agent/tool-approval-store.js +19 -2
- package/dist/agent/types.d.ts +9 -0
- package/dist/app-config/schema.d.ts +1 -0
- package/dist/app-config/workspace.d.ts +1 -0
- package/dist/app-config/workspace.js +8 -0
- package/dist/chat-threads/schema.d.ts +2 -2
- package/dist/client/AssistantChat.js +14 -6
- package/dist/client/EnvironmentBadge.d.ts +15 -0
- package/dist/client/EnvironmentBadge.js +79 -0
- package/dist/client/MultiTabAssistantChat.js +73 -17
- package/dist/client/agent-chat-adapter.js +11 -1
- package/dist/client/api-path.js +14 -1
- package/dist/client/app-providers.js +2 -1
- package/dist/client/chat/tool-call-display.d.ts +12 -2
- package/dist/client/chat/tool-call-display.js +17 -7
- package/dist/client/chat/widgets/WorkspaceFileWidget.js +1 -1
- package/dist/client/chat/widgets/builtin-tool-renderers.d.ts +6 -0
- package/dist/client/chat/widgets/builtin-tool-renderers.js +19 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +46 -32
- package/dist/client/onboarding/use-onboarding.d.ts +8 -1
- package/dist/client/onboarding/use-onboarding.js +22 -7
- package/dist/client/org/TeamPage.js +15 -2
- package/dist/client/org/hooks.d.ts +2 -0
- package/dist/client/org/hooks.js +12 -0
- package/dist/client/org/index.d.ts +2 -2
- package/dist/client/org/index.js +1 -1
- package/dist/client/session-replay.js +58 -15
- package/dist/client/settings/SettingsTabsPage.js +19 -6
- package/dist/client/sharing/ShareButton.js +43 -18
- package/dist/client/sharing/useShareButtonController.d.ts +12 -2
- package/dist/client/sharing/useShareButtonController.js +38 -8
- package/dist/client/sse-event-processor.d.ts +7 -0
- package/dist/client/sse-event-processor.js +4 -1
- package/dist/client/ui/index.d.ts +1 -0
- package/dist/client/ui/index.js +1 -0
- package/dist/client/use-action.js +26 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +53 -0
- package/dist/dashboard-storage/schema.d.ts +2 -2
- package/dist/data-programs/schema.d.ts +2 -2
- package/dist/extensions/fetch-tool.js +8 -2
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/extensions/store.js +3 -0
- package/dist/localization/core-messages/ar-SA.js +1 -0
- package/dist/localization/core-messages/de-DE.js +1 -0
- package/dist/localization/core-messages/en-US.d.ts +1 -0
- package/dist/localization/core-messages/en-US.js +1 -0
- package/dist/localization/core-messages/es-ES.js +1 -0
- package/dist/localization/core-messages/fr-FR.js +1 -0
- package/dist/localization/core-messages/hi-IN.js +1 -0
- package/dist/localization/core-messages/ja-JP.js +1 -0
- package/dist/localization/core-messages/ko-KR.js +1 -0
- package/dist/localization/core-messages/pt-BR.js +1 -0
- package/dist/localization/core-messages/zh-CN.js +1 -0
- package/dist/localization/core-messages/zh-TW.js +1 -0
- package/dist/localization/core-messages.d.ts +1 -0
- package/dist/localization/default-messages.d.ts +4 -0
- package/dist/localization/default-messages.js +4 -0
- package/dist/mcp/embed-app.js +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/org/handlers.d.ts +6 -1
- package/dist/org/handlers.js +43 -0
- package/dist/org/index.d.ts +3 -3
- package/dist/org/index.js +2 -2
- package/dist/org/migrations.js +33 -0
- package/dist/org/plugin.js +10 -1
- package/dist/org/schema.d.ts +315 -0
- package/dist/org/schema.js +20 -1
- package/dist/org/types.d.ts +3 -0
- package/dist/org/workspace-app-access.d.ts +10 -0
- package/dist/org/workspace-app-access.js +179 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/index.d.ts +10 -0
- package/dist/provider-api/index.js +45 -9
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +24 -4
- package/dist/server/poll.js +13 -3
- package/dist/server/sentry.d.ts +1 -1
- package/dist/sharing/access.d.ts +2 -2
- package/dist/sharing/access.js +56 -3
- package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
- package/dist/sharing/actions/list-resource-shares.js +26 -1
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/actions/share-resource.js +23 -4
- package/dist/sharing/actions/unshare-resource.d.ts +1 -1
- package/dist/sharing/actions/unshare-resource.js +1 -1
- package/dist/sharing/registry.d.ts +12 -0
- package/dist/sharing/schema.d.ts +4 -4
- package/dist/sharing/schema.js +2 -2
- package/dist/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/vite/client.d.ts +5 -0
- package/dist/vite/client.js +64 -2
- package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +9 -1
- package/dist/workspace-connections/actions/list-workspace-user-groups.js +9 -4
- package/dist/workspace-connections/groups.d.ts +2 -0
- package/dist/workspace-connections/groups.js +16 -5
- package/dist/workspace-files/actions.d.ts +1 -8
- package/dist/workspace-files/actions.js +5 -21
- package/dist/workspace-files/index.d.ts +1 -1
- package/dist/workspace-files/index.js +1 -1
- package/dist/workspace-files/store.d.ts +18 -0
- package/dist/workspace-files/store.js +24 -3
- package/package.json +3 -3
- package/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +1 -1
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
|
@@ -991,6 +991,11 @@ const PROVIDER_CONFIGS = {
|
|
|
991
991
|
id: "slack",
|
|
992
992
|
label: "Slack Web API",
|
|
993
993
|
defaultBaseUrl: "https://slack.com/api",
|
|
994
|
+
// Slack answers every call with HTTP 200, success or failure — see
|
|
995
|
+
// bodyOkField's doc comment. Without this, a rejected chat.postMessage
|
|
996
|
+
// (not_in_channel, channel_not_found, msg_too_long, …) looks identical to
|
|
997
|
+
// a delivered one at the transport level.
|
|
998
|
+
bodyOkField: "ok",
|
|
994
999
|
auth: {
|
|
995
1000
|
type: "bearer",
|
|
996
1001
|
keys: ["SLACK_BOT_TOKEN"],
|
|
@@ -1301,7 +1306,7 @@ export async function executeProviderApiRequest(args, runtime) {
|
|
|
1301
1306
|
body: pageBody,
|
|
1302
1307
|
headers,
|
|
1303
1308
|
});
|
|
1304
|
-
const resp = await fetchWithTimeout(pageUrl.href, {
|
|
1309
|
+
const resp = applyBodyEnvelopeOutcome(await fetchWithTimeout(pageUrl.href, {
|
|
1305
1310
|
method,
|
|
1306
1311
|
headers,
|
|
1307
1312
|
body: pageBody,
|
|
@@ -1315,7 +1320,7 @@ export async function executeProviderApiRequest(args, runtime) {
|
|
|
1315
1320
|
target: describeProviderRequestTarget(pageUrl.href, auth.secretValues),
|
|
1316
1321
|
requestKey,
|
|
1317
1322
|
},
|
|
1318
|
-
});
|
|
1323
|
+
}), config);
|
|
1319
1324
|
return {
|
|
1320
1325
|
text: resp.text ??
|
|
1321
1326
|
(resp.json !== undefined ? JSON.stringify(resp.json) : ""),
|
|
@@ -1347,7 +1352,7 @@ export async function executeProviderApiRequest(args, runtime) {
|
|
|
1347
1352
|
body,
|
|
1348
1353
|
headers,
|
|
1349
1354
|
});
|
|
1350
|
-
const response = await fetchWithTimeout(url.href, {
|
|
1355
|
+
const response = applyBodyEnvelopeOutcome(await fetchWithTimeout(url.href, {
|
|
1351
1356
|
method,
|
|
1352
1357
|
headers,
|
|
1353
1358
|
body,
|
|
@@ -1361,12 +1366,12 @@ export async function executeProviderApiRequest(args, runtime) {
|
|
|
1361
1366
|
target: describeProviderRequestTarget(url.href, auth.secretValues),
|
|
1362
1367
|
requestKey,
|
|
1363
1368
|
},
|
|
1364
|
-
});
|
|
1369
|
+
}), config);
|
|
1365
1370
|
// saveToFile: write full body to workspace file and return compact summary.
|
|
1366
1371
|
if (args.saveToFile) {
|
|
1367
1372
|
const rawText = response.text ??
|
|
1368
1373
|
(response.json !== undefined ? JSON.stringify(response.json) : "");
|
|
1369
|
-
const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status));
|
|
1374
|
+
const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status, response.ok));
|
|
1370
1375
|
return {
|
|
1371
1376
|
provider: { id: config.id, label: config.label },
|
|
1372
1377
|
request: {
|
|
@@ -2119,7 +2124,7 @@ async function executeCustomProviderApiRequest(args, customConfig, runtime) {
|
|
|
2119
2124
|
if (args.saveToFile) {
|
|
2120
2125
|
const rawText = response.text ??
|
|
2121
2126
|
(response.json !== undefined ? JSON.stringify(response.json) : "");
|
|
2122
|
-
const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status));
|
|
2127
|
+
const saved = (await handleSaveToFile(args.saveToFile, rawText, response.contentType, response.status, response.ok));
|
|
2123
2128
|
return {
|
|
2124
2129
|
provider: {
|
|
2125
2130
|
id: customConfig.id,
|
|
@@ -3817,6 +3822,31 @@ function providerQuotaExhaustedResponse(detail) {
|
|
|
3817
3822
|
},
|
|
3818
3823
|
};
|
|
3819
3824
|
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Flip transport-level `ok` to `false` when the provider's own success field
|
|
3827
|
+
* (config.bodyOkField) says the call failed. See ProviderApiConfig's
|
|
3828
|
+
* bodyOkField doc comment: Slack answers every request with HTTP 200, so
|
|
3829
|
+
* without this a rejected send is indistinguishable from a delivered one to
|
|
3830
|
+
* any caller — including the agent — that trusts `response.ok`.
|
|
3831
|
+
*/
|
|
3832
|
+
function applyBodyEnvelopeOutcome(response, config) {
|
|
3833
|
+
const field = config.bodyOkField;
|
|
3834
|
+
if (!field || !response.ok)
|
|
3835
|
+
return response;
|
|
3836
|
+
const body = response.json;
|
|
3837
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
3838
|
+
return response;
|
|
3839
|
+
if (body[field] !== false)
|
|
3840
|
+
return response;
|
|
3841
|
+
const error = stringOrNull(body.error);
|
|
3842
|
+
return {
|
|
3843
|
+
...response,
|
|
3844
|
+
ok: false,
|
|
3845
|
+
statusText: error
|
|
3846
|
+
? `${response.statusText} (${config.label} reported ${field}: false — ${error})`
|
|
3847
|
+
: `${response.statusText} (${config.label} reported ${field}: false)`,
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3820
3850
|
async function fetchProviderResponse(url, fetchOptions, options) {
|
|
3821
3851
|
const startedAt = Date.now();
|
|
3822
3852
|
const res = await fetch(url, fetchOptions);
|
|
@@ -4000,8 +4030,8 @@ function dotGet(obj, path) {
|
|
|
4000
4030
|
* Handle saveToFile: write the full provider-api response body to a workspace
|
|
4001
4031
|
* file and return a compact summary.
|
|
4002
4032
|
*/
|
|
4003
|
-
async function handleSaveToFile(filePath, responseText, contentType, status) {
|
|
4004
|
-
const { writeWorkspaceFile, SAVE_TO_FILE_MAX_BYTES: maxSaveBytes } = await import("../workspace-files/store.js");
|
|
4033
|
+
async function handleSaveToFile(filePath, responseText, contentType, status, ok = true) {
|
|
4034
|
+
const { writeWorkspaceFile, SAVE_TO_FILE_MAX_BYTES: maxSaveBytes, isScratchWorkspacePath, toWorkspaceFileCard, } = await import("../workspace-files/store.js");
|
|
4005
4035
|
const { getRequestOrgId, getRequestUserEmail } = await import("../server/request-context.js");
|
|
4006
4036
|
const orgId = getRequestOrgId();
|
|
4007
4037
|
const email = getRequestUserEmail();
|
|
@@ -4014,7 +4044,7 @@ async function handleSaveToFile(filePath, responseText, contentType, status) {
|
|
|
4014
4044
|
throw new Error("saveToFile requires an authenticated request context (no user email or orgId found).");
|
|
4015
4045
|
}
|
|
4016
4046
|
const mimeType = contentType?.split(";")[0].trim() ?? "text/plain";
|
|
4017
|
-
await writeWorkspaceFile(scope, filePath, responseText, mimeType, {
|
|
4047
|
+
const meta = await writeWorkspaceFile(scope, filePath, responseText, mimeType, {
|
|
4018
4048
|
maxFileBytes: maxSaveBytes,
|
|
4019
4049
|
});
|
|
4020
4050
|
const bytes = Buffer.byteLength(responseText, "utf8");
|
|
@@ -4023,9 +4053,15 @@ async function handleSaveToFile(filePath, responseText, contentType, status) {
|
|
|
4023
4053
|
savedToFile: true,
|
|
4024
4054
|
savedTo: filePath,
|
|
4025
4055
|
status,
|
|
4056
|
+
ok,
|
|
4026
4057
|
bytes,
|
|
4027
4058
|
contentType: mimeType,
|
|
4028
4059
|
preview: preview.length < responseText.length ? `${preview}…` : preview,
|
|
4060
|
+
// A durable (non-scratch) file renders a download card the moment it's
|
|
4061
|
+
// created — no separate show-workspace-file call needed to get a link.
|
|
4062
|
+
...(isScratchWorkspacePath(filePath)
|
|
4063
|
+
? {}
|
|
4064
|
+
: { file: toWorkspaceFileCard(meta) }),
|
|
4029
4065
|
};
|
|
4030
4066
|
}
|
|
4031
4067
|
/**
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
status?: undefined;
|
|
38
37
|
ok?: undefined;
|
|
38
|
+
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
+
ok?: undefined;
|
|
44
45
|
error: string;
|
|
45
46
|
removed?: undefined;
|
|
46
|
-
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
54
54
|
* or the current stored value without changing anything.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
+
ok?: undefined;
|
|
57
58
|
error: string;
|
|
58
59
|
note?: undefined;
|
|
59
|
-
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
|
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
+
error?: undefined;
|
|
30
31
|
ok: boolean;
|
|
31
32
|
key: string;
|
|
32
33
|
baseUrlKey?: string;
|
|
33
34
|
scope: AgentEngineApiKeyScope;
|
|
34
|
-
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
package/dist/server/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { defineEventHandler, getMethod, getQuery, getRequestIP, setResponseHeader, setResponseStatus, getCookie, setCookie, deleteCookie, getHeader, } from "h3";
|
|
3
3
|
import { getAppConfig } from "../app-config/index.js";
|
|
4
|
+
import { isWorkspaceAppAccessAllowed } from "../org/workspace-app-access.js";
|
|
4
5
|
import { EMBED_START_PATH } from "../shared/embed-auth.js";
|
|
5
6
|
import { EMBED_TARGET_HEADER } from "../shared/embed-auth.js";
|
|
6
7
|
import { FIRST_RUN_ONBOARDING_COOKIE, FIRST_RUN_ONBOARDING_MAX_AGE, } from "../shared/first-run-onboarding.js";
|
|
@@ -636,9 +637,16 @@ async function ensureEmailVerifiedForRedirect(request, response) {
|
|
|
636
637
|
args: [email],
|
|
637
638
|
});
|
|
638
639
|
}
|
|
639
|
-
catch {
|
|
640
|
-
// Better Auth already handled the verification route
|
|
641
|
-
// best-effort
|
|
640
|
+
catch (error) {
|
|
641
|
+
// Better Auth already handled the verification route, so this repair
|
|
642
|
+
// staying best-effort (never blocking the response) is correct. But a
|
|
643
|
+
// failure here must never be silent: "repair didn't run" and "there was
|
|
644
|
+
// nothing to repair" are the same observable outcome to a caller unless
|
|
645
|
+
// this is logged, which is exactly the shape of "clicked the verify
|
|
646
|
+
// link, login still says not verified" (Slack C0ATH3CCZT4, reported
|
|
647
|
+
// 2026-07-31 and 2026-08-05) — this UPDATE is the only place that would
|
|
648
|
+
// show whether Better Auth's own write is failing too.
|
|
649
|
+
captureAuthError(error, { route: "verify-email", email });
|
|
642
650
|
}
|
|
643
651
|
}
|
|
644
652
|
async function emailFromVerificationResponseSession(response) {
|
|
@@ -2163,8 +2171,20 @@ function createAuthGuardFn(app) {
|
|
|
2163
2171
|
return;
|
|
2164
2172
|
}
|
|
2165
2173
|
const session = await getSession(event);
|
|
2166
|
-
if (session)
|
|
2174
|
+
if (session) {
|
|
2175
|
+
const workspaceAppId = getAppConfig().app.workspaceId?.trim() || "";
|
|
2176
|
+
if (workspaceAppId &&
|
|
2177
|
+
workspaceAppId !== "dispatch" &&
|
|
2178
|
+
(p.startsWith("/api/") || p.startsWith("/_agent-native/")) &&
|
|
2179
|
+
!(await isWorkspaceAppAccessAllowed(workspaceAppId, {
|
|
2180
|
+
email: session.email,
|
|
2181
|
+
orgId: session.orgId,
|
|
2182
|
+
}))) {
|
|
2183
|
+
setResponseStatus(event, 403);
|
|
2184
|
+
return { error: "You do not have access to this workspace app." };
|
|
2185
|
+
}
|
|
2167
2186
|
return;
|
|
2187
|
+
}
|
|
2168
2188
|
if (p.startsWith("/api/") || p.startsWith("/_agent-native/")) {
|
|
2169
2189
|
setResponseStatus(event, 401);
|
|
2170
2190
|
return { error: "Unauthorized" };
|
package/dist/server/poll.js
CHANGED
|
@@ -231,6 +231,12 @@ function extensionTargetsForRow(row, shareRows) {
|
|
|
231
231
|
else if (principalType === "org" && principalId) {
|
|
232
232
|
addExtensionTarget(targets, { orgId: principalId });
|
|
233
233
|
}
|
|
234
|
+
else if (principalType === "group" && orgId) {
|
|
235
|
+
// Group membership is org-scoped. Invalidating the org target is a
|
|
236
|
+
// conservative fallback that keeps group-share changes fresh for every
|
|
237
|
+
// member without making the poll layer own group-directory queries.
|
|
238
|
+
addExtensionTarget(targets, { orgId });
|
|
239
|
+
}
|
|
234
240
|
}
|
|
235
241
|
return Array.from(targets.values());
|
|
236
242
|
}
|
|
@@ -1506,11 +1512,15 @@ export class AppSyncState {
|
|
|
1506
1512
|
// marker rows back into extension-source events for targeted client
|
|
1507
1513
|
// invalidation while preserving user/org scope.
|
|
1508
1514
|
if (extensionMarkerTs > this.lastExtensionMarkerTs) {
|
|
1515
|
+
// Bound the scan by the watermark instead of reading every marker row
|
|
1516
|
+
// ever written and filtering in memory: this table keeps one row per
|
|
1517
|
+
// session that has ever mutated an extension, so the unbounded read
|
|
1518
|
+
// grows with lifetime users rather than with pending work.
|
|
1509
1519
|
const extensionMarkerResult = await db.execute({
|
|
1510
|
-
sql: "SELECT session_id, value, updated_at FROM application_state WHERE key = ? ORDER BY updated_at ASC",
|
|
1511
|
-
args: [EXTENSION_CHANGE_MARKER_KEY],
|
|
1520
|
+
sql: "SELECT session_id, value, updated_at FROM application_state WHERE key = ? AND updated_at > ? ORDER BY updated_at ASC",
|
|
1521
|
+
args: [EXTENSION_CHANGE_MARKER_KEY, this.lastExtensionMarkerTs],
|
|
1512
1522
|
});
|
|
1513
|
-
const changedExtensionMarkers = extensionMarkerResult.rows
|
|
1523
|
+
const changedExtensionMarkers = extensionMarkerResult.rows;
|
|
1514
1524
|
if (this.lastExtensionMarkerTs > 0) {
|
|
1515
1525
|
this.recordExtensionChanges(changedExtensionMarkers
|
|
1516
1526
|
.map((row) => parseExtensionChangeMarker(row.session_id, row.value))
|
package/dist/server/sentry.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export declare function setSentryRequestContext(ctx: {
|
|
|
57
57
|
* this just records the error for observability.
|
|
58
58
|
*/
|
|
59
59
|
export declare function captureAuthError(error: unknown, context: {
|
|
60
|
-
route: "login" | "signup" | "logout" | "magic-link";
|
|
60
|
+
route: "login" | "signup" | "logout" | "magic-link" | "verify-email";
|
|
61
61
|
email?: string;
|
|
62
62
|
}): string | undefined;
|
|
63
63
|
export interface RouteErrorContext {
|
package/dist/sharing/access.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 1. Direct ownership — `owner_email = currentUser`.
|
|
6
6
|
* 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone
|
|
7
7
|
* in the same org; `public` grants read to any authenticated user.
|
|
8
|
-
* 3. Share rows — per-user or per-org grants in the
|
|
9
|
-
* with a role (`viewer | commenter | editor | admin`).
|
|
8
|
+
* 3. Share rows — per-user, per-group, or per-org grants in the
|
|
9
|
+
* `{type}_shares` table with a role (`viewer | commenter | editor | admin`).
|
|
10
10
|
*
|
|
11
11
|
* Use `applyAccessFilter()` on list/read queries to filter rows the current
|
|
12
12
|
* user can see. Use `assertAccess()` at the top of write actions to reject
|
package/dist/sharing/access.js
CHANGED
|
@@ -5,16 +5,18 @@
|
|
|
5
5
|
* 1. Direct ownership — `owner_email = currentUser`.
|
|
6
6
|
* 2. Visibility — `'private' | 'org' | 'public'`. `org` grants read to anyone
|
|
7
7
|
* in the same org; `public` grants read to any authenticated user.
|
|
8
|
-
* 3. Share rows — per-user or per-org grants in the
|
|
9
|
-
* with a role (`viewer | commenter | editor | admin`).
|
|
8
|
+
* 3. Share rows — per-user, per-group, or per-org grants in the
|
|
9
|
+
* `{type}_shares` table with a role (`viewer | commenter | editor | admin`).
|
|
10
10
|
*
|
|
11
11
|
* Use `applyAccessFilter()` on list/read queries to filter rows the current
|
|
12
12
|
* user can see. Use `assertAccess()` at the top of write actions to reject
|
|
13
13
|
* callers who lack the required role.
|
|
14
14
|
*/
|
|
15
15
|
import { and, eq, or, sql } from "drizzle-orm";
|
|
16
|
+
import { isPostgres } from "../db/client.js";
|
|
16
17
|
import { orgMembers } from "../org/schema.js";
|
|
17
18
|
import { getRequestAuthCapability, getRequestUserEmail, getRequestOrgId, } from "../server/request-context.js";
|
|
19
|
+
import { workspaceUserGroupsIncludeUser, workspaceUserGroupsTable, } from "../workspace-connections/groups.js";
|
|
18
20
|
import { listShareableResources, requireShareableResource, } from "./registry.js";
|
|
19
21
|
import { ROLE_RANK } from "./schema.js";
|
|
20
22
|
/**
|
|
@@ -140,6 +142,38 @@ export function accessFilter(resourceTable, sharesTable, rawCtx = currentAccess(
|
|
|
140
142
|
and ${shareScope}
|
|
141
143
|
and ${minRoleSql(minRole)})`);
|
|
142
144
|
}
|
|
145
|
+
if (reg?.supportsGroupShares && normalizedUserEmail && orgId) {
|
|
146
|
+
const groupTable = sql.raw(workspaceUserGroupsTable());
|
|
147
|
+
const groupMemberPredicate = isPostgres()
|
|
148
|
+
? sql `exists (
|
|
149
|
+
select 1
|
|
150
|
+
from jsonb_array_elements_text(
|
|
151
|
+
workspace_group.member_emails_json::jsonb
|
|
152
|
+
) as group_member(email)
|
|
153
|
+
where lower(group_member.email) = ${normalizedUserEmail}
|
|
154
|
+
)`
|
|
155
|
+
: sql `exists (
|
|
156
|
+
select 1
|
|
157
|
+
from json_each(workspace_group.member_emails_json) as group_member
|
|
158
|
+
where lower(group_member.value) = ${normalizedUserEmail}
|
|
159
|
+
)`;
|
|
160
|
+
clauses.push(sql `exists (select 1 from ${sharesTable}
|
|
161
|
+
where ${sharesTable.resourceId} = ${resourceTable.id}
|
|
162
|
+
and ${sharesTable.principalType} = 'group'
|
|
163
|
+
and ${minRoleSql(minRole)}
|
|
164
|
+
and exists (
|
|
165
|
+
select 1 from ${groupTable} as workspace_group
|
|
166
|
+
where workspace_group.id = ${sharesTable.principalId}
|
|
167
|
+
and workspace_group.org_id = ${resourceTable.orgId}
|
|
168
|
+
and workspace_group.org_id = ${orgId}
|
|
169
|
+
and exists (
|
|
170
|
+
select 1 from ${orgMembers} as workspace_member
|
|
171
|
+
where workspace_member.org_id = workspace_group.org_id
|
|
172
|
+
and lower(workspace_member.email) = ${normalizedUserEmail}
|
|
173
|
+
)
|
|
174
|
+
and ${groupMemberPredicate}
|
|
175
|
+
))`);
|
|
176
|
+
}
|
|
143
177
|
return or(...clauses) ?? sql `1=0`;
|
|
144
178
|
}
|
|
145
179
|
function ownerScopeFilter(reg, resourceTable, ctx) {
|
|
@@ -314,6 +348,9 @@ async function resolveAccessImpl(resourceType, resourceId, rawCtx = currentAcces
|
|
|
314
348
|
ownerMatchesActiveScope(reg, resource, ctx)) {
|
|
315
349
|
return { role: "owner", resource };
|
|
316
350
|
}
|
|
351
|
+
if (reg.canManageAccess && (await reg.canManageAccess(resource, ctx))) {
|
|
352
|
+
return { role: "admin", resource };
|
|
353
|
+
}
|
|
317
354
|
if (resource.visibility === "public" && reg.allowPublic !== false) {
|
|
318
355
|
// No share row needed; default viewer unless the resource registration
|
|
319
356
|
// deliberately grants a stronger public-by-link role or explicit shares
|
|
@@ -359,12 +396,28 @@ async function highestShareRole(reg, resourceId, ctx, resource) {
|
|
|
359
396
|
if (orgId) {
|
|
360
397
|
principalClauses.push(and(eq(reg.sharesTable.principalType, "org"), eq(reg.sharesTable.principalId, orgId)));
|
|
361
398
|
}
|
|
399
|
+
let best = null;
|
|
400
|
+
if (reg.supportsGroupShares && normalizedUserEmail && resource.orgId) {
|
|
401
|
+
const groupRows = await db
|
|
402
|
+
.select({
|
|
403
|
+
principalId: reg.sharesTable.principalId,
|
|
404
|
+
role: reg.sharesTable.role,
|
|
405
|
+
})
|
|
406
|
+
.from(reg.sharesTable)
|
|
407
|
+
.where(and(eq(reg.sharesTable.resourceId, resourceId), eq(reg.sharesTable.principalType, "group")));
|
|
408
|
+
for (const row of groupRows) {
|
|
409
|
+
if (await workspaceUserGroupsIncludeUser(resource.orgId, [row.principalId], normalizedUserEmail)) {
|
|
410
|
+
if (!best || ROLE_RANK[row.role] > ROLE_RANK[best]) {
|
|
411
|
+
best = row.role;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
362
416
|
const rows = await db
|
|
363
417
|
.select({ role: reg.sharesTable.role })
|
|
364
418
|
.from(reg.sharesTable)
|
|
365
419
|
.where(and(eq(reg.sharesTable.resourceId, resourceId), or(...principalClauses)))
|
|
366
420
|
.limit(10);
|
|
367
|
-
let best = null;
|
|
368
421
|
for (const r of rows) {
|
|
369
422
|
if (!best || ROLE_RANK[r.role] > ROLE_RANK[best])
|
|
370
423
|
best = r.role;
|
|
@@ -8,6 +8,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
8
8
|
policy: {
|
|
9
9
|
allowPublic: boolean;
|
|
10
10
|
requireOrgMemberForUserShares: boolean;
|
|
11
|
+
supportsGroupShares?: boolean;
|
|
11
12
|
};
|
|
12
13
|
orgId?: undefined;
|
|
13
14
|
role?: undefined;
|
|
@@ -22,6 +23,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
22
23
|
policy: {
|
|
23
24
|
allowPublic: boolean;
|
|
24
25
|
requireOrgMemberForUserShares: boolean;
|
|
26
|
+
supportsGroupShares?: boolean;
|
|
25
27
|
};
|
|
26
28
|
}>;
|
|
27
29
|
export default _default;
|
|
@@ -2,6 +2,7 @@ import { eq, inArray } from "drizzle-orm";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { defineAction } from "../../action.js";
|
|
4
4
|
import { organizations } from "../../org/schema.js";
|
|
5
|
+
import { listWorkspaceUserGroupsForOrg } from "../../workspace-connections/groups.js";
|
|
5
6
|
import { resolveAccess } from "../access.js";
|
|
6
7
|
import { requireShareableResource } from "../registry.js";
|
|
7
8
|
async function loadOrgDisplayNames(db, shares) {
|
|
@@ -28,6 +29,24 @@ async function loadOrgDisplayNames(db, shares) {
|
|
|
28
29
|
return new Map();
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
async function loadGroupDisplayNames(orgId, shares) {
|
|
33
|
+
const groupIds = Array.from(new Set(shares
|
|
34
|
+
.filter((share) => share.principalType === "group" && share.principalId)
|
|
35
|
+
.map((share) => share.principalId)));
|
|
36
|
+
if (!groupIds.length || !orgId)
|
|
37
|
+
return new Map();
|
|
38
|
+
try {
|
|
39
|
+
const groups = await listWorkspaceUserGroupsForOrg(orgId, groupIds);
|
|
40
|
+
return new Map(groups.flatMap((group) => {
|
|
41
|
+
const id = group.id;
|
|
42
|
+
const name = group.name.trim();
|
|
43
|
+
return id && name ? [[id, name]] : [];
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return new Map();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
31
50
|
export default defineAction({
|
|
32
51
|
description: "List the current visibility and share grants on a shareable resource. Any read access is sufficient.",
|
|
33
52
|
schema: z.object({
|
|
@@ -42,6 +61,9 @@ export default defineAction({
|
|
|
42
61
|
// resources that haven't opted into restrictions.
|
|
43
62
|
allowPublic: reg.allowPublic !== false,
|
|
44
63
|
requireOrgMemberForUserShares: reg.requireOrgMemberForUserShares === true,
|
|
64
|
+
...(reg.supportsGroupShares === true
|
|
65
|
+
? { supportsGroupShares: true }
|
|
66
|
+
: {}),
|
|
45
67
|
};
|
|
46
68
|
// Only `ownerEmail`/`orgId`/`visibility` are read below, all of which the
|
|
47
69
|
// projected load carries — so the share dialog never pulls a resource's
|
|
@@ -55,6 +77,7 @@ export default defineAction({
|
|
|
55
77
|
.from(reg.sharesTable)
|
|
56
78
|
.where(eq(reg.sharesTable.resourceId, args.resourceId));
|
|
57
79
|
const orgDisplayNames = await loadOrgDisplayNames(db, shares);
|
|
80
|
+
const groupDisplayNames = await loadGroupDisplayNames(access.resource.orgId, shares);
|
|
58
81
|
return {
|
|
59
82
|
ownerEmail: access.resource.ownerEmail ?? null,
|
|
60
83
|
orgId: access.resource.orgId ?? null,
|
|
@@ -67,7 +90,9 @@ export default defineAction({
|
|
|
67
90
|
principalId: s.principalId,
|
|
68
91
|
displayName: s.principalType === "org"
|
|
69
92
|
? orgDisplayNames.get(s.principalId)
|
|
70
|
-
:
|
|
93
|
+
: s.principalType === "group"
|
|
94
|
+
? groupDisplayNames.get(s.principalId)
|
|
95
|
+
: undefined,
|
|
71
96
|
role: s.role,
|
|
72
97
|
createdAt: s.createdAt,
|
|
73
98
|
})),
|
|
@@ -3,7 +3,7 @@ export declare function resolveShareNotificationUrl(explicitUrl: string | undefi
|
|
|
3
3
|
declare const _default: import("../../action.js").ActionDefinition<{
|
|
4
4
|
resourceType: string;
|
|
5
5
|
resourceId: string;
|
|
6
|
-
principalType: "org" | "user";
|
|
6
|
+
principalType: "group" | "org" | "user";
|
|
7
7
|
principalId: string;
|
|
8
8
|
role?: "admin" | "commenter" | "editor" | "viewer";
|
|
9
9
|
notify?: boolean;
|
|
@@ -8,6 +8,7 @@ import { sendEmail, isEmailConfigured } from "../../server/email.js";
|
|
|
8
8
|
import { invalidateCollabAccessCache } from "../../server/poll.js";
|
|
9
9
|
import { getRequestUserEmail } from "../../server/request-context.js";
|
|
10
10
|
import { getUserProfile } from "../../user-profile/store.js";
|
|
11
|
+
import { assertWorkspaceUserGroupIds } from "../../workspace-connections/groups.js";
|
|
11
12
|
import { assertAccess, ForbiddenError } from "../access.js";
|
|
12
13
|
import { requireShareableResource } from "../registry.js";
|
|
13
14
|
import { getExtensionShareChangeTargets, notifyExtensionShareChanged, } from "./extension-change.js";
|
|
@@ -122,7 +123,7 @@ async function isOrgMemberOrInvited(orgId, email) {
|
|
|
122
123
|
return invited.rows.length > 0;
|
|
123
124
|
}
|
|
124
125
|
export default defineAction({
|
|
125
|
-
description: "Grant a user or org access to a shareable resource. Owner or admin role required.",
|
|
126
|
+
description: "Grant a user, group, or org access to a shareable resource. Owner or admin role required.",
|
|
126
127
|
// (audit H5) Sharing-grant operations are admin-tier and let a caller
|
|
127
128
|
// expand who can read/write a resource. Refuse from the tools iframe
|
|
128
129
|
// bridge so a malicious shared tool can't silently re-share its
|
|
@@ -134,11 +135,11 @@ export default defineAction({
|
|
|
134
135
|
.describe("Registered resource type, e.g. 'document', 'form'."),
|
|
135
136
|
resourceId: z.string().describe("Id of the resource to share."),
|
|
136
137
|
principalType: z
|
|
137
|
-
.enum(["user", "org"])
|
|
138
|
-
.describe("'user' for an individual, 'org' for
|
|
138
|
+
.enum(["user", "group", "org"])
|
|
139
|
+
.describe("'user' for an individual, 'group' for an organization group, or 'org' for the whole organization."),
|
|
139
140
|
principalId: z
|
|
140
141
|
.string()
|
|
141
|
-
.describe("Email (user) or org id (org) of the principal."),
|
|
142
|
+
.describe("Email (user), group id (group), or org id (org) of the principal."),
|
|
142
143
|
role: z
|
|
143
144
|
.enum(["viewer", "commenter", "editor", "admin"])
|
|
144
145
|
.default("viewer")
|
|
@@ -165,9 +166,24 @@ export default defineAction({
|
|
|
165
166
|
if (!actor)
|
|
166
167
|
throw new ForbiddenError("Not signed in");
|
|
167
168
|
const principalId = normalizePrincipalId(args.principalType, args.principalId);
|
|
169
|
+
if (args.principalType === "group" && reg.supportsGroupShares !== true) {
|
|
170
|
+
throw new ForbiddenError(`${reg.displayName} does not support organization groups yet.`);
|
|
171
|
+
}
|
|
168
172
|
if (args.principalType === "user" && !isEmailPrincipalId(principalId)) {
|
|
169
173
|
throw new Error("User shares must use an email address, not an internal user id.");
|
|
170
174
|
}
|
|
175
|
+
if (args.principalType === "group") {
|
|
176
|
+
const resourceOrgId = access.resource?.orgId;
|
|
177
|
+
if (!resourceOrgId) {
|
|
178
|
+
throw new ForbiddenError(`${reg.displayName} can only be shared with a group from within an organization.`);
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
await assertWorkspaceUserGroupIds([principalId], resourceOrgId);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
throw new ForbiddenError(`${reg.displayName} can only be shared with a group from its own organization.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
171
187
|
const beforeExtensionTargets = await getExtensionShareChangeTargets(args.resourceType, args.resourceId);
|
|
172
188
|
if (reg.requireOrgMemberForUserShares) {
|
|
173
189
|
const resourceOrgId = access.resource?.orgId;
|
|
@@ -189,6 +205,9 @@ export default defineAction({
|
|
|
189
205
|
throw new ForbiddenError(`${reg.displayName} can only be shared with its own organization, not a different one.`);
|
|
190
206
|
}
|
|
191
207
|
}
|
|
208
|
+
else if (args.principalType === "group") {
|
|
209
|
+
// Group ownership was validated above against the resource org.
|
|
210
|
+
}
|
|
192
211
|
}
|
|
193
212
|
const db = reg.getDb();
|
|
194
213
|
const [existing] = await db
|
|
@@ -22,7 +22,7 @@ export default defineAction({
|
|
|
22
22
|
schema: z.object({
|
|
23
23
|
resourceType: z.string(),
|
|
24
24
|
resourceId: z.string(),
|
|
25
|
-
principalType: z.enum(["user", "org"]),
|
|
25
|
+
principalType: z.enum(["user", "group", "org"]),
|
|
26
26
|
principalId: z.string(),
|
|
27
27
|
}),
|
|
28
28
|
run: async (args) => {
|
|
@@ -168,6 +168,18 @@ export interface ShareableResourceRegistration {
|
|
|
168
168
|
* Default: `false` (matches the historical behavior — any email can be granted).
|
|
169
169
|
*/
|
|
170
170
|
requireOrgMemberForUserShares?: boolean;
|
|
171
|
+
/** Set true when this resource's DB is mounted with the org group tables. */
|
|
172
|
+
supportsGroupShares?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Optional resource-specific manager check. Returning true grants the
|
|
175
|
+
* caller admin capability even when the row is private or they have no
|
|
176
|
+
* explicit share. Workspace apps use this for organization owners/admins;
|
|
177
|
+
* the owner is still recognized by the normal owner_email check.
|
|
178
|
+
*/
|
|
179
|
+
canManageAccess?: (resource: any, ctx: {
|
|
180
|
+
userEmail?: string;
|
|
181
|
+
orgId?: string;
|
|
182
|
+
}) => boolean | Promise<boolean>;
|
|
171
183
|
/**
|
|
172
184
|
* Optional per-resource access-context adapter. Most resources should use the
|
|
173
185
|
* request user/org unchanged. Templates with an intentional alternate local
|
package/dist/sharing/schema.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function ownableColumns(): {
|
|
|
42
42
|
* Columns:
|
|
43
43
|
* - `id` — share row id (generated by the share action)
|
|
44
44
|
* - `resourceId` — id of the parent resource
|
|
45
|
-
* - `principalType` — `'user' | 'org'`
|
|
45
|
+
* - `principalType` — `'user' | 'group' | 'org'`
|
|
46
46
|
* - `principalId` — email (for user) or org id (for org)
|
|
47
47
|
* - `role` — `'viewer' | 'commenter' | 'editor' | 'admin'`
|
|
48
48
|
* - `createdBy` — email of the user who created the share
|
|
@@ -100,14 +100,14 @@ export declare function createSharesTable(tableName: string): import("drizzle-or
|
|
|
100
100
|
tableName: string;
|
|
101
101
|
dataType: "string";
|
|
102
102
|
columnType: "SQLiteText";
|
|
103
|
-
data: "org" | "user";
|
|
103
|
+
data: "group" | "org" | "user";
|
|
104
104
|
driverParam: string;
|
|
105
105
|
notNull: true;
|
|
106
106
|
hasDefault: false;
|
|
107
107
|
isPrimaryKey: false;
|
|
108
108
|
isAutoincrement: false;
|
|
109
109
|
hasRuntimeDefault: false;
|
|
110
|
-
enumValues: ["user", "org"];
|
|
110
|
+
enumValues: ["user", "group", "org"];
|
|
111
111
|
baseColumn: never;
|
|
112
112
|
identity: undefined;
|
|
113
113
|
generated: undefined;
|
|
@@ -195,7 +195,7 @@ export declare function createSharesTable(tableName: string): import("drizzle-or
|
|
|
195
195
|
}>;
|
|
196
196
|
export type Visibility = "private" | "org" | "public";
|
|
197
197
|
export type ShareRole = "viewer" | "commenter" | "editor" | "admin";
|
|
198
|
-
export type PrincipalType = "user" | "org";
|
|
198
|
+
export type PrincipalType = "user" | "group" | "org";
|
|
199
199
|
/** Role precedence — higher number = stronger capability. */
|
|
200
200
|
export declare const ROLE_RANK: Record<ShareRole | "owner", number>;
|
|
201
201
|
export declare function roleSatisfies(actual: ShareRole | "owner", minimum: ShareRole | "owner"): boolean;
|