@agent-native/dispatch 0.27.3 → 0.27.5

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.
@@ -0,0 +1,105 @@
1
+ import { verifyA2AToken } from "@agent-native/core/a2a";
2
+ import {
3
+ isOrgMember,
4
+ resolveOrgByDomain,
5
+ resolveOrgIdForEmail,
6
+ } from "@agent-native/core/org";
7
+ import type {
8
+ ActionRouteAuthAdapter,
9
+ ActionRouteResolvedCaller,
10
+ } from "@agent-native/core/server";
11
+
12
+ export const WORKSPACE_APPS_ACTION_PATH =
13
+ "/_agent-native/actions/list-workspace-apps";
14
+
15
+ function isWorkspaceAppsActionPath(event: any): boolean {
16
+ const rawUrl =
17
+ (typeof event?.context?._mountedPathname === "string" &&
18
+ event.context._mountedPathname) ||
19
+ (typeof event?.path === "string" && event.path) ||
20
+ (typeof event?.url?.pathname === "string" && event.url.pathname) ||
21
+ (typeof event?.node?.req?.url === "string" && event.node.req.url) ||
22
+ (typeof event?.req?.url === "string" && event.req.url) ||
23
+ "/";
24
+ const requestPath =
25
+ String(rawUrl).split("?", 1)[0].replace(/\/+$/, "") || "/";
26
+ const appBasePath = process.env.APP_BASE_PATH?.trim().replace(/\/+$/, "");
27
+ return (
28
+ requestPath === WORKSPACE_APPS_ACTION_PATH ||
29
+ Boolean(
30
+ appBasePath &&
31
+ `${appBasePath}${WORKSPACE_APPS_ACTION_PATH}` === requestPath,
32
+ )
33
+ );
34
+ }
35
+
36
+ function readAuthorizationHeader(event: any): string | undefined {
37
+ const headers = event?.node?.req?.headers ?? event?.req?.headers;
38
+ if (typeof headers?.get === "function") {
39
+ return headers.get("authorization") ?? undefined;
40
+ }
41
+ const value = headers?.authorization ?? headers?.Authorization;
42
+ return Array.isArray(value) ? value[0] : value;
43
+ }
44
+
45
+ /**
46
+ * The hosted workspace registry is a read-only action that accepts a caller
47
+ * JWT from Dispatch. Keep this resolver path-scoped so the shared secret never
48
+ * becomes a bearer credential for unrelated Dispatch actions.
49
+ */
50
+ export const workspaceAppActionRouteAuth: ActionRouteAuthAdapter = {
51
+ resolveCaller: async (event): Promise<ActionRouteResolvedCaller | null> => {
52
+ if (!isWorkspaceAppsActionPath(event)) return null;
53
+
54
+ const authorization = readAuthorizationHeader(event);
55
+ if (!authorization) return null;
56
+ if (!authorization.startsWith("Bearer ")) {
57
+ throw new Error("Invalid workspace registry authorization");
58
+ }
59
+
60
+ const token = authorization.slice("Bearer ".length).trim();
61
+ if (!token) throw new Error("Invalid workspace registry authorization");
62
+
63
+ const identity = await verifyA2AToken(token, event);
64
+ if (!identity.email) {
65
+ throw new Error("Invalid workspace registry authorization");
66
+ }
67
+
68
+ const orgDomain = identity.orgDomain?.trim().toLowerCase();
69
+ const claimedOrgId = identity.orgId?.trim();
70
+ let orgId: string | null;
71
+ if (claimedOrgId) {
72
+ // A signed org_id is authoritative when the sender had an org-scoped
73
+ // request but could not resolve a domain. If both claims exist, reject
74
+ // a mismatch rather than allowing either claim to widen scope.
75
+ if (orgDomain) {
76
+ const org = await resolveOrgByDomain(orgDomain);
77
+ if (org && org.orgId !== claimedOrgId) {
78
+ throw new Error("Invalid workspace registry authorization");
79
+ }
80
+ }
81
+ if (!(await isOrgMember(claimedOrgId, identity.email))) {
82
+ throw new Error("Invalid workspace registry authorization");
83
+ }
84
+ orgId = claimedOrgId;
85
+ } else if (orgDomain) {
86
+ // A verified domain claim identifies the caller's intended org. Resolve
87
+ // it locally instead of falling back to the receiver's active-org or
88
+ // first-membership selection, which can be wrong for multi-org users.
89
+ const org = await resolveOrgByDomain(orgDomain);
90
+ if (!org || !(await isOrgMember(org.orgId, identity.email))) {
91
+ throw new Error("Invalid workspace registry authorization");
92
+ }
93
+ orgId = org.orgId;
94
+ } else {
95
+ // Preserve compatibility with legacy tokens that predate org_domain.
96
+ orgId = await resolveOrgIdForEmail(identity.email);
97
+ }
98
+
99
+ return {
100
+ owner: identity.email,
101
+ anonymous: false,
102
+ orgId,
103
+ };
104
+ },
105
+ };
@@ -33,6 +33,10 @@ describe("Dispatch agent chat plugin", () => {
33
33
  "start-workspace-app-creation",
34
34
  ]),
35
35
  },
36
+ actionRoutePublicPaths: ["/_agent-native/actions/list-workspace-apps"],
37
+ actionRouteAuth: expect.objectContaining({
38
+ resolveCaller: expect.any(Function),
39
+ }),
36
40
  }),
37
41
  );
38
42
  });
@@ -2,6 +2,10 @@ import { getOrgContext } from "@agent-native/core/org";
2
2
  import { createAgentChatPlugin } from "@agent-native/core/server";
3
3
 
4
4
  import { dispatchActions } from "../../actions/index.js";
5
+ import {
6
+ workspaceAppActionRouteAuth,
7
+ WORKSPACE_APPS_ACTION_PATH,
8
+ } from "../lib/workspace-app-action-auth.js";
5
9
 
6
10
  const INITIAL_TOOL_NAMES = [
7
11
  "view-screen",
@@ -53,6 +57,8 @@ export default createAgentChatPlugin({
53
57
  // a build-time-generated `.generated/actions-registry.ts` (the latter is a
54
58
  // template-only construct that the Vite plugin emits next to actions/).
55
59
  actions: dispatchActions,
60
+ actionRouteAuth: workspaceAppActionRouteAuth,
61
+ actionRoutePublicPaths: [WORKSPACE_APPS_ACTION_PATH],
56
62
  codeExecution: { production: "sandboxed" },
57
63
  systemPrompt: `You are the central dispatch for this workspace.
58
64