@agent-native/dispatch 0.40.24-nightly-20261015153252 → 0.40.24

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/CHANGELOG.md CHANGED
@@ -1,24 +1,22 @@
1
1
  # @agent-native/dispatch
2
2
 
3
- ## 0.40.24-nightly-20261015153252
3
+ ## 0.40.24
4
4
 
5
5
  ### Patch Changes
6
6
 
7
7
  - 2743662: Use the shared app permission policy for organization owner and admin access to Dispatch administration. Dispatch now requires `@agent-native/core` 0.206.0 or later, the first release with that policy.
8
- - Updated dependencies [eb4cd30]
9
- - Updated dependencies [ebf1cc7]
8
+ - Release all public npm packages with a patch version bump.
9
+ - 517fade: Say why a workspace app answers 503 "Workspace app access is temporarily unavailable". The 503 body now carries a `code` (and, when a Dispatch registry reply arrived, `upstreamStatus`, `audience`, and `detail`), and each cause is logged once per instance. A new `checkWorkspaceAppAccess` returns the cause; `isWorkspaceAppAccessAllowed` keeps its `true` / `false` / `"unavailable"` contract. Dispatch logs why it refused a registry token (server-side only; the 401 stays a fixed refusal), using the new `diagnoseA2ATokenFailure` from `@agent-native/core/a2a`.
10
10
  - Updated dependencies [e90b90a]
11
11
  - Updated dependencies [5b201d1]
12
- - Updated dependencies [db3a30e]
13
12
  - Updated dependencies [ce01559]
14
- - Updated dependencies [b95626f]
15
- - Updated dependencies [64bf791]
13
+ - Updated dependencies
14
+ - Updated dependencies [c139e35]
15
+ - Updated dependencies [c139e35]
16
16
  - Updated dependencies [e5a64ae]
17
- - Updated dependencies [ef6a530]
18
17
  - Updated dependencies [e6e1f56]
19
- - Updated dependencies [4e49b3a]
20
- - @agent-native/core@0.209.1-nightly-20261015153252
21
- - @agent-native/toolkit@0.209.1-nightly-20261015153252
18
+ - Updated dependencies [73da788]
19
+ - @agent-native/toolkit@0.209.1
22
20
 
23
21
  ## 0.40.23
24
22
 
@@ -1273,11 +1271,4 @@
1273
1271
  - 436340b: Share the canonical localized authentication copy with native sign-in surfaces
1274
1272
  and allow authenticated packaged callers to mint workspace embed sessions.
1275
1273
 
1276
- ## 0.27.17
1277
-
1278
- ### Patch Changes
1279
-
1280
- - Updated dependencies [95ea873]
1281
- - @agent-native/toolkit@0.16.4
1282
-
1283
1274
  For the full list of releases, see the [changelog archive](./changelog/archive/CHANGELOG.md).
@@ -47,10 +47,10 @@ declare const _default: import("node_modules/@agent-native/core/dist").ActionDef
47
47
  notes?: string | undefined;
48
48
  scope?: "org" | "user" | undefined;
49
49
  }, {
50
- message?: undefined;
51
50
  id?: undefined;
52
51
  provider?: undefined;
53
52
  deleted?: undefined;
53
+ message?: undefined;
54
54
  providers: {
55
55
  id: string;
56
56
  label: string;
@@ -65,9 +65,9 @@ declare const _default: import("node_modules/@agent-native/core/dist").ActionDef
65
65
  registered?: undefined;
66
66
  label?: undefined;
67
67
  } | {
68
- message?: undefined;
69
68
  id?: undefined;
70
69
  deleted?: undefined;
70
+ message?: undefined;
71
71
  providers?: undefined;
72
72
  count?: undefined;
73
73
  found: boolean;
@@ -75,9 +75,9 @@ declare const _default: import("node_modules/@agent-native/core/dist").ActionDef
75
75
  registered?: undefined;
76
76
  label?: undefined;
77
77
  } | {
78
- message?: undefined;
79
78
  provider?: undefined;
80
79
  deleted?: undefined;
80
+ message?: undefined;
81
81
  providers?: undefined;
82
82
  count?: undefined;
83
83
  found: boolean;
@@ -85,8 +85,8 @@ declare const _default: import("node_modules/@agent-native/core/dist").ActionDef
85
85
  registered?: undefined;
86
86
  label?: undefined;
87
87
  } | {
88
- message?: undefined;
89
88
  provider?: undefined;
89
+ message?: undefined;
90
90
  providers?: undefined;
91
91
  count?: undefined;
92
92
  found?: undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-app-action-auth.d.ts","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-action-auth.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,2BAA2B,CAAC;AAGnC,eAAO,MAAM,0BAA0B,+CACO,CAAC;AAC/C,eAAO,MAAM,+BAA+B,4DACe,CAAC;AA8B5D;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,sBA6DzC,CAAC"}
1
+ {"version":3,"file":"workspace-app-action-auth.d.ts","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-action-auth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,2BAA2B,CAAC;AAGnC,eAAO,MAAM,0BAA0B,+CACO,CAAC;AAC/C,eAAO,MAAM,+BAA+B,4DACe,CAAC;AAgD5D;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,EAAE,sBAuEzC,CAAC"}
@@ -1,8 +1,23 @@
1
- import { verifyA2AToken } from "@agent-native/core/a2a";
1
+ import { diagnoseA2ATokenFailure, verifyA2AToken, } from "@agent-native/core/a2a";
2
2
  import { isOrgMember, resolveOrgByDomain, resolveOrgIdForEmail, } from "@agent-native/core/org";
3
3
  import { getSession } from "@agent-native/core/server";
4
4
  export const WORKSPACE_APPS_ACTION_PATH = "/_agent-native/actions/list-workspace-apps";
5
5
  export const WORKSPACE_APP_CLAIM_ACTION_PATH = "/_agent-native/actions/claim-workspace-app-organization";
6
+ const REGISTRY_AUTH_REFUSED = "Invalid workspace registry authorization";
7
+ // The registry is re-asked on every request of every guarded app, so a single
8
+ // misconfigured workspace would otherwise log the same refusal per request.
9
+ const reportedRefusals = new Set();
10
+ // The caller always gets the same bare 401, whatever refused it: any finer
11
+ // answer would tell an unauthenticated caller whether a guessed secret was
12
+ // right. The reason and cause exist for the server log only.
13
+ function refuseRegistryCaller(reason, cause) {
14
+ const key = cause ? `${reason}:${cause}` : reason;
15
+ if (!reportedRefusals.has(key)) {
16
+ reportedRefusals.add(key);
17
+ console.warn("[workspace-registry-auth] refused caller", { reason, cause });
18
+ }
19
+ throw new Error(REGISTRY_AUTH_REFUSED);
20
+ }
6
21
  function isWorkspaceAppsActionPath(event) {
7
22
  const rawUrl = (typeof event?.context?._mountedPathname === "string" &&
8
23
  event.context._mountedPathname) ||
@@ -37,7 +52,7 @@ export const workspaceAppActionRouteAuth = {
37
52
  if (!authorization)
38
53
  return null;
39
54
  if (!authorization.startsWith("Bearer ")) {
40
- throw new Error("Invalid workspace registry authorization");
55
+ refuseRegistryCaller("malformed-authorization");
41
56
  }
42
57
  const hasRequestUrl = typeof event?.node?.req?.url === "string" ||
43
58
  typeof event?.req?.url === "string" ||
@@ -52,10 +67,16 @@ export const workspaceAppActionRouteAuth = {
52
67
  }
53
68
  const token = authorization.slice("Bearer ".length).trim();
54
69
  if (!token)
55
- throw new Error("Invalid workspace registry authorization");
70
+ refuseRegistryCaller("malformed-authorization");
56
71
  const identity = await verifyA2AToken(token, event);
57
72
  if (!identity.email) {
58
- throw new Error("Invalid workspace registry authorization");
73
+ // An organization principal is a token that verified but may not name a
74
+ // user: its org secret equals the deploy secret, or only the org secret
75
+ // verified it. Anything else failed outright.
76
+ if (identity.identityAssurance === "organization") {
77
+ refuseRegistryCaller("token-without-user");
78
+ }
79
+ refuseRegistryCaller("token-not-verified", await diagnoseA2ATokenFailure(token, event));
59
80
  }
60
81
  const orgDomain = identity.orgDomain?.trim().toLowerCase();
61
82
  const claimedOrgId = identity.orgId?.trim();
@@ -64,18 +85,20 @@ export const workspaceAppActionRouteAuth = {
64
85
  if (orgDomain) {
65
86
  const org = await resolveOrgByDomain(orgDomain);
66
87
  if (org && org.orgId !== claimedOrgId) {
67
- throw new Error("Invalid workspace registry authorization");
88
+ refuseRegistryCaller("org-mismatch");
68
89
  }
69
90
  }
70
91
  if (!(await isOrgMember(claimedOrgId, identity.email))) {
71
- throw new Error("Invalid workspace registry authorization");
92
+ refuseRegistryCaller("not-org-member");
72
93
  }
73
94
  orgId = claimedOrgId;
74
95
  }
75
96
  else if (orgDomain) {
76
97
  const org = await resolveOrgByDomain(orgDomain);
77
- if (!org || !(await isOrgMember(org.orgId, identity.email))) {
78
- throw new Error("Invalid workspace registry authorization");
98
+ if (!org)
99
+ refuseRegistryCaller("org-unknown");
100
+ if (!(await isOrgMember(org.orgId, identity.email))) {
101
+ refuseRegistryCaller("not-org-member");
79
102
  }
80
103
  orgId = org.orgId;
81
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-app-action-auth.js","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-action-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAKhC,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAEvD,MAAM,CAAC,MAAM,0BAA0B,GACrC,4CAA4C,CAAC;AAC/C,MAAM,CAAC,MAAM,+BAA+B,GAC1C,yDAAyD,CAAC;AAE5D,SAAS,yBAAyB,CAAC,KAAU;IAC3C,MAAM,MAAM,GACV,CAAC,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,KAAK,QAAQ;QACnD,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC;QACjC,CAAC,OAAO,KAAK,EAAE,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC;QAC/C,CAAC,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC;QAChE,CAAC,OAAO,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QACjE,CAAC,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;QACtD,GAAG,CAAC;IACN,MAAM,WAAW,GACf,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC;IAC7D,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1E,OAAO,CAAC,0BAA0B,EAAE,+BAA+B,CAAC,CAAC,IAAI,CACvE,CAAC,UAAU,EAAE,EAAE,CACb,WAAW,KAAK,UAAU;QAC1B,OAAO,CAAC,WAAW,IAAI,GAAG,WAAW,GAAG,UAAU,EAAE,KAAK,WAAW,CAAC,CACxE,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAAC,KAAU;IACzC,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,IAAI,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC;IACjE,IAAI,OAAO,OAAO,EAAE,GAAG,KAAK,UAAU,EAAE,CAAC;QACvC,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,SAAS,CAAC;IACnD,CAAC;IACD,MAAM,KAAK,GAAG,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,aAAa,CAAC;IAC/D,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAA2B;IACjE,aAAa,EAAE,KAAK,EAAE,KAAK,EAA6C,EAAE;QACxE,IAAI,CAAC,yBAAyB,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEnD,MAAM,aAAa,GAAG,uBAAuB,CAAC,KAAK,CAAC,CAAC;QACrD,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC;QAChC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC9D,CAAC;QAED,MAAM,aAAa,GACjB,OAAO,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ;YACzC,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,KAAK,QAAQ;YACnC,OAAO,KAAK,EAAE,GAAG,KAAK,QAAQ,CAAC;QACjC,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,CAAC,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC/D,IAAI,OAAO,EAAE,KAAK,EAAE,CAAC;YACnB,OAAO;gBACL,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,SAAS,EAAE,KAAK;gBAChB,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnD,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3D,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAExE,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACpD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;QAC9D,CAAC;QAED,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3D,MAAM,YAAY,GAAG,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC;QAC5C,IAAI,KAAoB,CAAC;QACzB,IAAI,YAAY,EAAE,CAAC;YACjB,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,GAAG,GAAG,MAAM,kBAAkB,CAAC,SAAS,CAAC,CAAC;gBAChD,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,KAAK,YAAY,EAAE,CAAC;oBACtC,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;gBAC9D,CAAC;YACH,CAAC;YACD,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBACvD,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;YAC9D,CAAC;YACD,KAAK,GAAG,YAAY,CAAC;QACvB,CAAC;aAAM,IAAI,SAAS,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,MAAM,kBAAkB,CAAC,SAAS,CAAC,CAAC;YAChD,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC5D,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;YAC9D,CAAC;YACD,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;QACpB,CAAC;aAAM,CAAC;YACN,KAAK,GAAG,MAAM,oBAAoB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrD,CAAC;QAED,OAAO;YACL,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,SAAS,EAAE,KAAK;YAChB,KAAK;SACN,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { verifyA2AToken } from \"@agent-native/core/a2a\";\nimport {\n isOrgMember,\n resolveOrgByDomain,\n resolveOrgIdForEmail,\n} from \"@agent-native/core/org\";\nimport type {\n ActionRouteAuthAdapter,\n ActionRouteResolvedCaller,\n} from \"@agent-native/core/server\";\nimport { getSession } from \"@agent-native/core/server\";\n\nexport const WORKSPACE_APPS_ACTION_PATH =\n \"/_agent-native/actions/list-workspace-apps\";\nexport const WORKSPACE_APP_CLAIM_ACTION_PATH =\n \"/_agent-native/actions/claim-workspace-app-organization\";\n\nfunction isWorkspaceAppsActionPath(event: any): boolean {\n const rawUrl =\n (typeof event?.context?._mountedPathname === \"string\" &&\n event.context._mountedPathname) ||\n (typeof event?.path === \"string\" && event.path) ||\n (typeof event?.url?.pathname === \"string\" && event.url.pathname) ||\n (typeof event?.node?.req?.url === \"string\" && event.node.req.url) ||\n (typeof event?.req?.url === \"string\" && event.req.url) ||\n \"/\";\n const requestPath =\n String(rawUrl).split(\"?\", 1)[0].replace(/\\/+$/, \"\") || \"/\";\n const appBasePath = process.env.APP_BASE_PATH?.trim().replace(/\\/+$/, \"\");\n return [WORKSPACE_APPS_ACTION_PATH, WORKSPACE_APP_CLAIM_ACTION_PATH].some(\n (actionPath) =>\n requestPath === actionPath ||\n Boolean(appBasePath && `${appBasePath}${actionPath}` === requestPath),\n );\n}\n\nfunction readAuthorizationHeader(event: any): string | undefined {\n const headers = event?.node?.req?.headers ?? event?.req?.headers;\n if (typeof headers?.get === \"function\") {\n return headers.get(\"authorization\") ?? undefined;\n }\n const value = headers?.authorization ?? headers?.Authorization;\n return Array.isArray(value) ? value[0] : value;\n}\n\n/**\n * The hosted workspace registry actions accept a caller JWT from Dispatch.\n * Keep this resolver path-scoped so the shared secret never becomes a bearer\n * credential for unrelated Dispatch actions.\n */\nexport const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {\n resolveCaller: async (event): Promise<ActionRouteResolvedCaller | null> => {\n if (!isWorkspaceAppsActionPath(event)) return null;\n\n const authorization = readAuthorizationHeader(event);\n if (!authorization) return null;\n if (!authorization.startsWith(\"Bearer \")) {\n throw new Error(\"Invalid workspace registry authorization\");\n }\n\n const hasRequestUrl =\n typeof event?.node?.req?.url === \"string\" ||\n typeof event?.req?.url === \"string\" ||\n typeof event?.url === \"string\";\n const session = hasRequestUrl ? await getSession(event) : null;\n if (session?.email) {\n return {\n owner: session.email,\n anonymous: false,\n ...(session.orgId ? { orgId: session.orgId } : {}),\n };\n }\n\n const token = authorization.slice(\"Bearer \".length).trim();\n if (!token) throw new Error(\"Invalid workspace registry authorization\");\n\n const identity = await verifyA2AToken(token, event);\n if (!identity.email) {\n throw new Error(\"Invalid workspace registry authorization\");\n }\n\n const orgDomain = identity.orgDomain?.trim().toLowerCase();\n const claimedOrgId = identity.orgId?.trim();\n let orgId: string | null;\n if (claimedOrgId) {\n if (orgDomain) {\n const org = await resolveOrgByDomain(orgDomain);\n if (org && org.orgId !== claimedOrgId) {\n throw new Error(\"Invalid workspace registry authorization\");\n }\n }\n if (!(await isOrgMember(claimedOrgId, identity.email))) {\n throw new Error(\"Invalid workspace registry authorization\");\n }\n orgId = claimedOrgId;\n } else if (orgDomain) {\n const org = await resolveOrgByDomain(orgDomain);\n if (!org || !(await isOrgMember(org.orgId, identity.email))) {\n throw new Error(\"Invalid workspace registry authorization\");\n }\n orgId = org.orgId;\n } else {\n orgId = await resolveOrgIdForEmail(identity.email);\n }\n\n return {\n owner: identity.email,\n anonymous: false,\n orgId,\n };\n },\n};\n"]}
1
+ {"version":3,"file":"workspace-app-action-auth.js","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-action-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n diagnoseA2ATokenFailure,\n verifyA2AToken,\n} from \"@agent-native/core/a2a\";\nimport {\n isOrgMember,\n resolveOrgByDomain,\n resolveOrgIdForEmail,\n} from \"@agent-native/core/org\";\nimport type {\n ActionRouteAuthAdapter,\n ActionRouteResolvedCaller,\n} from \"@agent-native/core/server\";\nimport { getSession } from \"@agent-native/core/server\";\n\nexport const WORKSPACE_APPS_ACTION_PATH =\n \"/_agent-native/actions/list-workspace-apps\";\nexport const WORKSPACE_APP_CLAIM_ACTION_PATH =\n \"/_agent-native/actions/claim-workspace-app-organization\";\n\nconst REGISTRY_AUTH_REFUSED = \"Invalid workspace registry authorization\";\n\n// The registry is re-asked on every request of every guarded app, so a single\n// misconfigured workspace would otherwise log the same refusal per request.\nconst reportedRefusals = new Set<string>();\n\n// The caller always gets the same bare 401, whatever refused it: any finer\n// answer would tell an unauthenticated caller whether a guessed secret was\n// right. The reason and cause exist for the server log only.\nfunction refuseRegistryCaller(reason: string, cause?: string): never {\n const key = cause ? `${reason}:${cause}` : reason;\n if (!reportedRefusals.has(key)) {\n reportedRefusals.add(key);\n console.warn(\"[workspace-registry-auth] refused caller\", { reason, cause });\n }\n throw new Error(REGISTRY_AUTH_REFUSED);\n}\n\nfunction isWorkspaceAppsActionPath(event: any): boolean {\n const rawUrl =\n (typeof event?.context?._mountedPathname === \"string\" &&\n event.context._mountedPathname) ||\n (typeof event?.path === \"string\" && event.path) ||\n (typeof event?.url?.pathname === \"string\" && event.url.pathname) ||\n (typeof event?.node?.req?.url === \"string\" && event.node.req.url) ||\n (typeof event?.req?.url === \"string\" && event.req.url) ||\n \"/\";\n const requestPath =\n String(rawUrl).split(\"?\", 1)[0].replace(/\\/+$/, \"\") || \"/\";\n const appBasePath = process.env.APP_BASE_PATH?.trim().replace(/\\/+$/, \"\");\n return [WORKSPACE_APPS_ACTION_PATH, WORKSPACE_APP_CLAIM_ACTION_PATH].some(\n (actionPath) =>\n requestPath === actionPath ||\n Boolean(appBasePath && `${appBasePath}${actionPath}` === requestPath),\n );\n}\n\nfunction readAuthorizationHeader(event: any): string | undefined {\n const headers = event?.node?.req?.headers ?? event?.req?.headers;\n if (typeof headers?.get === \"function\") {\n return headers.get(\"authorization\") ?? undefined;\n }\n const value = headers?.authorization ?? headers?.Authorization;\n return Array.isArray(value) ? value[0] : value;\n}\n\n/**\n * The hosted workspace registry actions accept a caller JWT from Dispatch.\n * Keep this resolver path-scoped so the shared secret never becomes a bearer\n * credential for unrelated Dispatch actions.\n */\nexport const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {\n resolveCaller: async (event): Promise<ActionRouteResolvedCaller | null> => {\n if (!isWorkspaceAppsActionPath(event)) return null;\n\n const authorization = readAuthorizationHeader(event);\n if (!authorization) return null;\n if (!authorization.startsWith(\"Bearer \")) {\n refuseRegistryCaller(\"malformed-authorization\");\n }\n\n const hasRequestUrl =\n typeof event?.node?.req?.url === \"string\" ||\n typeof event?.req?.url === \"string\" ||\n typeof event?.url === \"string\";\n const session = hasRequestUrl ? await getSession(event) : null;\n if (session?.email) {\n return {\n owner: session.email,\n anonymous: false,\n ...(session.orgId ? { orgId: session.orgId } : {}),\n };\n }\n\n const token = authorization.slice(\"Bearer \".length).trim();\n if (!token) refuseRegistryCaller(\"malformed-authorization\");\n\n const identity = await verifyA2AToken(token, event);\n if (!identity.email) {\n // An organization principal is a token that verified but may not name a\n // user: its org secret equals the deploy secret, or only the org secret\n // verified it. Anything else failed outright.\n if (identity.identityAssurance === \"organization\") {\n refuseRegistryCaller(\"token-without-user\");\n }\n refuseRegistryCaller(\n \"token-not-verified\",\n await diagnoseA2ATokenFailure(token, event),\n );\n }\n\n const orgDomain = identity.orgDomain?.trim().toLowerCase();\n const claimedOrgId = identity.orgId?.trim();\n let orgId: string | null;\n if (claimedOrgId) {\n if (orgDomain) {\n const org = await resolveOrgByDomain(orgDomain);\n if (org && org.orgId !== claimedOrgId) {\n refuseRegistryCaller(\"org-mismatch\");\n }\n }\n if (!(await isOrgMember(claimedOrgId, identity.email))) {\n refuseRegistryCaller(\"not-org-member\");\n }\n orgId = claimedOrgId;\n } else if (orgDomain) {\n const org = await resolveOrgByDomain(orgDomain);\n if (!org) refuseRegistryCaller(\"org-unknown\");\n if (!(await isOrgMember(org.orgId, identity.email))) {\n refuseRegistryCaller(\"not-org-member\");\n }\n orgId = org.orgId;\n } else {\n orgId = await resolveOrgIdForEmail(identity.email);\n }\n\n return {\n owner: identity.email,\n anonymous: false,\n orgId,\n };\n },\n};\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.40.24-nightly-20261015153252",
3
+ "version": "0.40.24",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -89,7 +89,7 @@
89
89
  "tailwind-merge": "^3.5.0",
90
90
  "vaul": "^1.1.2",
91
91
  "zod": "^4.3.6",
92
- "@agent-native/toolkit": "0.209.1-nightly-20261015153252"
92
+ "@agent-native/toolkit": "0.209.1"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@react-router/dev": "^8.1.0",
@@ -103,10 +103,10 @@
103
103
  "typescript-7": "npm:typescript@^7.0.2",
104
104
  "vite": "8.1.0",
105
105
  "vitest": "^4.1.5",
106
- "@agent-native/core": "0.209.1-nightly-20261015153252"
106
+ "@agent-native/core": "0.209.1"
107
107
  },
108
108
  "peerDependencies": {
109
- "@agent-native/core": "0.209.1-nightly-20261015153252",
109
+ "@agent-native/core": ">=0.209.1",
110
110
  "react": ">=19.2.7",
111
111
  "react-dom": ">=19.2.7",
112
112
  "react-router": ">=8"
@@ -1,6 +1,7 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  const mocks = vi.hoisted(() => ({
4
+ diagnoseA2ATokenFailure: vi.fn(),
4
5
  verifyA2AToken: vi.fn(),
5
6
  isOrgMember: vi.fn(),
6
7
  resolveOrgByDomain: vi.fn(),
@@ -8,6 +9,7 @@ const mocks = vi.hoisted(() => ({
8
9
  }));
9
10
 
10
11
  vi.mock("@agent-native/core/a2a", () => ({
12
+ diagnoseA2ATokenFailure: mocks.diagnoseA2ATokenFailure,
11
13
  verifyA2AToken: mocks.verifyA2AToken,
12
14
  }));
13
15
 
@@ -62,6 +64,133 @@ describe("workspace app action auth", () => {
62
64
  ).rejects.toThrow("Invalid workspace registry authorization");
63
65
  });
64
66
 
67
+ describe("refuses every bad registry bearer the same way and logs why", () => {
68
+ const REFUSED = "Invalid workspace registry authorization";
69
+ let warn: ReturnType<typeof vi.spyOn>;
70
+
71
+ beforeEach(() => {
72
+ warn = vi.spyOn(console, "warn").mockImplementation(() => {});
73
+ });
74
+
75
+ afterEach(() => {
76
+ warn.mockRestore();
77
+ });
78
+
79
+ async function refusalFor(authorization: string) {
80
+ return workspaceAppActionRouteAuth
81
+ .resolveCaller?.(eventFor(WORKSPACE_APPS_ACTION_PATH, authorization))
82
+ .then(
83
+ () => null,
84
+ (error: unknown) => error as Error,
85
+ );
86
+ }
87
+
88
+ // The caller sees one fixed refusal, so no answer tells an unauthenticated
89
+ // caller which credential check a guess got past.
90
+ function expectBareRefusal(refusal: Error | null | undefined) {
91
+ expect(refusal).toBeInstanceOf(Error);
92
+ expect(refusal?.message).toBe(REFUSED);
93
+ expect(Object.keys(refusal as Error)).toEqual([]);
94
+ }
95
+
96
+ function expectLogged(entry: { reason: string; cause?: string }) {
97
+ expect(warn).toHaveBeenCalledWith(
98
+ "[workspace-registry-auth] refused caller",
99
+ entry,
100
+ );
101
+ }
102
+
103
+ it("logs which check an unverified token failed, and tells the caller nothing more", async () => {
104
+ mocks.verifyA2AToken.mockResolvedValue({ email: null, orgDomain: null });
105
+ mocks.diagnoseA2ATokenFailure.mockResolvedValue("signature");
106
+
107
+ expectBareRefusal(await refusalFor("Bearer wrong-secret"));
108
+
109
+ expect(mocks.diagnoseA2ATokenFailure).toHaveBeenCalledWith(
110
+ "wrong-secret",
111
+ expect.anything(),
112
+ );
113
+ expectLogged({ reason: "token-not-verified", cause: "signature" });
114
+ });
115
+
116
+ it("logs a verified token that was demoted to an organization principal", async () => {
117
+ mocks.verifyA2AToken.mockResolvedValue({
118
+ email: null,
119
+ orgDomain: "builder.io",
120
+ orgId: "org-builder",
121
+ identityAssurance: "organization",
122
+ });
123
+
124
+ expectBareRefusal(await refusalFor("Bearer demoted"));
125
+
126
+ expect(mocks.diagnoseA2ATokenFailure).not.toHaveBeenCalled();
127
+ expectLogged({ reason: "token-without-user", cause: undefined });
128
+ });
129
+
130
+ it("logs an organization that does not exist here", async () => {
131
+ mocks.verifyA2AToken.mockResolvedValue({
132
+ email: "steve@builder.io",
133
+ orgDomain: "builder.io",
134
+ });
135
+ mocks.resolveOrgByDomain.mockResolvedValue(null);
136
+
137
+ expectBareRefusal(await refusalFor("Bearer verified"));
138
+
139
+ expectLogged({ reason: "org-unknown", cause: undefined });
140
+ });
141
+
142
+ it("logs a caller who is not a member of the organization", async () => {
143
+ mocks.verifyA2AToken.mockResolvedValue({
144
+ email: "steve@builder.io",
145
+ orgDomain: "builder.io",
146
+ });
147
+ mocks.resolveOrgByDomain.mockResolvedValue({
148
+ orgId: "org-builder",
149
+ orgName: "Builder.io",
150
+ });
151
+ mocks.isOrgMember.mockResolvedValue(false);
152
+
153
+ expectBareRefusal(await refusalFor("Bearer verified"));
154
+
155
+ expectLogged({ reason: "not-org-member", cause: undefined });
156
+ });
157
+
158
+ it("logs an organization claim that names a different organization", async () => {
159
+ mocks.verifyA2AToken.mockResolvedValue({
160
+ email: "steve@builder.io",
161
+ orgDomain: "builder.io",
162
+ orgId: "org-claimed",
163
+ });
164
+ mocks.resolveOrgByDomain.mockResolvedValue({
165
+ orgId: "org-local",
166
+ orgName: "Builder.io",
167
+ });
168
+
169
+ expectBareRefusal(await refusalFor("Bearer verified"));
170
+
171
+ expectLogged({ reason: "org-mismatch", cause: undefined });
172
+ });
173
+
174
+ it("logs a malformed authorization header", async () => {
175
+ expectBareRefusal(await refusalFor("Basic abc"));
176
+
177
+ expectLogged({ reason: "malformed-authorization", cause: undefined });
178
+ });
179
+
180
+ it("logs each distinct refusal once, not once per request", async () => {
181
+ mocks.verifyA2AToken.mockResolvedValue({ email: null, orgDomain: null });
182
+ mocks.diagnoseA2ATokenFailure.mockResolvedValue("expired");
183
+
184
+ await refusalFor("Bearer old-one");
185
+ await refusalFor("Bearer old-two");
186
+
187
+ const expired = warn.mock.calls.filter(
188
+ ([, entry]) => (entry as { cause?: string }).cause === "expired",
189
+ );
190
+ expect(expired).toHaveLength(1);
191
+ });
192
+ });
193
+
65
194
  it.each([WORKSPACE_APPS_ACTION_PATH, WORKSPACE_APP_CLAIM_ACTION_PATH])(
66
195
  "returns the verified caller and local org scope for %s",
67
196
  async (actionPath) => {
@@ -1,4 +1,7 @@
1
- import { verifyA2AToken } from "@agent-native/core/a2a";
1
+ import {
2
+ diagnoseA2ATokenFailure,
3
+ verifyA2AToken,
4
+ } from "@agent-native/core/a2a";
2
5
  import {
3
6
  isOrgMember,
4
7
  resolveOrgByDomain,
@@ -15,6 +18,24 @@ export const WORKSPACE_APPS_ACTION_PATH =
15
18
  export const WORKSPACE_APP_CLAIM_ACTION_PATH =
16
19
  "/_agent-native/actions/claim-workspace-app-organization";
17
20
 
21
+ const REGISTRY_AUTH_REFUSED = "Invalid workspace registry authorization";
22
+
23
+ // The registry is re-asked on every request of every guarded app, so a single
24
+ // misconfigured workspace would otherwise log the same refusal per request.
25
+ const reportedRefusals = new Set<string>();
26
+
27
+ // The caller always gets the same bare 401, whatever refused it: any finer
28
+ // answer would tell an unauthenticated caller whether a guessed secret was
29
+ // right. The reason and cause exist for the server log only.
30
+ function refuseRegistryCaller(reason: string, cause?: string): never {
31
+ const key = cause ? `${reason}:${cause}` : reason;
32
+ if (!reportedRefusals.has(key)) {
33
+ reportedRefusals.add(key);
34
+ console.warn("[workspace-registry-auth] refused caller", { reason, cause });
35
+ }
36
+ throw new Error(REGISTRY_AUTH_REFUSED);
37
+ }
38
+
18
39
  function isWorkspaceAppsActionPath(event: any): boolean {
19
40
  const rawUrl =
20
41
  (typeof event?.context?._mountedPathname === "string" &&
@@ -55,7 +76,7 @@ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
55
76
  const authorization = readAuthorizationHeader(event);
56
77
  if (!authorization) return null;
57
78
  if (!authorization.startsWith("Bearer ")) {
58
- throw new Error("Invalid workspace registry authorization");
79
+ refuseRegistryCaller("malformed-authorization");
59
80
  }
60
81
 
61
82
  const hasRequestUrl =
@@ -72,11 +93,20 @@ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
72
93
  }
73
94
 
74
95
  const token = authorization.slice("Bearer ".length).trim();
75
- if (!token) throw new Error("Invalid workspace registry authorization");
96
+ if (!token) refuseRegistryCaller("malformed-authorization");
76
97
 
77
98
  const identity = await verifyA2AToken(token, event);
78
99
  if (!identity.email) {
79
- throw new Error("Invalid workspace registry authorization");
100
+ // An organization principal is a token that verified but may not name a
101
+ // user: its org secret equals the deploy secret, or only the org secret
102
+ // verified it. Anything else failed outright.
103
+ if (identity.identityAssurance === "organization") {
104
+ refuseRegistryCaller("token-without-user");
105
+ }
106
+ refuseRegistryCaller(
107
+ "token-not-verified",
108
+ await diagnoseA2ATokenFailure(token, event),
109
+ );
80
110
  }
81
111
 
82
112
  const orgDomain = identity.orgDomain?.trim().toLowerCase();
@@ -86,17 +116,18 @@ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
86
116
  if (orgDomain) {
87
117
  const org = await resolveOrgByDomain(orgDomain);
88
118
  if (org && org.orgId !== claimedOrgId) {
89
- throw new Error("Invalid workspace registry authorization");
119
+ refuseRegistryCaller("org-mismatch");
90
120
  }
91
121
  }
92
122
  if (!(await isOrgMember(claimedOrgId, identity.email))) {
93
- throw new Error("Invalid workspace registry authorization");
123
+ refuseRegistryCaller("not-org-member");
94
124
  }
95
125
  orgId = claimedOrgId;
96
126
  } else if (orgDomain) {
97
127
  const org = await resolveOrgByDomain(orgDomain);
98
- if (!org || !(await isOrgMember(org.orgId, identity.email))) {
99
- throw new Error("Invalid workspace registry authorization");
128
+ if (!org) refuseRegistryCaller("org-unknown");
129
+ if (!(await isOrgMember(org.orgId, identity.email))) {
130
+ refuseRegistryCaller("not-org-member");
100
131
  }
101
132
  orgId = org.orgId;
102
133
  } else {