@agent-native/core 0.162.0 → 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/sharing/SKILL.md +27 -2
- 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/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/pages/DeckEditor.tsx +1 -1
- package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -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/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/dashboard-storage/schema.d.ts +2 -2
- package/dist/data-programs/schema.d.ts +2 -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/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 +8 -8
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- 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 +14 -1
- package/dist/server/poll.js +6 -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/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/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/org/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { table, text, integer } from "../db/schema.js";
|
|
1
|
+
import { table, text, integer, ownableColumns, createSharesTable, } from "../db/schema.js";
|
|
2
2
|
export const organizations = table("organizations", {
|
|
3
3
|
id: text("id").primaryKey(),
|
|
4
4
|
name: text("name").notNull(),
|
|
@@ -39,3 +39,22 @@ export const orgInvitations = table("org_invitations", {
|
|
|
39
39
|
status: text("status").notNull(),
|
|
40
40
|
role: text("role"),
|
|
41
41
|
});
|
|
42
|
+
/** Workspace app access is framework-owned so every mounted app can enforce it. */
|
|
43
|
+
export const workspaceApps = table("workspace_apps", {
|
|
44
|
+
id: text("id").primaryKey(),
|
|
45
|
+
...ownableColumns(),
|
|
46
|
+
// Workspace apps are visible to their organization by default. The generic
|
|
47
|
+
// ownable primitive remains private-by-default for user-created resources;
|
|
48
|
+
// app creation is a deliberate workspace-level exception.
|
|
49
|
+
visibility: text("visibility", {
|
|
50
|
+
enum: ["private", "org", "public"],
|
|
51
|
+
})
|
|
52
|
+
.notNull()
|
|
53
|
+
.default("org"),
|
|
54
|
+
name: text("name").notNull(),
|
|
55
|
+
description: text("description"),
|
|
56
|
+
path: text("path").notNull(),
|
|
57
|
+
createdAt: integer("created_at").notNull(),
|
|
58
|
+
updatedAt: integer("updated_at").notNull(),
|
|
59
|
+
});
|
|
60
|
+
export const workspaceAppShares = createSharesTable("workspace_app_shares");
|
package/dist/org/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type OrgRole = "owner" | "admin" | "member";
|
|
5
5
|
export type RequiredAuthProvider = "google" | null;
|
|
6
|
+
export type WorkspaceAppDefaultVisibility = "private" | "org";
|
|
6
7
|
export interface OrgContext {
|
|
7
8
|
email: string;
|
|
8
9
|
orgId: string | null;
|
|
@@ -43,6 +44,8 @@ export interface OrgInfo {
|
|
|
43
44
|
workspaceUrl: string | null;
|
|
44
45
|
/** Sign-in provider required for members of the active org. */
|
|
45
46
|
requiredAuthProvider: RequiredAuthProvider;
|
|
47
|
+
/** Default visibility applied when a new workspace app is first registered. */
|
|
48
|
+
workspaceAppDefaultVisibility?: WorkspaceAppDefaultVisibility;
|
|
46
49
|
/**
|
|
47
50
|
* Whether the active org has an A2A secret. The value itself is never part
|
|
48
51
|
* of this payload — owners/admins fetch it on demand from
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WorkspaceAppAccessContext {
|
|
2
|
+
email: string;
|
|
3
|
+
orgId?: string | null;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Enforce the workspace-app ACL before a hosted app's authenticated API
|
|
7
|
+
* surface is reached. The app shell remains cacheable and anonymous; this
|
|
8
|
+
* check protects the session-backed APIs/actions that make the app useful.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isWorkspaceAppAccessAllowed(appId: string, context: WorkspaceAppAccessContext): Promise<boolean>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { signA2AToken } from "../a2a/client.js";
|
|
2
|
+
import { getAppConfig } from "../app-config/index.js";
|
|
3
|
+
import { getDbExec } from "../db/client.js";
|
|
4
|
+
import { workspaceUserGroupsIncludeUser } from "../workspace-connections/groups.js";
|
|
5
|
+
import { getOrgA2ASecret, getOrgDomain } from "./context.js";
|
|
6
|
+
const WORKSPACE_APPS_ACTION_PATH = "/_agent-native/actions/list-workspace-apps";
|
|
7
|
+
const WORKSPACE_APP_ACCESS_TIMEOUT_MS = 2_500;
|
|
8
|
+
function normalizedEmail(email) {
|
|
9
|
+
return email.trim().toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
function configuredWorkspaceDirectory() {
|
|
12
|
+
const workspace = getAppConfig().workspace;
|
|
13
|
+
return (workspace.orgDirectoryUrl?.trim() || workspace.gatewayUrl?.trim() || null);
|
|
14
|
+
}
|
|
15
|
+
function workspaceAppsActionUrl(base) {
|
|
16
|
+
try {
|
|
17
|
+
const baseUrl = new URL(base);
|
|
18
|
+
if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const basePath = baseUrl.pathname.replace(/\/+$/, "");
|
|
22
|
+
baseUrl.pathname = `${basePath}${WORKSPACE_APPS_ACTION_PATH}`;
|
|
23
|
+
baseUrl.search = "";
|
|
24
|
+
baseUrl.searchParams.set("includeAgentCards", "false");
|
|
25
|
+
baseUrl.searchParams.set("audience", "all");
|
|
26
|
+
baseUrl.hash = "";
|
|
27
|
+
return baseUrl;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// coercion-ok: malformed configured URL is a typed unavailable registry.
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function workspaceAppsFromResponse(value) {
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value;
|
|
37
|
+
if (value &&
|
|
38
|
+
typeof value === "object" &&
|
|
39
|
+
Array.isArray(value.apps)) {
|
|
40
|
+
return value.apps;
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hosted app databases are not the authority for workspace-app rows. Ask the
|
|
46
|
+
* Dispatch registry action, which resolves the ACL against its shared store,
|
|
47
|
+
* so an app-scoped database cannot accidentally turn a missing local row into
|
|
48
|
+
* access. A configured registry is fail-closed on every network/auth error.
|
|
49
|
+
*/
|
|
50
|
+
async function hostedWorkspaceAppAccess(appId, context, email) {
|
|
51
|
+
const configuredDirectory = configuredWorkspaceDirectory();
|
|
52
|
+
if (!configuredDirectory)
|
|
53
|
+
return null;
|
|
54
|
+
const url = workspaceAppsActionUrl(configuredDirectory);
|
|
55
|
+
if (!url)
|
|
56
|
+
return false;
|
|
57
|
+
const orgId = context.orgId?.trim() || null;
|
|
58
|
+
const [orgDomain, orgSecret] = orgId
|
|
59
|
+
? await Promise.all([getOrgDomain(orgId), getOrgA2ASecret(orgId)])
|
|
60
|
+
: [null, null];
|
|
61
|
+
let token;
|
|
62
|
+
try {
|
|
63
|
+
token = await signA2AToken(email, orgDomain?.trim() || undefined, orgSecret?.trim() || undefined, {
|
|
64
|
+
expiresIn: "1m",
|
|
65
|
+
preferGlobalSecret: true,
|
|
66
|
+
...(orgId ? { extraClaims: { org_id: orgId } } : {}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error("[workspace-app-access] registry token unavailable", error);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeout = setTimeout(() => controller.abort(), WORKSPACE_APP_ACCESS_TIMEOUT_MS);
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(url, {
|
|
77
|
+
headers: {
|
|
78
|
+
accept: "application/json",
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
},
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok)
|
|
84
|
+
return false;
|
|
85
|
+
const apps = workspaceAppsFromResponse(
|
|
86
|
+
// coercion-ok: malformed registry JSON is an authorization failure.
|
|
87
|
+
await response.json().catch(() => null));
|
|
88
|
+
return apps.some((app) => app.id === appId);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error("[workspace-app-access] registry access check failed", error);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Enforce the workspace-app ACL before a hosted app's authenticated API
|
|
100
|
+
* surface is reached. The app shell remains cacheable and anonymous; this
|
|
101
|
+
* check protects the session-backed APIs/actions that make the app useful.
|
|
102
|
+
*/
|
|
103
|
+
export async function isWorkspaceAppAccessAllowed(appId, context) {
|
|
104
|
+
const normalizedAppId = appId.trim();
|
|
105
|
+
const email = normalizedEmail(context.email);
|
|
106
|
+
if (!normalizedAppId || normalizedAppId === "dispatch" || !email) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const hostedAccess = await hostedWorkspaceAppAccess(normalizedAppId, context, email);
|
|
110
|
+
if (hostedAccess !== null)
|
|
111
|
+
return hostedAccess;
|
|
112
|
+
// Standalone/local deployments have no Dispatch registry URL. Keep the
|
|
113
|
+
// direct lookup for that mode, but never let missing or malformed ACL state
|
|
114
|
+
// grant access.
|
|
115
|
+
try {
|
|
116
|
+
const db = getDbExec();
|
|
117
|
+
const appResult = await db.execute({
|
|
118
|
+
sql: `SELECT owner_email, org_id, visibility
|
|
119
|
+
FROM workspace_apps WHERE id = ? LIMIT 1`,
|
|
120
|
+
args: [normalizedAppId],
|
|
121
|
+
});
|
|
122
|
+
const app = appResult.rows[0];
|
|
123
|
+
if (!app)
|
|
124
|
+
return false;
|
|
125
|
+
const ownerEmail = normalizedEmail(typeof app.owner_email === "string" ? app.owner_email : "");
|
|
126
|
+
const resourceOrgId = (typeof app.org_id === "string" ? app.org_id : "").trim() || null;
|
|
127
|
+
const orgId = context.orgId?.trim() || null;
|
|
128
|
+
const sameOrg = !!resourceOrgId && resourceOrgId === orgId;
|
|
129
|
+
if (ownerEmail === email && (!resourceOrgId || sameOrg))
|
|
130
|
+
return true;
|
|
131
|
+
if (!sameOrg || !orgId)
|
|
132
|
+
return false;
|
|
133
|
+
const memberResult = await db.execute({
|
|
134
|
+
sql: `SELECT role FROM org_members
|
|
135
|
+
WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
|
|
136
|
+
args: [orgId, email],
|
|
137
|
+
});
|
|
138
|
+
if (memberResult.rows.length === 0)
|
|
139
|
+
return false;
|
|
140
|
+
const memberRole = String(memberResult.rows[0]?.role ?? "");
|
|
141
|
+
if (memberRole === "owner" || memberRole === "admin")
|
|
142
|
+
return true;
|
|
143
|
+
if (app.visibility === "org")
|
|
144
|
+
return true;
|
|
145
|
+
if (app.visibility !== "private")
|
|
146
|
+
return false;
|
|
147
|
+
const userShare = await db.execute({
|
|
148
|
+
sql: `SELECT 1 FROM workspace_app_shares
|
|
149
|
+
WHERE resource_id = ? AND principal_type = 'user'
|
|
150
|
+
AND LOWER(principal_id) = ? LIMIT 1`,
|
|
151
|
+
args: [normalizedAppId, email],
|
|
152
|
+
});
|
|
153
|
+
if (userShare.rows.length > 0)
|
|
154
|
+
return true;
|
|
155
|
+
const orgShare = await db.execute({
|
|
156
|
+
sql: `SELECT 1 FROM workspace_app_shares
|
|
157
|
+
WHERE resource_id = ? AND principal_type = 'org'
|
|
158
|
+
AND principal_id = ? LIMIT 1`,
|
|
159
|
+
args: [normalizedAppId, orgId],
|
|
160
|
+
});
|
|
161
|
+
if (orgShare.rows.length > 0)
|
|
162
|
+
return true;
|
|
163
|
+
const groupShares = await db.execute({
|
|
164
|
+
sql: `SELECT principal_id FROM workspace_app_shares
|
|
165
|
+
WHERE resource_id = ? AND principal_type = 'group'`,
|
|
166
|
+
args: [normalizedAppId],
|
|
167
|
+
});
|
|
168
|
+
const groupIds = groupShares.rows
|
|
169
|
+
.map((row) => typeof row.principal_id === "string" ? row.principal_id : "")
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
return workspaceUserGroupsIncludeUser(resourceOrgId, groupIds, email);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
// Missing migrations, missing rows, and every other DB failure deny
|
|
175
|
+
// protected app access until the authoritative ACL is available.
|
|
176
|
+
console.error("[workspace-app-access] access check failed", error);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -75,10 +75,10 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
75
75
|
user: "user";
|
|
76
76
|
}>>;
|
|
77
77
|
}, z.core.$strip>>, {
|
|
78
|
+
message?: undefined;
|
|
78
79
|
id?: undefined;
|
|
79
80
|
provider?: undefined;
|
|
80
81
|
deleted?: undefined;
|
|
81
|
-
message?: undefined;
|
|
82
82
|
providers: {
|
|
83
83
|
id: string;
|
|
84
84
|
label: string;
|
|
@@ -93,40 +93,40 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
|
|
|
93
93
|
registered?: undefined;
|
|
94
94
|
label?: undefined;
|
|
95
95
|
} | {
|
|
96
|
+
message?: undefined;
|
|
97
|
+
count?: undefined;
|
|
96
98
|
id?: undefined;
|
|
97
99
|
deleted?: undefined;
|
|
98
|
-
message?: undefined;
|
|
99
100
|
providers?: undefined;
|
|
100
|
-
count?: undefined;
|
|
101
101
|
found: boolean;
|
|
102
102
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
103
103
|
registered?: undefined;
|
|
104
104
|
label?: undefined;
|
|
105
105
|
} | {
|
|
106
|
+
message?: undefined;
|
|
107
|
+
count?: undefined;
|
|
106
108
|
provider?: undefined;
|
|
107
109
|
deleted?: undefined;
|
|
108
|
-
message?: undefined;
|
|
109
110
|
providers?: undefined;
|
|
110
|
-
count?: undefined;
|
|
111
111
|
found: boolean;
|
|
112
112
|
id: string;
|
|
113
113
|
registered?: undefined;
|
|
114
114
|
label?: undefined;
|
|
115
115
|
} | {
|
|
116
|
-
provider?: undefined;
|
|
117
116
|
message?: undefined;
|
|
118
|
-
providers?: undefined;
|
|
119
117
|
count?: undefined;
|
|
118
|
+
provider?: undefined;
|
|
119
|
+
providers?: undefined;
|
|
120
120
|
found?: undefined;
|
|
121
121
|
deleted: boolean;
|
|
122
122
|
id: string;
|
|
123
123
|
registered?: undefined;
|
|
124
124
|
label?: undefined;
|
|
125
125
|
} | {
|
|
126
|
+
count?: undefined;
|
|
126
127
|
provider?: undefined;
|
|
127
128
|
deleted?: undefined;
|
|
128
129
|
providers?: undefined;
|
|
129
|
-
count?: undefined;
|
|
130
130
|
found?: undefined;
|
|
131
131
|
registered: boolean;
|
|
132
132
|
id: string;
|
|
@@ -312,6 +312,7 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
312
312
|
scope?: "org" | "user" | undefined;
|
|
313
313
|
}, {
|
|
314
314
|
id?: undefined;
|
|
315
|
+
deleted?: undefined;
|
|
315
316
|
providers: {
|
|
316
317
|
id: string;
|
|
317
318
|
label: string;
|
|
@@ -324,27 +325,26 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
324
325
|
count: number;
|
|
325
326
|
provider?: undefined;
|
|
326
327
|
found?: undefined;
|
|
327
|
-
deleted?: undefined;
|
|
328
328
|
registered?: undefined;
|
|
329
329
|
label?: undefined;
|
|
330
330
|
message?: undefined;
|
|
331
331
|
} | {
|
|
332
332
|
id?: undefined;
|
|
333
|
+
deleted?: undefined;
|
|
333
334
|
providers?: undefined;
|
|
334
335
|
count?: undefined;
|
|
335
336
|
found: boolean;
|
|
336
337
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
337
|
-
deleted?: undefined;
|
|
338
338
|
registered?: undefined;
|
|
339
339
|
label?: undefined;
|
|
340
340
|
message?: undefined;
|
|
341
341
|
} | {
|
|
342
|
+
deleted?: undefined;
|
|
342
343
|
providers?: undefined;
|
|
343
344
|
count?: undefined;
|
|
344
345
|
provider?: undefined;
|
|
345
346
|
found: boolean;
|
|
346
347
|
id: string;
|
|
347
|
-
deleted?: undefined;
|
|
348
348
|
registered?: undefined;
|
|
349
349
|
label?: undefined;
|
|
350
350
|
message?: undefined;
|
|
@@ -359,11 +359,11 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
359
359
|
label?: undefined;
|
|
360
360
|
message?: undefined;
|
|
361
361
|
} | {
|
|
362
|
+
deleted?: undefined;
|
|
362
363
|
providers?: undefined;
|
|
363
364
|
count?: undefined;
|
|
364
365
|
provider?: undefined;
|
|
365
366
|
found?: undefined;
|
|
366
|
-
deleted?: undefined;
|
|
367
367
|
registered: boolean;
|
|
368
368
|
id: string;
|
|
369
369
|
label: string;
|
|
@@ -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";
|
|
@@ -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" };
|
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
|
}
|
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
|
})),
|