@agent-native/core 0.212.0 → 0.213.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.
Files changed (90) hide show
  1. package/dist/a2a/anthropic-managed-agents.js +1 -0
  2. package/dist/a2a/caller-auth.js +7 -2
  3. package/dist/a2a/client.d.ts +1 -0
  4. package/dist/a2a/client.js +27 -0
  5. package/dist/a2a/handlers.js +15 -3
  6. package/dist/a2a/organization-identity.d.ts +4 -1
  7. package/dist/a2a/organization-identity.js +3 -1
  8. package/dist/a2a/server.d.ts +6 -5
  9. package/dist/a2a/server.js +79 -6
  10. package/dist/a2a-claims.d.ts +1 -0
  11. package/dist/a2a-claims.js +1 -0
  12. package/dist/agent/actions/manage-provider-key-policy.js +4 -2
  13. package/dist/agent/app-model-defaults.js +2 -10
  14. package/dist/agent/loop-settings.js +2 -10
  15. package/dist/audit/actions/export-audit-events.js +1 -0
  16. package/dist/audit/attribution.d.ts +5 -4
  17. package/dist/audit/attribution.js +2 -1
  18. package/dist/audit/index.d.ts +1 -1
  19. package/dist/audit/ocsf.js +1 -0
  20. package/dist/audit/read-scope.js +2 -12
  21. package/dist/audit/record.js +1 -0
  22. package/dist/audit/store.js +12 -3
  23. package/dist/audit/types.d.ts +4 -0
  24. package/dist/automation/index.d.ts +1 -1
  25. package/dist/automations/service.js +3 -10
  26. package/dist/client/org/hooks.d.ts +3 -0
  27. package/dist/client/org/hooks.js +12 -0
  28. package/dist/collab/awareness.d.ts +2 -2
  29. package/dist/collab/struct-routes.d.ts +1 -1
  30. package/dist/connections/catalog.d.ts +1 -1
  31. package/dist/connections/catalog.js +7 -1
  32. package/dist/credentials/index.js +18 -2
  33. package/dist/db/schema-snapshot.generated.js +5 -2
  34. package/dist/feature-flags/a2a-action-route.js +1 -0
  35. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  36. package/dist/file-upload/storage-settings.js +2 -9
  37. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  38. package/dist/jobs/tools.js +2 -12
  39. package/dist/mcp/actions/service-token-access.js +15 -11
  40. package/dist/mcp/credential-issuance.js +9 -2
  41. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  42. package/dist/notifications/routes.d.ts +6 -6
  43. package/dist/org/actions.d.ts +1 -1
  44. package/dist/org/actions.js +3 -7
  45. package/dist/org/app-roles.js +10 -1
  46. package/dist/org/context.d.ts +1 -0
  47. package/dist/org/context.js +122 -24
  48. package/dist/org/federation.d.ts +1 -1
  49. package/dist/org/federation.js +2 -2
  50. package/dist/org/handlers.d.ts +4 -0
  51. package/dist/org/handlers.js +142 -44
  52. package/dist/org/index.d.ts +2 -1
  53. package/dist/org/membership.d.ts +6 -4
  54. package/dist/org/membership.js +7 -5
  55. package/dist/org/migrations.js +6 -0
  56. package/dist/org/plugin.d.ts +1 -0
  57. package/dist/org/plugin.js +9 -1
  58. package/dist/org/private-icon-handlers.js +4 -10
  59. package/dist/org/types.d.ts +2 -0
  60. package/dist/org/workspace-app-access-outcome.d.ts +1 -1
  61. package/dist/org/workspace-app-access.js +77 -14
  62. package/dist/progress/routes.d.ts +1 -1
  63. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  64. package/dist/provider-api/actions/provider-api.d.ts +3 -3
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/routes.d.ts +10 -10
  67. package/dist/server/action-routes.d.ts +1 -0
  68. package/dist/server/action-routes.js +13 -1
  69. package/dist/server/agent-chat/prompt-resources.js +1 -1
  70. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  71. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  72. package/dist/server/index.d.ts +1 -0
  73. package/dist/server/index.js +1 -0
  74. package/dist/server/org-admin.js +3 -2
  75. package/dist/server/personal-provider-key-holders.d.ts +1 -1
  76. package/dist/server/personal-provider-key-holders.js +7 -2
  77. package/dist/server/personal-provider-key-policy.d.ts +2 -0
  78. package/dist/server/personal-provider-key-policy.js +21 -5
  79. package/dist/server/request-context.d.ts +2 -0
  80. package/dist/server/request-context.js +7 -0
  81. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  82. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -13
  83. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  84. package/dist/triggers/routes.js +2 -9
  85. package/dist/usage/alerts-store.js +2 -9
  86. package/dist/usage/metrics-store.js +2 -9
  87. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
  88. package/dist/workspace-connections/groups.js +2 -11
  89. package/package.json +3 -3
  90. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +16 -13
@@ -7,6 +7,7 @@ import { getDbExec, isTransientDatabaseError } from "../db/client.js";
7
7
  import { isAnonymousWaitlistSessionEmail } from "../server/anonymous-identity.js";
8
8
  import { crossSiteCookieAttrs, getSession } from "../server/auth.js";
9
9
  import { shouldWriteFirstRunOnboardingEligibility } from "../server/first-run-onboarding-build-mode.js";
10
+ import { effectiveOrgMemberRole, readOrgMemberRole, } from "../server/personal-provider-key-policy.js";
10
11
  import { getVerifiedServiceIdentityFromEvent, getRequestContext, hasExplicitPersonalOrgScope, } from "../server/request-context.js";
11
12
  import { getSetting } from "../settings/store.js";
12
13
  import { getUserSetting } from "../settings/user-settings.js";
@@ -46,6 +47,32 @@ function normalizeOrgRole(value) {
46
47
  ? value
47
48
  : null;
48
49
  }
50
+ function delegatedOrgMemberScope(email) {
51
+ const requestContext = getRequestContext();
52
+ if (requestContext?.a2aIdentityAssurance !== "org-member")
53
+ return undefined;
54
+ const requestEmail = requestContext.userEmail?.trim().toLowerCase();
55
+ const requestOrgId = requestContext.orgId?.trim();
56
+ if (!requestEmail ||
57
+ !requestOrgId ||
58
+ (email && email.trim().toLowerCase() !== requestEmail)) {
59
+ return null;
60
+ }
61
+ return requestOrgId;
62
+ }
63
+ function scopeMembershipsForDelegatedIdentity(email, memberships) {
64
+ const scopedOrgId = delegatedOrgMemberScope(email);
65
+ if (scopedOrgId === undefined || memberships === null)
66
+ return memberships;
67
+ if (!scopedOrgId)
68
+ return [];
69
+ return memberships.flatMap((membership) => {
70
+ if (membership.orgId !== scopedOrgId)
71
+ return [];
72
+ const role = effectiveOrgMemberRole(membership.role, scopedOrgId, email);
73
+ return role ? [{ ...membership, role }] : [];
74
+ });
75
+ }
49
76
  function emailDomainOf(email) {
50
77
  return email.split("@")[1]?.toLowerCase() || null;
51
78
  }
@@ -94,10 +121,13 @@ async function refreshFederatedMemberships(event, email, memberships, selectedOr
94
121
  if (!result.active) {
95
122
  return memberships.filter((item) => item.orgId !== selected.orgId);
96
123
  }
97
- if (result.role === selected.role) {
124
+ const role = effectiveOrgMemberRole(result.role, selected.orgId, email);
125
+ if (!role)
126
+ return memberships.filter((item) => item.orgId !== selected.orgId);
127
+ if (role === selected.role) {
98
128
  return memberships;
99
129
  }
100
- return memberships.map((item) => item.orgId === selected.orgId ? { ...item, role: result.role } : item);
130
+ return memberships.map((item) => item.orgId === selected.orgId ? { ...item, role } : item);
101
131
  }
102
132
  const MEMBERSHIPS_CACHE_KEY = "__anOrgMembershipsCache";
103
133
  const ACTIVE_ORG_SETTING_CACHE_KEY = "__anActiveOrgSettingCache";
@@ -143,12 +173,19 @@ export function markActiveOrgSelectionChanged(event) {
143
173
  });
144
174
  }
145
175
  function loadMembershipsForEvent(event, email) {
176
+ const scopedOrgId = delegatedOrgMemberScope(email);
177
+ if (scopedOrgId === null)
178
+ return Promise.resolve([]);
146
179
  const ctx = event.context;
147
180
  const cache = (ctx[MEMBERSHIPS_CACHE_KEY] ??
148
181
  (ctx[MEMBERSHIPS_CACHE_KEY] = new Map()));
149
182
  let promise = cache.get(email);
150
183
  if (!promise) {
151
- promise = loadMemberships(email).catch((err) => {
184
+ promise = loadMemberships(email)
185
+ .then((memberships) => scopedOrgId === undefined
186
+ ? memberships
187
+ : scopeMembershipsForDelegatedIdentity(email, memberships))
188
+ .catch((err) => {
152
189
  cache.delete(email);
153
190
  throw err;
154
191
  });
@@ -166,17 +203,25 @@ async function resolveOrgContextUncached(event) {
166
203
  const email = session?.email;
167
204
  if (!email)
168
205
  return EMPTY_CONTEXT;
206
+ const requestContext = getRequestContext();
207
+ const isDelegatedOrgMember = requestContext?.a2aIdentityAssurance === "org-member";
208
+ const delegatedOrgId = delegatedOrgMemberScope(email);
209
+ if (isDelegatedOrgMember && !delegatedOrgId) {
210
+ return { email, orgId: null, orgName: null, role: null };
211
+ }
169
212
  if (isAnonymousWaitlistSessionEmail(email)) {
170
213
  return { email, orgId: null, orgName: null, role: null };
171
214
  }
172
- if (hasExplicitPersonalOrgScope(event)) {
215
+ if (!isDelegatedOrgMember && hasExplicitPersonalOrgScope(event)) {
173
216
  return { email, orgId: null, orgName: null, role: null };
174
217
  }
175
- const sessionOrgId = typeof session.orgId === "string" && session.orgId.trim()
176
- ? session.orgId.trim()
177
- : null;
178
- const sessionOrgRole = normalizeOrgRole(session.orgRole);
179
- const requestContext = getRequestContext();
218
+ const sessionOrgId = delegatedOrgId ??
219
+ (typeof session.orgId === "string" && session.orgId.trim()
220
+ ? session.orgId.trim()
221
+ : null);
222
+ const sessionOrgRole = isDelegatedOrgMember
223
+ ? "member"
224
+ : normalizeOrgRole(session.orgRole);
180
225
  const verifiedServiceIdentity = getVerifiedServiceIdentityFromEvent(event);
181
226
  const serviceRole = implicitServiceOrgRole({
182
227
  email,
@@ -209,7 +254,9 @@ async function resolveOrgContextUncached(event) {
209
254
  }
210
255
  }
211
256
  const exec = getDbExec();
212
- const activeOrgSettingPromise = loadActiveOrgSettingForEvent(event, email);
257
+ const activeOrgSettingPromise = isDelegatedOrgMember
258
+ ? Promise.resolve(null)
259
+ : loadActiveOrgSettingForEvent(event, email);
213
260
  activeOrgSettingPromise.catch(() => { });
214
261
  let memberships;
215
262
  try {
@@ -219,6 +266,10 @@ async function resolveOrgContextUncached(event) {
219
266
  // A transient membership read must not downgrade an authenticated request
220
267
  // to a private/solo scope when the session already carries its org.
221
268
  if (sessionOrgId && isTransientDatabaseError(err)) {
269
+ if (isDelegatedOrgMember) {
270
+ markOrgScopeResolutionUnavailable(event);
271
+ return { email, orgId: null, orgName: null, role: null };
272
+ }
222
273
  return {
223
274
  email,
224
275
  orgId: sessionOrgId,
@@ -231,6 +282,9 @@ async function resolveOrgContextUncached(event) {
231
282
  if (memberships === null) {
232
283
  // A null membership read is unavailable scope, not proof of a solo user.
233
284
  markOrgScopeResolutionUnavailable(event);
285
+ if (isDelegatedOrgMember) {
286
+ return { email, orgId: null, orgName: null, role: null };
287
+ }
234
288
  if (sessionOrgId) {
235
289
  return {
236
290
  email,
@@ -242,16 +296,18 @@ async function resolveOrgContextUncached(event) {
242
296
  return { email, orgId: null, orgName: null, role: null };
243
297
  }
244
298
  const activeOrgSetting = await activeOrgSettingPromise;
245
- const explicitPersonal = activeOrgSetting?.orgId === null;
246
- const selectedMembership = explicitPersonal
247
- ? undefined
248
- : ((activeOrgSetting?.orgId
249
- ? memberships.find((membership) => membership.orgId === activeOrgSetting.orgId)
250
- : undefined) ??
251
- (sessionOrgId
252
- ? memberships.find((membership) => membership.orgId === sessionOrgId)
299
+ const explicitPersonal = !isDelegatedOrgMember && activeOrgSetting?.orgId === null;
300
+ const selectedMembership = isDelegatedOrgMember
301
+ ? memberships.find((membership) => membership.orgId === delegatedOrgId)
302
+ : explicitPersonal
303
+ ? undefined
304
+ : ((activeOrgSetting?.orgId
305
+ ? memberships.find((membership) => membership.orgId === activeOrgSetting.orgId)
253
306
  : undefined) ??
254
- memberships[0]);
307
+ (sessionOrgId
308
+ ? memberships.find((membership) => membership.orgId === sessionOrgId)
309
+ : undefined) ??
310
+ memberships[0]);
255
311
  const selectedOrgId = selectedMembership?.orgId ?? null;
256
312
  const selectedWasFederated = Boolean(selectedMembership?.identityAuthority || selectedMembership?.identityId);
257
313
  const refreshedMemberships = await refreshFederatedMemberships(event, email, memberships, selectedOrgId);
@@ -268,7 +324,8 @@ async function resolveOrgContextUncached(event) {
268
324
  markFederationMembershipValidated(event, email, selectedOrgId);
269
325
  }
270
326
  const emailDomain = emailDomainOf(email);
271
- const shouldTryDomainAutoJoin = !explicitPersonal &&
327
+ const shouldTryDomainAutoJoin = !isDelegatedOrgMember &&
328
+ !explicitPersonal &&
272
329
  session.emailVerified === true &&
273
330
  emailDomain !== null &&
274
331
  !isFreeEmailProvider(emailDomain) &&
@@ -358,7 +415,9 @@ async function resolveOrgContextUncached(event) {
358
415
  }
359
416
  return { email, orgId: null, orgName: null, role: null };
360
417
  }
361
- if (memberships.length === 0 && autoCreateDefaultOrgEnabled()) {
418
+ if (!isDelegatedOrgMember &&
419
+ memberships.length === 0 &&
420
+ autoCreateDefaultOrgEnabled()) {
362
421
  const created = await tryCreateDefaultOrg(exec, email, session, event);
363
422
  if (created)
364
423
  return created;
@@ -381,7 +440,8 @@ async function loadMemberships(email) {
381
440
  return cachedMemberships(email, () => loadMembershipsUncached(email));
382
441
  }
383
442
  export async function listOrgMemberships(email) {
384
- return loadMemberships(email);
443
+ const memberships = await loadMemberships(email);
444
+ return scopeMembershipsForDelegatedIdentity(email, memberships);
385
445
  }
386
446
  export async function listOrgMembershipsForEvent(event, email, selectedOrgId) {
387
447
  const memberships = await loadMembershipsForEvent(event, email);
@@ -423,6 +483,15 @@ async function loadMembershipsUncached(email) {
423
483
  }) ?? null);
424
484
  }
425
485
  export async function resolveOrgScopeForEmail(email) {
486
+ const delegatedOrgId = delegatedOrgMemberScope(email);
487
+ if (delegatedOrgId !== undefined) {
488
+ if (!delegatedOrgId)
489
+ return { status: "unknown" };
490
+ const role = await readOrgMemberRole(delegatedOrgId, email);
491
+ return role
492
+ ? { status: "organization", orgId: delegatedOrgId }
493
+ : { status: "unknown" };
494
+ }
426
495
  if (getRequestContext()?.orgScope === "personal") {
427
496
  return { status: "personal" };
428
497
  }
@@ -457,6 +526,13 @@ export async function resolveActiveOrgIdForStorageCredential(email) {
457
526
  const normalizedEmail = email.trim().toLowerCase();
458
527
  if (!normalizedEmail)
459
528
  return null;
529
+ const delegatedOrgId = delegatedOrgMemberScope(normalizedEmail);
530
+ if (delegatedOrgId !== undefined) {
531
+ if (!delegatedOrgId)
532
+ return null;
533
+ const role = await readOrgMemberRole(delegatedOrgId, normalizedEmail);
534
+ return role ? delegatedOrgId : null;
535
+ }
460
536
  const { rows } = await getDbExec().execute({
461
537
  sql: `SELECT m.org_id AS "orgId"
462
538
  FROM org_members m
@@ -476,6 +552,22 @@ export async function resolveActiveOrgIdForStorageCredential(email) {
476
552
  return rows.some((row) => row.orgId === activeOrgId) ? activeOrgId : null;
477
553
  }
478
554
  export async function resolveOrgIdForEmailViaEvent(event, email) {
555
+ const delegatedOrgId = delegatedOrgMemberScope(email);
556
+ if (delegatedOrgId !== undefined) {
557
+ if (!delegatedOrgId)
558
+ return null;
559
+ const memberships = await loadMembershipsForEvent(event, email);
560
+ if (!memberships || !memberships.some((m) => m.orgId === delegatedOrgId)) {
561
+ return null;
562
+ }
563
+ const refreshed = await refreshFederatedMemberships(event, email, memberships, delegatedOrgId);
564
+ if (refreshed !== memberships) {
565
+ updateMembershipsForEvent(event, email, refreshed);
566
+ }
567
+ return refreshed.some((m) => m.orgId === delegatedOrgId)
568
+ ? delegatedOrgId
569
+ : null;
570
+ }
479
571
  if (getRequestContext()?.orgScope === "personal")
480
572
  return null;
481
573
  const settingPromise = loadActiveOrgSettingForEvent(event, email);
@@ -842,7 +934,8 @@ export async function resolveA2AOrganizationCredentialsByDomain(domain) {
842
934
  if (!normalizedDomain)
843
935
  return null;
844
936
  const { rows } = await getDbExec().execute({
845
- sql: `SELECT id, allowed_domain, a2a_secret
937
+ sql: `SELECT id, allowed_domain, a2a_secret,
938
+ a2a_member_delegation_enabled
846
939
  FROM organizations WHERE LOWER(allowed_domain) = ? LIMIT 2`,
847
940
  args: [normalizedDomain],
848
941
  });
@@ -858,7 +951,12 @@ export async function resolveA2AOrganizationCredentialsByDomain(domain) {
858
951
  const secret = String(row.a2a_secret ?? "").trim();
859
952
  if (!orgId || orgDomain !== normalizedDomain || !secret)
860
953
  return null;
861
- return { orgId, orgDomain, secret };
954
+ return {
955
+ orgId,
956
+ orgDomain,
957
+ secret,
958
+ a2aMemberDelegationEnabled: row.a2a_member_delegation_enabled === true,
959
+ };
862
960
  }
863
961
  export async function isSoleOrgDomain(domain) {
864
962
  try {
@@ -52,7 +52,7 @@ export declare function validateFederatedOrganizationMembership(event: H3Event |
52
52
  export declare function validateFederatedOrganizationMembershipForCurrentRequest(input: {
53
53
  orgId: string;
54
54
  email: string;
55
- }): Promise<FederatedMembershipValidation>;
55
+ }, event?: H3Event): Promise<FederatedMembershipValidation>;
56
56
  export declare function syncOrganizationToIdentityHub(event: H3Event, input: FederatedOrganizationSyncInput): Promise<boolean>;
57
57
  export declare function provisionFederatedOrganization(identity: FederatedOrganizationIdentity, options?: {
58
58
  /** The signing-in caller's request; see `setActiveOrgId`. */
@@ -408,8 +408,8 @@ export async function validateFederatedOrganizationMembership(event, input) {
408
408
  }
409
409
  return { active: true, role: body.memberRole };
410
410
  }
411
- export async function validateFederatedOrganizationMembershipForCurrentRequest(input) {
412
- return validateFederatedOrganizationMembership(requestEventFromContext(), input);
411
+ export async function validateFederatedOrganizationMembershipForCurrentRequest(input, event) {
412
+ return validateFederatedOrganizationMembership(event ?? requestEventFromContext(), input);
413
413
  }
414
414
  export async function syncOrganizationToIdentityHub(event, input) {
415
415
  const rollout = await federationRolloutState(input.email, input.id);
@@ -62,6 +62,7 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
62
62
  workspaceAppDefaultVisibility: WorkspaceAppDefaultVisibility;
63
63
  signInMethods: import("./sign-in-methods.js").OrgSignInMethods;
64
64
  a2aSecretSet: boolean;
65
+ a2aMemberDelegationEnabled: boolean;
65
66
  soloDeploymentAdmin: boolean;
66
67
  }>>;
67
68
  export declare const retryPendingFederatedRemovalHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
@@ -175,6 +176,9 @@ export declare const setRequiredAuthProviderHandler: import("h3").EventHandlerWi
175
176
  revokedLegacySessions: number;
176
177
  provider: RequiredAuthProvider;
177
178
  }>>;
179
+ export declare const setA2AMemberDelegationHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
180
+ enabled: any;
181
+ }>>;
178
182
  export declare const revealA2ASecretHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
179
183
  a2aSecret: string;
180
184
  }>>;
@@ -35,6 +35,8 @@ import { getAppProductionUrl, resolveAppRuntimeUrl, } from "../server/app-url.js
35
35
  import { isTrustedSelfHostedRuntime, resolveVercelDeploymentProtectionHeaders, } from "../server/credential-provider.js";
36
36
  import { sendEmail, isEmailConfigured } from "../server/email.js";
37
37
  import { readBody } from "../server/h3-helpers.js";
38
+ import { effectiveOrgMemberRole } from "../server/personal-provider-key-policy.js";
39
+ import { getRequestContext } from "../server/request-context.js";
38
40
  import { resolveDeploymentSignInMethods } from "../server/social-sign-in-providers.js";
39
41
  import { getOrgSetting, putOrgSetting } from "../settings/org-settings.js";
40
42
  import { isEmailDerivedName } from "../user-profile/shared.js";
@@ -140,37 +142,82 @@ async function getSessionForEvent(event) {
140
142
  }
141
143
  export const getMyOrgHandler = defineEventHandler(async (event) => {
142
144
  const ctx = await getOrgContext(event);
145
+ const requestContext = getRequestContext();
146
+ const delegatedOrgMember = requestContext?.a2aIdentityAssurance === "org-member";
147
+ const delegatedEmail = requestContext?.userEmail?.trim().toLowerCase();
148
+ const delegatedOrgId = delegatedOrgMember
149
+ ? requestContext.orgId?.trim()
150
+ : undefined;
151
+ const scopedDelegatedOrgId = delegatedEmail &&
152
+ delegatedEmail === ctx.email.trim().toLowerCase() &&
153
+ delegatedOrgId &&
154
+ ctx.orgId === delegatedOrgId
155
+ ? delegatedOrgId
156
+ : null;
157
+ const visibleOrgId = delegatedOrgMember ? scopedDelegatedOrgId : ctx.orgId;
158
+ const visibleOrgName = visibleOrgId ? ctx.orgName : null;
159
+ const visibleRole = delegatedOrgMember
160
+ ? visibleOrgId
161
+ ? "member"
162
+ : null
163
+ : ctx.role;
143
164
  const e = await exec();
144
- const allOrgsRes = await e.execute({
145
- sql: `SELECT m.org_id AS "orgId", m.role AS role, o.name AS "orgName",
146
- o.icon_json AS "iconJson", o.icon_revision AS "iconRevision"
147
- FROM org_members m
148
- INNER JOIN organizations o ON m.org_id = o.id
149
- WHERE LOWER(m.email) = ?
150
- AND m.federation_removal_pending_at IS NULL`,
151
- args: [ctx.email.toLowerCase()],
152
- });
153
- const orgs = allOrgsRes.rows.map((r) => ({
154
- orgId: String(r.orgId ?? r.org_id),
155
- role: String(r.role),
156
- orgName: String(r.orgName ?? r.org_name),
157
- icon: parseOrganizationIconJson(r.iconJson ?? r.icon_json),
158
- iconRevision: Number(r.iconRevision ?? r.icon_revision ?? 0),
159
- }));
160
- const pendingRemovalRes = await e.execute({
161
- sql: `SELECT m.org_id AS "orgId", o.name AS "orgName"
162
- FROM org_members m
163
- INNER JOIN organizations o ON m.org_id = o.id
164
- WHERE LOWER(m.email) = ?
165
- AND m.federation_removal_pending_at IS NOT NULL`,
166
- args: [ctx.email.toLowerCase()],
165
+ const allOrgsRes = delegatedOrgMember
166
+ ? scopedDelegatedOrgId
167
+ ? await e.execute({
168
+ sql: `SELECT m.org_id AS "orgId", m.role AS role, o.name AS "orgName",
169
+ o.icon_json AS "iconJson", o.icon_revision AS "iconRevision"
170
+ FROM org_members m
171
+ INNER JOIN organizations o ON m.org_id = o.id
172
+ WHERE LOWER(m.email) = ? AND m.org_id = ?
173
+ AND m.federation_removal_pending_at IS NULL`,
174
+ args: [ctx.email.toLowerCase(), scopedDelegatedOrgId],
175
+ })
176
+ : { rows: [] }
177
+ : await e.execute({
178
+ sql: `SELECT m.org_id AS "orgId", m.role AS role, o.name AS "orgName",
179
+ o.icon_json AS "iconJson", o.icon_revision AS "iconRevision"
180
+ FROM org_members m
181
+ INNER JOIN organizations o ON m.org_id = o.id
182
+ WHERE LOWER(m.email) = ?
183
+ AND m.federation_removal_pending_at IS NULL`,
184
+ args: [ctx.email.toLowerCase()],
185
+ });
186
+ const orgs = allOrgsRes.rows.flatMap((r) => {
187
+ const orgId = String(r.orgId ?? r.org_id);
188
+ const role = delegatedOrgMember
189
+ ? effectiveOrgMemberRole(r.role, orgId, ctx.email)
190
+ : String(r.role);
191
+ return role
192
+ ? [
193
+ {
194
+ orgId,
195
+ role,
196
+ orgName: String(r.orgName ?? r.org_name),
197
+ icon: parseOrganizationIconJson(r.iconJson ?? r.icon_json),
198
+ iconRevision: Number(r.iconRevision ?? r.icon_revision ?? 0),
199
+ },
200
+ ]
201
+ : [];
167
202
  });
203
+ const pendingRemovalRes = delegatedOrgMember
204
+ ? { rows: [] }
205
+ : await e.execute({
206
+ sql: `SELECT m.org_id AS "orgId", o.name AS "orgName"
207
+ FROM org_members m
208
+ INNER JOIN organizations o ON m.org_id = o.id
209
+ WHERE LOWER(m.email) = ?
210
+ AND m.federation_removal_pending_at IS NOT NULL`,
211
+ args: [ctx.email.toLowerCase()],
212
+ });
168
213
  const pendingRemovals = pendingRemovalRes.rows.map((r) => ({
169
214
  orgId: String(r.orgId ?? r.org_id),
170
215
  orgName: String(r.orgName ?? r.org_name),
171
216
  }));
172
217
  let domainMatches = [];
173
- const domain = ctx.email.split("@")[1]?.toLowerCase();
218
+ const domain = delegatedOrgMember
219
+ ? undefined
220
+ : ctx.email.split("@")[1]?.toLowerCase();
174
221
  if (domain) {
175
222
  try {
176
223
  const dmRes = await e.execute({
@@ -199,14 +246,16 @@ export const getMyOrgHandler = defineEventHandler(async (event) => {
199
246
  let workspaceUrl = null;
200
247
  let requiredAuthProvider = null;
201
248
  let a2aSecretSet = false;
249
+ let a2aMemberDelegationEnabled = false;
202
250
  let icon = null;
203
251
  let iconRevision = 0;
204
- if (ctx.orgId) {
252
+ if (visibleOrgId) {
205
253
  const adRes = await e.execute({
206
- sql: `SELECT allowed_domain, a2a_secret, workspace_url, required_auth_provider,
254
+ sql: `SELECT allowed_domain, a2a_secret, a2a_member_delegation_enabled,
255
+ workspace_url, required_auth_provider,
207
256
  icon_json, icon_revision
208
257
  FROM organizations WHERE id = ? LIMIT 1`,
209
- args: [ctx.orgId],
258
+ args: [visibleOrgId],
210
259
  });
211
260
  if (adRes.rows[0]) {
212
261
  allowedDomain =
@@ -214,35 +263,49 @@ export const getMyOrgHandler = defineEventHandler(async (event) => {
214
263
  workspaceUrl = String(adRes.rows[0].workspace_url ?? "") || null;
215
264
  requiredAuthProvider = parseRequiredAuthProvider(adRes.rows[0].required_auth_provider ?? null);
216
265
  a2aSecretSet = Boolean(String(adRes.rows[0].a2a_secret ?? "").trim());
266
+ a2aMemberDelegationEnabled =
267
+ adRes.rows[0].a2a_member_delegation_enabled === true;
217
268
  icon = parseOrganizationIconJson(adRes.rows[0].icon_json);
218
269
  iconRevision = Number(adRes.rows[0].icon_revision ?? 0);
219
270
  }
220
271
  }
221
- const isOwnerOrAdmin = ctx.role === "owner" || ctx.role === "admin";
272
+ const isOwnerOrAdmin = visibleRole === "owner" || visibleRole === "admin";
222
273
  let workspaceAppDefaultVisibility = "org";
223
- if (ctx.orgId) {
224
- const setting = await getOrgSetting(ctx.orgId, WORKSPACE_APP_DEFAULT_VISIBILITY_KEY);
274
+ if (visibleOrgId) {
275
+ const setting = await getOrgSetting(visibleOrgId, WORKSPACE_APP_DEFAULT_VISIBILITY_KEY);
225
276
  workspaceAppDefaultVisibility = normalizeWorkspaceAppDefaultVisibility(setting?.visibility);
226
277
  }
227
- const invitesRes = await e.execute({
228
- sql: `SELECT i.id AS id, i.org_id AS "orgId", o.name AS "orgName", i.invited_by AS "invitedBy"
229
- FROM org_invitations i
230
- INNER JOIN organizations o ON i.org_id = o.id
231
- WHERE LOWER(i.email) = ? AND i.status = 'pending'`,
232
- args: [ctx.email.toLowerCase()],
233
- });
278
+ const invitesRes = delegatedOrgMember
279
+ ? scopedDelegatedOrgId
280
+ ? await e.execute({
281
+ sql: `SELECT i.id AS id, i.org_id AS "orgId", o.name AS "orgName", i.invited_by AS "invitedBy"
282
+ FROM org_invitations i
283
+ INNER JOIN organizations o ON i.org_id = o.id
284
+ WHERE LOWER(i.email) = ? AND i.org_id = ? AND i.status = 'pending'`,
285
+ args: [ctx.email.toLowerCase(), scopedDelegatedOrgId],
286
+ })
287
+ : { rows: [] }
288
+ : await e.execute({
289
+ sql: `SELECT i.id AS id, i.org_id AS "orgId", o.name AS "orgName", i.invited_by AS "invitedBy"
290
+ FROM org_invitations i
291
+ INNER JOIN organizations o ON i.org_id = o.id
292
+ WHERE LOWER(i.email) = ? AND i.status = 'pending'`,
293
+ args: [ctx.email.toLowerCase()],
294
+ });
234
295
  const pendingInvitations = invitesRes.rows.map((r) => ({
235
296
  id: String(r.id),
236
297
  orgId: String(r.orgId ?? r.org_id),
237
298
  orgName: String(r.orgName ?? r.org_name),
238
299
  invitedBy: String(r.invitedBy ?? r.invited_by),
239
300
  }));
240
- scheduleFederatedOrgSync(event, { ...ctx, icon, iconRevision });
301
+ if (!delegatedOrgMember) {
302
+ scheduleFederatedOrgSync(event, { ...ctx, icon, iconRevision });
303
+ }
241
304
  return {
242
- email: ctx.email,
243
- orgId: ctx.orgId,
244
- orgName: ctx.orgName,
245
- role: ctx.role,
305
+ email: delegatedOrgMember ? (delegatedEmail ?? "") : ctx.email,
306
+ orgId: visibleOrgId,
307
+ orgName: visibleOrgName,
308
+ role: visibleRole,
246
309
  icon,
247
310
  iconRevision,
248
311
  emailConfigured: await isEmailConfigured(),
@@ -268,8 +331,13 @@ export const getMyOrgHandler = defineEventHandler(async (event) => {
268
331
  // so the value would sit in JSON any script on the page can read, and it
269
332
  // signs the JWTs peers accept as first-party callers. Reveal is an explicit
270
333
  // owner GET on /_agent-native/org/a2a-secret.
271
- a2aSecretSet: canManageOrgA2ASecret(ctx.role) ? a2aSecretSet : undefined,
272
- soloDeploymentAdmin: ctx.orgId ? undefined : isTrustedSelfHostedRuntime(),
334
+ a2aSecretSet: canManageOrgA2ASecret(visibleRole) ? a2aSecretSet : undefined,
335
+ a2aMemberDelegationEnabled: visibleRole === "owner" ? a2aMemberDelegationEnabled : undefined,
336
+ soloDeploymentAdmin: delegatedOrgMember
337
+ ? undefined
338
+ : visibleOrgId
339
+ ? undefined
340
+ : isTrustedSelfHostedRuntime(),
273
341
  };
274
342
  });
275
343
  export const retryPendingFederatedRemovalHandler = defineEventHandler(async (event) => {
@@ -1558,6 +1626,33 @@ export const setRequiredAuthProviderHandler = defineEventHandler(async (event) =
1558
1626
  const result = await setRequiredAuthProvider(ctx.orgId, provider);
1559
1627
  return { provider, ...result };
1560
1628
  });
1629
+ export const setA2AMemberDelegationHandler = defineEventHandler(async (event) => {
1630
+ const ctx = await getOrgContext(event);
1631
+ if (!ctx.orgId) {
1632
+ throw createError({ statusCode: 400, message: "No active organization" });
1633
+ }
1634
+ if (ctx.role !== "owner") {
1635
+ throw createError({
1636
+ statusCode: 403,
1637
+ message: "Only the organization owner can manage member delegation",
1638
+ });
1639
+ }
1640
+ const body = await readBody(event);
1641
+ if (typeof body?.enabled !== "boolean") {
1642
+ throw createError({
1643
+ statusCode: 400,
1644
+ message: "enabled must be a boolean",
1645
+ });
1646
+ }
1647
+ const e = await exec();
1648
+ await e.execute({
1649
+ sql: `UPDATE organizations
1650
+ SET a2a_member_delegation_enabled = ?
1651
+ WHERE id = ?`,
1652
+ args: [body.enabled, ctx.orgId],
1653
+ });
1654
+ return { enabled: body.enabled };
1655
+ });
1561
1656
  export const revealA2ASecretHandler = defineEventHandler(async (event) => {
1562
1657
  const ctx = await getOrgContext(event);
1563
1658
  if (!ctx.orgId) {
@@ -1625,6 +1720,7 @@ export const setA2ASecretHandler = defineEventHandler(async (event) => {
1625
1720
  sql: `UPDATE organizations SET a2a_secret = ? WHERE id = ?`,
1626
1721
  args: [secret, ctx.orgId],
1627
1722
  });
1723
+ invalidateMemberOrgCaches();
1628
1724
  return { a2aSecret: secret, previousSecret };
1629
1725
  });
1630
1726
  /**
@@ -1885,6 +1981,7 @@ export const receiveA2ASecretHandler = defineEventHandler(async (event) => {
1885
1981
  verified = null;
1886
1982
  }
1887
1983
  if (!verified ||
1984
+ verified.identityAssurance !== "organization" ||
1888
1985
  verified.orgId !== localOrgId ||
1889
1986
  verified.orgDomain?.trim().toLowerCase() !== orgDomain) {
1890
1987
  throw createError({
@@ -1904,5 +2001,6 @@ export const receiveA2ASecretHandler = defineEventHandler(async (event) => {
1904
2001
  sql: `UPDATE organizations SET a2a_secret = ? WHERE id = ?`,
1905
2002
  args: [newSecret, localOrgId],
1906
2003
  });
2004
+ invalidateMemberOrgCaches();
1907
2005
  return { ok: true, orgId: localOrgId };
1908
2006
  });
@@ -159,13 +159,14 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
159
159
  workspaceAppDefaultVisibility: import("./types.js").WorkspaceAppDefaultVisibility;
160
160
  signInMethods: import("./sign-in-methods.js").OrgSignInMethods;
161
161
  a2aSecretSet: boolean;
162
+ a2aMemberDelegationEnabled: boolean;
162
163
  soloDeploymentAdmin: boolean;
163
164
  }>>;
164
165
  export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
165
166
  success: boolean;
167
+ id?: undefined;
166
168
  name?: undefined;
167
169
  role?: undefined;
168
- id?: undefined;
169
170
  } | {
170
171
  success?: undefined;
171
172
  id: string;
@@ -1,10 +1,12 @@
1
+ import type { H3Event } from "h3";
1
2
  export declare function isMissingOrganizationTableError(error: unknown): boolean;
2
3
  export declare function isOrgMember(orgId: string, email: string, options?: {
3
4
  requireOrganizationMetadata?: boolean;
5
+ requestEvent?: H3Event;
4
6
  }): Promise<boolean>;
5
7
  /**
6
- * Checks membership for an A2A identity authenticated by an organization
7
- * secret. Unlike legacy callers, this requires readable organization metadata
8
- * as well as the membership row before the token subject can become an owner.
8
+ * Checks the live roster for an opted-in org-member A2A assertion. This proves
9
+ * membership in the asserted org; authorization still caps the identity at
10
+ * member regardless of the stored owner/admin role.
9
11
  */
10
- export declare function isOrgMemberForA2A(orgId: string, email: string): Promise<boolean>;
12
+ export declare function isOrgMemberForA2A(orgId: string, email: string, requestEvent?: H3Event): Promise<boolean>;
@@ -65,23 +65,25 @@ async function isOrgMemberWithPolicy(orgId, email, options) {
65
65
  const validation = await validateFederatedOrganizationMembershipForCurrentRequest({
66
66
  orgId,
67
67
  email: normalized,
68
- });
68
+ }, options.requestEvent);
69
69
  return validation.active;
70
70
  }
71
71
  export function isOrgMember(orgId, email, options = {}) {
72
72
  return isOrgMemberWithPolicy(orgId, email, {
73
73
  allowMissingOrganizationTable: !options.requireOrganizationMetadata,
74
74
  requireOrganizationRecord: !!options.requireOrganizationMetadata,
75
+ requestEvent: options.requestEvent,
75
76
  });
76
77
  }
77
78
  /**
78
- * Checks membership for an A2A identity authenticated by an organization
79
- * secret. Unlike legacy callers, this requires readable organization metadata
80
- * as well as the membership row before the token subject can become an owner.
79
+ * Checks the live roster for an opted-in org-member A2A assertion. This proves
80
+ * membership in the asserted org; authorization still caps the identity at
81
+ * member regardless of the stored owner/admin role.
81
82
  */
82
- export function isOrgMemberForA2A(orgId, email) {
83
+ export function isOrgMemberForA2A(orgId, email, requestEvent) {
83
84
  return isOrgMemberWithPolicy(orgId, email, {
84
85
  allowMissingOrganizationTable: false,
85
86
  requireOrganizationRecord: true,
87
+ requestEvent,
86
88
  });
87
89
  }