@agent-native/core 0.162.0 → 0.163.1
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/sharing/SKILL.md +27 -2
- package/corpus/templates/analytics/server/plugins/collab.ts +1 -0
- package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- 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/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/crm/.agents/skills/sharing/SKILL.md +27 -2
- 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/sharing/SKILL.md +27 -2
- package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
- package/corpus/templates/slides/DEVELOPING.md +17 -0
- package/corpus/templates/slides/actions/get-deck-access-status.ts +67 -0
- package/corpus/templates/slides/actions/request-deck-access.ts +569 -0
- package/corpus/templates/slides/agent-native.json +3 -0
- 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/components/layout/Header.tsx +2 -0
- package/corpus/templates/slides/app/hooks/use-deck-access.ts +51 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +12 -0
- package/corpus/templates/slides/app/lib/deck-editor-loading.ts +7 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +173 -33
- package/corpus/templates/slides/changelog/2026-08-18-private-deck-links-now-explain-access-and-let-viewers-notify.md +6 -0
- package/corpus/templates/slides/server/db/schema.ts +10 -0
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
- package/corpus/templates/slides/server/plugins/db.ts +17 -0
- package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
- 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/analytics.d.ts +1 -0
- package/dist/client/analytics.js +19 -5
- 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/sharing/ShareButton.js +43 -18
- package/dist/client/sharing/useShareButtonController.d.ts +12 -2
- package/dist/client/sharing/useShareButtonController.js +38 -8
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/storage.d.ts +2 -0
- package/dist/collab/storage.js +7 -0
- package/dist/config.js +11 -1
- package/dist/dashboard-storage/schema.d.ts +2 -2
- package/dist/data-programs/schema.d.ts +2 -2
- package/dist/deploy/build.d.ts +1 -1
- package/dist/deploy/build.js +89 -12
- 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/observability/routes.d.ts +1 -1
- package/dist/observability/tracking-identity.js +2 -0
- 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/provider-api/actions/custom-provider-registration.d.ts +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- 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 +14 -1
- package/dist/server/collab-plugin.d.ts +6 -0
- package/dist/server/collab-plugin.js +27 -6
- package/dist/server/deploy-environment.d.ts +1 -1
- package/dist/server/deploy-environment.js +40 -2
- package/dist/server/poll.js +6 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/sentry-config.js +8 -4
- package/dist/server/sentry.js +4 -0
- 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/sharing/SKILL.md +27 -2
- package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
- package/dist/tracking/registry.js +8 -6
- package/dist/vite/client.js +5 -0
- 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/docs/content/deployment.mdx +19 -5
- package/package.json +1 -1
- package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
- package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
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";
|
|
@@ -2170,8 +2171,20 @@ function createAuthGuardFn(app) {
|
|
|
2170
2171
|
return;
|
|
2171
2172
|
}
|
|
2172
2173
|
const session = await getSession(event);
|
|
2173
|
-
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
|
+
}
|
|
2174
2186
|
return;
|
|
2187
|
+
}
|
|
2175
2188
|
if (p.startsWith("/api/") || p.startsWith("/_agent-native/")) {
|
|
2176
2189
|
setResponseStatus(event, 401);
|
|
2177
2190
|
return { error: "Unauthorized" };
|
|
@@ -41,6 +41,8 @@ export interface CollabPluginOptions {
|
|
|
41
41
|
idColumn?: string;
|
|
42
42
|
/** Whether to auto-seed existing documents on startup. Default: true */
|
|
43
43
|
autoSeed?: boolean;
|
|
44
|
+
/** Map a source-table id to the id used by the collab document store. */
|
|
45
|
+
resolveCollabDocumentId?: (sourceId: string) => string;
|
|
44
46
|
/**
|
|
45
47
|
* Callback invoked after a collab update to sync the content column.
|
|
46
48
|
* If not provided, the plugin auto-syncs using table/contentColumn/idColumn.
|
|
@@ -78,4 +80,8 @@ export interface CollabPluginOptions {
|
|
|
78
80
|
}
|
|
79
81
|
export declare function normalizeCollabAccess(options: Pick<CollabPluginOptions, "access" | "resourceType" | "resolveResourceId">): NormalizedCollabAccess;
|
|
80
82
|
export declare function createCollabPlugin(options?: CollabPluginOptions): NitroPluginDef;
|
|
83
|
+
export declare function selectUnseededCollabRows(rows: ReadonlyArray<Record<string, unknown>>, idColumn: string, existingDocIds: ReadonlySet<string>, resolveCollabDocumentId?: (sourceId: string) => string): Array<{
|
|
84
|
+
row: Record<string, unknown>;
|
|
85
|
+
docId: string;
|
|
86
|
+
}>;
|
|
81
87
|
export {};
|
|
@@ -16,7 +16,7 @@ import { defineEventHandler, getMethod, setResponseStatus, } from "h3";
|
|
|
16
16
|
import { postAwareness, getActiveUsers } from "../collab/awareness.js";
|
|
17
17
|
import { getCollabEmitter } from "../collab/emitter.js";
|
|
18
18
|
import { getCollabState, postCollabUpdate, postCollabText, postCollabSearchReplace, } from "../collab/routes.js";
|
|
19
|
-
import {
|
|
19
|
+
import { listCollabDocIds } from "../collab/storage.js";
|
|
20
20
|
import { postCollabJson, getCollabJson, postCollabPatch, } from "../collab/struct-routes.js";
|
|
21
21
|
import { seedFromText, seedFromJson } from "../collab/ydoc-manager.js";
|
|
22
22
|
import { getDbExec } from "../db/client.js";
|
|
@@ -82,6 +82,7 @@ function warnForImplicitAllAuthenticatedAccess(table) {
|
|
|
82
82
|
export function createCollabPlugin(options = {}) {
|
|
83
83
|
const normalizedAccess = normalizeCollabAccess(options);
|
|
84
84
|
const { table = "documents", contentColumn = "content", idColumn = "id", autoSeed = true, maxPayloadBytes = DEFAULT_MAX_PAYLOAD_BYTES, } = options;
|
|
85
|
+
const resolveCollabDocumentId = options.resolveCollabDocumentId ?? ((sourceId) => sourceId);
|
|
85
86
|
const resourceType = normalizedAccess.mode === "resource"
|
|
86
87
|
? normalizedAccess.resourceType
|
|
87
88
|
: undefined;
|
|
@@ -308,13 +309,14 @@ export function createCollabPlugin(options = {}) {
|
|
|
308
309
|
// Run in background so it doesn't block startup
|
|
309
310
|
setTimeout(async () => {
|
|
310
311
|
try {
|
|
312
|
+
// Read existing ids once, then apply the plugin's document-id
|
|
313
|
+
// mapping before filtering. A source-table SQL predicate can use
|
|
314
|
+
// the wrong id for integrations such as Analytics' dash-* docs.
|
|
315
|
+
const existingDocIds = await listCollabDocIds();
|
|
311
316
|
const client = getDbExec();
|
|
312
317
|
const { rows } = await client.execute(`SELECT ${idColumn}, ${seedColumn} FROM ${table}`);
|
|
313
|
-
for (const row of rows) {
|
|
314
|
-
|
|
315
|
-
const exists = await hasCollabState(docId);
|
|
316
|
-
if (exists)
|
|
317
|
-
continue;
|
|
318
|
+
for (const { row, docId } of selectUnseededCollabRows(rows, idColumn, existingDocIds, resolveCollabDocumentId)) {
|
|
319
|
+
let seeded = false;
|
|
318
320
|
if (isJson) {
|
|
319
321
|
const raw = row[seedColumn] ?? "{}";
|
|
320
322
|
try {
|
|
@@ -323,6 +325,7 @@ export function createCollabPlugin(options = {}) {
|
|
|
323
325
|
? "array"
|
|
324
326
|
: "map";
|
|
325
327
|
await seedFromJson(docId, parsed, "data", inferredType);
|
|
328
|
+
seeded = true;
|
|
326
329
|
}
|
|
327
330
|
catch {
|
|
328
331
|
// Invalid JSON — skip
|
|
@@ -331,7 +334,10 @@ export function createCollabPlugin(options = {}) {
|
|
|
331
334
|
else {
|
|
332
335
|
const content = row[seedColumn] ?? "";
|
|
333
336
|
await seedFromText(docId, content);
|
|
337
|
+
seeded = true;
|
|
334
338
|
}
|
|
339
|
+
if (seeded)
|
|
340
|
+
existingDocIds.add(docId);
|
|
335
341
|
}
|
|
336
342
|
}
|
|
337
343
|
catch {
|
|
@@ -341,3 +347,18 @@ export function createCollabPlugin(options = {}) {
|
|
|
341
347
|
}
|
|
342
348
|
};
|
|
343
349
|
}
|
|
350
|
+
export function selectUnseededCollabRows(rows, idColumn, existingDocIds, resolveCollabDocumentId = (sourceId) => sourceId) {
|
|
351
|
+
const seenDocIds = new Set(existingDocIds);
|
|
352
|
+
const unseeded = [];
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
const sourceId = String(row[idColumn] ?? "");
|
|
355
|
+
if (!sourceId)
|
|
356
|
+
continue;
|
|
357
|
+
const docId = resolveCollabDocumentId(sourceId);
|
|
358
|
+
if (seenDocIds.has(docId))
|
|
359
|
+
continue;
|
|
360
|
+
seenDocIds.add(docId);
|
|
361
|
+
unseeded.push({ row, docId });
|
|
362
|
+
}
|
|
363
|
+
return unseeded;
|
|
364
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* not the vendor, and a second backend that reports errors without them
|
|
6
6
|
* produces issues nobody can tie to a release.
|
|
7
7
|
*/
|
|
8
|
-
/** The deploy environment name, e.g. `production`, `
|
|
8
|
+
/** The deploy environment name, e.g. `production`, `beta`, or `preview`. */
|
|
9
9
|
export declare function resolveDeployEnvironment(): string;
|
|
10
10
|
/**
|
|
11
11
|
* Resolve the agent-native version baked into core's package.json so the
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { isAgentNativeDeploymentEnvironment } from "../config.js";
|
|
11
12
|
function firstNonEmpty(...values) {
|
|
12
13
|
for (const value of values) {
|
|
13
14
|
const trimmed = value?.trim();
|
|
@@ -16,9 +17,46 @@ function firstNonEmpty(...values) {
|
|
|
16
17
|
}
|
|
17
18
|
return undefined;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
function normalizeFallbackEnvironment(value) {
|
|
21
|
+
const normalized = firstNonEmpty(value)?.toLowerCase();
|
|
22
|
+
if (normalized === "development" || normalized === "test")
|
|
23
|
+
return "local";
|
|
24
|
+
return isAgentNativeDeploymentEnvironment(normalized)
|
|
25
|
+
? normalized
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
/** The deploy environment name, e.g. `production`, `beta`, or `preview`. */
|
|
20
29
|
export function resolveDeployEnvironment() {
|
|
21
|
-
|
|
30
|
+
const explicit = firstNonEmpty(process.env.AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT)?.toLowerCase();
|
|
31
|
+
if (explicit) {
|
|
32
|
+
if (!isAgentNativeDeploymentEnvironment(explicit)) {
|
|
33
|
+
throw new Error('AGENT_NATIVE_DEPLOYMENT_ENVIRONMENT must be "local", "beta", "production", or "preview"');
|
|
34
|
+
}
|
|
35
|
+
return explicit;
|
|
36
|
+
}
|
|
37
|
+
const context = firstNonEmpty(process.env.CONTEXT, process.env.NETLIFY_CONTEXT, process.env.AGENT_NATIVE_BUILD_DEPLOY_CONTEXT)?.toLowerCase();
|
|
38
|
+
const branch = process.env.BRANCH?.trim().toLowerCase();
|
|
39
|
+
const vercelEnv = process.env.VERCEL_ENV?.trim().toLowerCase();
|
|
40
|
+
if (branch === "beta")
|
|
41
|
+
return "beta";
|
|
42
|
+
if (branch === "production" ||
|
|
43
|
+
(context === "production" && branch !== "beta")) {
|
|
44
|
+
return "production";
|
|
45
|
+
}
|
|
46
|
+
if (context === "branch-deploy" && branch === "main")
|
|
47
|
+
return "beta";
|
|
48
|
+
if (context === "deploy-preview" ||
|
|
49
|
+
context === "branch-deploy" ||
|
|
50
|
+
branch?.startsWith("deploy-preview") ||
|
|
51
|
+
vercelEnv === "preview") {
|
|
52
|
+
return "preview";
|
|
53
|
+
}
|
|
54
|
+
if (!context && !branch && !vercelEnv) {
|
|
55
|
+
return (normalizeFallbackEnvironment(firstNonEmpty(process.env.SENTRY_ENVIRONMENT, process.env.NODE_ENV)) ?? "production");
|
|
56
|
+
}
|
|
57
|
+
return (normalizeFallbackEnvironment(firstNonEmpty(context, vercelEnv)) ??
|
|
58
|
+
normalizeFallbackEnvironment(process.env.NODE_ENV) ??
|
|
59
|
+
"production");
|
|
22
60
|
}
|
|
23
61
|
/**
|
|
24
62
|
* Resolve the agent-native version baked into core's package.json so the
|
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
|
}
|
|
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
expiresAt?: undefined;
|
|
27
27
|
ttlSeconds?: undefined;
|
|
28
28
|
} | {
|
|
29
|
-
error?: undefined;
|
|
30
29
|
token: string;
|
|
31
30
|
expiresAt: string;
|
|
32
31
|
ttlSeconds: number;
|
|
32
|
+
error?: undefined;
|
|
33
33
|
}>>;
|
|
@@ -28,11 +28,15 @@ export function resolvePublicSentryDsn() {
|
|
|
28
28
|
}
|
|
29
29
|
export function getSentryClientConfigScript() {
|
|
30
30
|
const dsn = resolvePublicSentryDsn();
|
|
31
|
-
|
|
32
|
-
return null;
|
|
31
|
+
const deploymentEnvironment = resolveDeployEnvironment();
|
|
33
32
|
const config = {
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
...(dsn
|
|
34
|
+
? {
|
|
35
|
+
sentryDsn: dsn,
|
|
36
|
+
sentryEnvironment: resolveSentryEnvironment(),
|
|
37
|
+
}
|
|
38
|
+
: {}),
|
|
39
|
+
deploymentEnvironment,
|
|
36
40
|
};
|
|
37
41
|
return [
|
|
38
42
|
"<script data-agent-native-sentry-config>",
|
package/dist/server/sentry.js
CHANGED
|
@@ -62,6 +62,10 @@ export function initServerSentry() {
|
|
|
62
62
|
// cookies, or process.env contents to Sentry without explicit consent.
|
|
63
63
|
sendDefaultPii: false,
|
|
64
64
|
beforeSend(event) {
|
|
65
|
+
event.tags = {
|
|
66
|
+
...event.tags,
|
|
67
|
+
deployment_environment: resolveDeployEnvironment(),
|
|
68
|
+
};
|
|
65
69
|
// Drop rules live in `error-noise-filter.ts` so every backend applies
|
|
66
70
|
// the same ones — they describe which server errors are non-bugs in
|
|
67
71
|
// this framework, not a Sentry preference.
|
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
|