@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.
Files changed (102) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +27 -2
  3. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +27 -2
  4. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +27 -2
  5. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +27 -2
  6. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  7. package/corpus/templates/clips/app/lib/camera-composite.ts +15 -1
  8. package/corpus/templates/clips/changelog/2026-08-18-clips-no-longer-crashes-when-a-camera-bubble-starts-at-a-tin.md +6 -0
  9. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +27 -2
  10. package/corpus/templates/content/actions/create-content-database.ts +1 -1
  11. package/corpus/templates/content/actions/create-document.ts +1 -1
  12. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +27 -2
  13. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +27 -2
  14. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +6 -1
  15. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +27 -2
  16. package/corpus/templates/factory/.agents/skills/sharing/SKILL.md +27 -2
  17. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +27 -2
  18. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +27 -2
  19. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +27 -2
  20. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +27 -2
  21. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +27 -2
  22. package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +3 -3
  23. package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +3 -3
  24. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -1
  25. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
  26. package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +1 -1
  27. package/corpus/templates/slides/app/pages/DeckEditor.tsx +1 -1
  28. package/corpus/templates/slides/server/handlers/request-auth-context.ts +1 -0
  29. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +27 -2
  30. package/dist/app-config/schema.d.ts +1 -0
  31. package/dist/app-config/workspace.d.ts +1 -0
  32. package/dist/app-config/workspace.js +8 -0
  33. package/dist/chat-threads/schema.d.ts +2 -2
  34. package/dist/client/org/TeamPage.js +15 -2
  35. package/dist/client/org/hooks.d.ts +2 -0
  36. package/dist/client/org/hooks.js +12 -0
  37. package/dist/client/org/index.d.ts +2 -2
  38. package/dist/client/org/index.js +1 -1
  39. package/dist/client/sharing/ShareButton.js +43 -18
  40. package/dist/client/sharing/useShareButtonController.d.ts +12 -2
  41. package/dist/client/sharing/useShareButtonController.js +38 -8
  42. package/dist/collab/awareness.d.ts +2 -2
  43. package/dist/dashboard-storage/schema.d.ts +2 -2
  44. package/dist/data-programs/schema.d.ts +2 -2
  45. package/dist/extensions/schema.d.ts +2 -2
  46. package/dist/extensions/store.js +3 -0
  47. package/dist/localization/core-messages/ar-SA.js +1 -0
  48. package/dist/localization/core-messages/de-DE.js +1 -0
  49. package/dist/localization/core-messages/en-US.d.ts +1 -0
  50. package/dist/localization/core-messages/en-US.js +1 -0
  51. package/dist/localization/core-messages/es-ES.js +1 -0
  52. package/dist/localization/core-messages/fr-FR.js +1 -0
  53. package/dist/localization/core-messages/hi-IN.js +1 -0
  54. package/dist/localization/core-messages/ja-JP.js +1 -0
  55. package/dist/localization/core-messages/ko-KR.js +1 -0
  56. package/dist/localization/core-messages/pt-BR.js +1 -0
  57. package/dist/localization/core-messages/zh-CN.js +1 -0
  58. package/dist/localization/core-messages/zh-TW.js +1 -0
  59. package/dist/localization/core-messages.d.ts +1 -0
  60. package/dist/localization/default-messages.d.ts +4 -0
  61. package/dist/localization/default-messages.js +4 -0
  62. package/dist/org/handlers.d.ts +6 -1
  63. package/dist/org/handlers.js +43 -0
  64. package/dist/org/index.d.ts +3 -3
  65. package/dist/org/index.js +2 -2
  66. package/dist/org/migrations.js +33 -0
  67. package/dist/org/plugin.js +10 -1
  68. package/dist/org/schema.d.ts +315 -0
  69. package/dist/org/schema.js +20 -1
  70. package/dist/org/types.d.ts +3 -0
  71. package/dist/org/workspace-app-access.d.ts +10 -0
  72. package/dist/org/workspace-app-access.js +179 -0
  73. package/dist/progress/routes.d.ts +1 -1
  74. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  75. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  76. package/dist/resources/handlers.d.ts +1 -1
  77. package/dist/secrets/routes.d.ts +9 -9
  78. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  79. package/dist/server/auth.js +14 -1
  80. package/dist/server/poll.js +6 -0
  81. package/dist/sharing/access.d.ts +2 -2
  82. package/dist/sharing/access.js +56 -3
  83. package/dist/sharing/actions/list-resource-shares.d.ts +2 -0
  84. package/dist/sharing/actions/list-resource-shares.js +26 -1
  85. package/dist/sharing/actions/share-resource.d.ts +1 -1
  86. package/dist/sharing/actions/share-resource.js +23 -4
  87. package/dist/sharing/actions/unshare-resource.d.ts +1 -1
  88. package/dist/sharing/actions/unshare-resource.js +1 -1
  89. package/dist/sharing/registry.d.ts +12 -0
  90. package/dist/sharing/schema.d.ts +4 -4
  91. package/dist/sharing/schema.js +2 -2
  92. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  93. package/dist/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  94. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
  95. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +9 -1
  96. package/dist/workspace-connections/actions/list-workspace-user-groups.js +9 -4
  97. package/dist/workspace-connections/groups.d.ts +2 -0
  98. package/dist/workspace-connections/groups.js +16 -5
  99. package/package.json +1 -1
  100. package/src/templates/chat/.agents/skills/sharing/SKILL.md +27 -2
  101. package/src/templates/default/.agents/skills/sharing/SKILL.md +27 -2
  102. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +27 -2
@@ -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");
@@ -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
+ }
@@ -15,6 +15,6 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- ok: boolean;
19
18
  error?: undefined;
19
+ ok: boolean;
20
20
  }>>;
@@ -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;
@@ -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 {};
@@ -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" };
@@ -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
  }
@@ -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 `{type}_shares` table
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
@@ -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 `{type}_shares` table
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
- : undefined,
93
+ : s.principalType === "group"
94
+ ? groupDisplayNames.get(s.principalId)
95
+ : undefined,
71
96
  role: s.role,
72
97
  createdAt: s.createdAt,
73
98
  })),