@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 +8 -17
- package/dist/actions/provider-api-register.d.ts +4 -4
- package/dist/server/lib/workspace-app-action-auth.d.ts.map +1 -1
- package/dist/server/lib/workspace-app-action-auth.js +31 -8
- package/dist/server/lib/workspace-app-action-auth.js.map +1 -1
- package/package.json +4 -4
- package/src/server/lib/workspace-app-action-auth.spec.ts +130 -1
- package/src/server/lib/workspace-app-action-auth.ts +39 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
# @agent-native/dispatch
|
|
2
2
|
|
|
3
|
-
## 0.40.24
|
|
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
|
-
-
|
|
9
|
-
-
|
|
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
|
|
15
|
-
- Updated dependencies [
|
|
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 [
|
|
20
|
-
- @agent-native/
|
|
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":"
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
refuseRegistryCaller("malformed-authorization");
|
|
56
71
|
const identity = await verifyA2AToken(token, event);
|
|
57
72
|
if (!identity.email) {
|
|
58
|
-
|
|
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
|
-
|
|
88
|
+
refuseRegistryCaller("org-mismatch");
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
if (!(await isOrgMember(claimedOrgId, identity.email))) {
|
|
71
|
-
|
|
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
|
|
78
|
-
|
|
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,
|
|
1
|
+
{"version":3,"file":"workspace-app-action-auth.js","sourceRoot":"","sources":["../../../src/server/lib/workspace-app-action-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,cAAc,GACf,MAAM,wBAAwB,CAAC;AAChC,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,MAAM,qBAAqB,GAAG,0CAA0C,CAAC;AAEzE,8EAA8E;AAC9E,4EAA4E;AAC5E,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;AAE3C,2EAA2E;AAC3E,2EAA2E;AAC3E,6DAA6D;AAC7D,SAAS,oBAAoB,CAAC,MAAc,EAAE,KAAc;IAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IAClD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,0CAA0C,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;AACzC,CAAC;AAED,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,oBAAoB,CAAC,yBAAyB,CAAC,CAAC;QAClD,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,oBAAoB,CAAC,yBAAyB,CAAC,CAAC;QAE5D,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACpD,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;YACpB,wEAAwE;YACxE,wEAAwE;YACxE,8CAA8C;YAC9C,IAAI,QAAQ,CAAC,iBAAiB,KAAK,cAAc,EAAE,CAAC;gBAClD,oBAAoB,CAAC,oBAAoB,CAAC,CAAC;YAC7C,CAAC;YACD,oBAAoB,CAClB,oBAAoB,EACpB,MAAM,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,CAC5C,CAAC;QACJ,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,oBAAoB,CAAC,cAAc,CAAC,CAAC;gBACvC,CAAC;YACH,CAAC;YACD,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBACvD,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;YACzC,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;gBAAE,oBAAoB,CAAC,aAAa,CAAC,CAAC;YAC9C,IAAI,CAAC,CAAC,MAAM,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBACpD,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;YACzC,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 {\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
|
|
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
|
|
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
|
|
106
|
+
"@agent-native/core": "0.209.1"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
|
-
"@agent-native/core": "0.209.1
|
|
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 {
|
|
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
|
-
|
|
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)
|
|
96
|
+
if (!token) refuseRegistryCaller("malformed-authorization");
|
|
76
97
|
|
|
77
98
|
const identity = await verifyA2AToken(token, event);
|
|
78
99
|
if (!identity.email) {
|
|
79
|
-
|
|
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
|
-
|
|
119
|
+
refuseRegistryCaller("org-mismatch");
|
|
90
120
|
}
|
|
91
121
|
}
|
|
92
122
|
if (!(await isOrgMember(claimedOrgId, identity.email))) {
|
|
93
|
-
|
|
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
|
|
99
|
-
|
|
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 {
|