@forumone/throughline-approvals 0.8.2 → 0.8.4
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,5 +1,73 @@
|
|
|
1
1
|
# @forumone/throughline-approvals
|
|
2
2
|
|
|
3
|
+
## 0.8.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d02772f: `get_approval_status` now refuses an unauthenticated caller, and answers only
|
|
8
|
+
the requester or a member of an approver group.
|
|
9
|
+
|
|
10
|
+
It had no authorization check of any kind. The handler was `async (input) =>` —
|
|
11
|
+
the signature did not accept `ctx`, so there was nothing to check against — and
|
|
12
|
+
it `findByID`d an arbitrary `approvalId` at the Local API default of
|
|
13
|
+
`overrideAccess: true`, returning the target, the requester, the approver
|
|
14
|
+
groups, the decision and the decision notes. The other four tools on this
|
|
15
|
+
server all check `ctx.user`, which made this the one approvals tool that still
|
|
16
|
+
answered when the rest correctly denied. Audit 04 F-20.
|
|
17
|
+
|
|
18
|
+
The rule is the union of what the two list tools already show a caller —
|
|
19
|
+
`list_my_requests` returns the approvals they requested, `list_pending_approvals`
|
|
20
|
+
the ones routed to their groups — so the by-id read agrees with the by-list
|
|
21
|
+
reads instead of being a way around them. No admin bypass, because
|
|
22
|
+
`respond_to_approval` has none either and a second policy on the same collection
|
|
23
|
+
is how the two drift.
|
|
24
|
+
|
|
25
|
+
A refused read returns `Approval not found`, the same sentence as a genuine
|
|
26
|
+
miss: ids are sequential, and a distinguishable refusal enumerates who is asking
|
|
27
|
+
whom to approve what.
|
|
28
|
+
|
|
29
|
+
**Behavioural change for consumers.** A `get_approval_status` call arriving with
|
|
30
|
+
no `ctx.user` now returns `{ error: 'Must be authenticated…' }` instead of the
|
|
31
|
+
approval. In practice that is every `Bearer`-authenticated MCP call, because
|
|
32
|
+
`@payloadcms/plugin-mcp` does not assign `req.user` — the same fact that made
|
|
33
|
+
the four gated tools deny and this one answer.
|
|
34
|
+
|
|
35
|
+
- Updated dependencies [d02772f]
|
|
36
|
+
- @forumone/throughline-core@0.9.1
|
|
37
|
+
- @forumone/throughline-publishing@0.9.4
|
|
38
|
+
|
|
39
|
+
## 0.8.3
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- 1f3346f: `get_approval_status` now refuses an unauthenticated caller, and answers only
|
|
44
|
+
the requester or a member of an approver group.
|
|
45
|
+
|
|
46
|
+
It had no authorization check of any kind. The handler was `async (input) =>` —
|
|
47
|
+
the signature did not accept `ctx`, so there was nothing to check against — and
|
|
48
|
+
it `findByID`d an arbitrary `approvalId` at the Local API default of
|
|
49
|
+
`overrideAccess: true`, returning the target, the requester, the approver
|
|
50
|
+
groups, the decision and the decision notes. The other four tools on this
|
|
51
|
+
server all check `ctx.user`, which made this the one approvals tool that still
|
|
52
|
+
answered when the rest correctly denied. Audit 04 F-20.
|
|
53
|
+
|
|
54
|
+
The rule is the union of what the two list tools already show a caller —
|
|
55
|
+
`list_my_requests` returns the approvals they requested, `list_pending_approvals`
|
|
56
|
+
the ones routed to their groups — so the by-id read agrees with the by-list
|
|
57
|
+
reads instead of being a way around them. No admin bypass, because
|
|
58
|
+
`respond_to_approval` has none either and a second policy on the same collection
|
|
59
|
+
is how the two drift.
|
|
60
|
+
|
|
61
|
+
A refused read returns `Approval not found`, the same sentence as a genuine
|
|
62
|
+
miss: ids are sequential, and a distinguishable refusal enumerates who is asking
|
|
63
|
+
whom to approve what.
|
|
64
|
+
|
|
65
|
+
**Behavioural change for consumers.** A `get_approval_status` call arriving with
|
|
66
|
+
no `ctx.user` now returns `{ error: 'Must be authenticated…' }` instead of the
|
|
67
|
+
approval. In practice that is every `Bearer`-authenticated MCP call, because
|
|
68
|
+
`@payloadcms/plugin-mcp` does not assign `req.user` — the same fact that made
|
|
69
|
+
the four gated tools deny and this one answer.
|
|
70
|
+
|
|
3
71
|
## 0.8.2
|
|
4
72
|
|
|
5
73
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-approval-status.d.ts","sourceRoot":"","sources":["../../src/tools/get-approval-status.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"get-approval-status.d.ts","sourceRoot":"","sources":["../../src/tools/get-approval-status.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,OAAO,KAAK,EAAkB,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAE9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAG3D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,sBAAsB,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAC1D;AAqDD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CA2C1F"}
|
|
@@ -2,6 +2,52 @@ import { z } from 'zod';
|
|
|
2
2
|
import { unwrapRelationshipId, withMeta } from '@forumone/throughline-core';
|
|
3
3
|
import { DEFAULT_APPROVALS_SLUG } from '../collection.js';
|
|
4
4
|
import { APPROVALS_TOOLS } from './descriptors.js';
|
|
5
|
+
/*
|
|
6
|
+
Who may read one approval by id.
|
|
7
|
+
|
|
8
|
+
Audit 04 F-20. This handler was `async (input) =>` — the signature did not
|
|
9
|
+
accept `ctx` at all, so there was nothing to check against and nothing was
|
|
10
|
+
checked. It `findByID`s an arbitrary `approvalId` at the Local API default of
|
|
11
|
+
`overrideAccess: true` and returns the target, the requester, the approver
|
|
12
|
+
groups, the decision and the decision notes.
|
|
13
|
+
|
|
14
|
+
The other four tools on this server all refuse an unauthenticated caller. This
|
|
15
|
+
was the one that still answered when they denied, and that asymmetry is not
|
|
16
|
+
theoretical: `@payloadcms/plugin-mcp` never assigns `req.user`, so for a
|
|
17
|
+
`Bearer`-authenticated MCP call `ctx.user` is null and the refusal below is the
|
|
18
|
+
branch that actually fires today.
|
|
19
|
+
|
|
20
|
+
## The rule is the union of the two list tools, and deliberately nothing more
|
|
21
|
+
|
|
22
|
+
`list_my_requests` shows a caller the approvals they requested.
|
|
23
|
+
`list_pending_approvals` shows a caller the approvals routed to one of their
|
|
24
|
+
groups. Between them they already disclose every field this tool returns. So
|
|
25
|
+
the rule here is *requester or approver-group member*, which makes the by-id
|
|
26
|
+
read agree with the by-list reads rather than being a way around them.
|
|
27
|
+
|
|
28
|
+
No admin bypass, because `respond_to_approval` has none either and a second
|
|
29
|
+
policy on the same collection is how the two drift. An admin who needs an
|
|
30
|
+
arbitrary approval has the admin UI and the REST collection, both of which
|
|
31
|
+
apply `approval-requests`' own access rules — which is the thing this server
|
|
32
|
+
should not be quietly wider than.
|
|
33
|
+
|
|
34
|
+
The order of the three checks is load-bearing. Identity first, because the
|
|
35
|
+
unauthenticated refusal must not depend on the id existing; then the document;
|
|
36
|
+
then membership.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Whether this caller is a party to this approval.
|
|
40
|
+
*
|
|
41
|
+
* Group membership is compared exactly as `respond_to_approval` compares it —
|
|
42
|
+
* `approverGroups` against `ctx.user.groups`, slug by slug — so a change in how
|
|
43
|
+
* groups are named breaks both together rather than leaving this one behind.
|
|
44
|
+
*/
|
|
45
|
+
function isPartyTo(approval, user) {
|
|
46
|
+
if (unwrapRelationshipId(approval['requestedBy']) === user.id)
|
|
47
|
+
return true;
|
|
48
|
+
const approverGroups = approval['approverGroups'] ?? [];
|
|
49
|
+
return approverGroups.some((group) => user.groups.includes(group));
|
|
50
|
+
}
|
|
5
51
|
export function createGetApprovalStatusTool(deps) {
|
|
6
52
|
const inputSchema = withMeta({
|
|
7
53
|
approvalId: z.string(),
|
|
@@ -9,13 +55,23 @@ export function createGetApprovalStatusTool(deps) {
|
|
|
9
55
|
return {
|
|
10
56
|
...APPROVALS_TOOLS.getApprovalStatus,
|
|
11
57
|
inputSchema,
|
|
12
|
-
handler: async (input) => {
|
|
58
|
+
handler: async (input, ctx) => {
|
|
59
|
+
if (!ctx.user) {
|
|
60
|
+
return { error: 'Must be authenticated to read an approval request' };
|
|
61
|
+
}
|
|
13
62
|
const approval = (await deps.payload.findByID({
|
|
14
63
|
collection: deps.options.collectionSlug ?? DEFAULT_APPROVALS_SLUG,
|
|
15
64
|
id: input.approvalId,
|
|
16
65
|
}));
|
|
17
66
|
if (!approval)
|
|
18
67
|
return { error: 'Approval not found' };
|
|
68
|
+
/*
|
|
69
|
+
The same sentence as "not found", and that is the point: a caller who is
|
|
70
|
+
not a party to this request learns nothing about whether the id names one.
|
|
71
|
+
Ids are sequential, so a distinguishable refusal enumerates the collection.
|
|
72
|
+
*/
|
|
73
|
+
if (!isPartyTo(approval, ctx.user))
|
|
74
|
+
return { error: 'Approval not found' };
|
|
19
75
|
return {
|
|
20
76
|
approvalId: String(approval['id']),
|
|
21
77
|
status: approval['status'],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-approval-status.js","sourceRoot":"","sources":["../../src/tools/get-approval-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAE3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAOlD,MAAM,UAAU,2BAA2B,CAAC,IAA2B;IACrE,MAAM,WAAW,GAAG,QAAQ,CAAC;QAC3B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;KACvB,CAAC,CAAA;IAEF,OAAO;QACL,GAAG,eAAe,CAAC,iBAAiB;QACpC,WAAW;QACX,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"get-approval-status.js","sourceRoot":"","sources":["../../src/tools/get-approval-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAE3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAOlD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCE;AAEF;;;;;;GAMG;AACH,SAAS,SAAS,CAChB,QAAiC,EACjC,IAAyC;IAEzC,IAAI,oBAAoB,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAA;IAE1E,MAAM,cAAc,GAAI,QAAQ,CAAC,gBAAgB,CAA0B,IAAI,EAAE,CAAA;IACjF,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAA;AACpE,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,IAA2B;IACrE,MAAM,WAAW,GAAG,QAAQ,CAAC;QAC3B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;KACvB,CAAC,CAAA;IAEF,OAAO;QACL,GAAG,eAAe,CAAC,iBAAiB;QACpC,WAAW;QACX,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YAC5B,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;gBACd,OAAO,EAAE,KAAK,EAAE,mDAAmD,EAAE,CAAA;YACvE,CAAC;YAED,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAC5C,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,sBAAsB;gBACjE,EAAE,EAAE,KAAK,CAAC,UAAU;aACrB,CAAC,CAAmC,CAAA;YACrC,IAAI,CAAC,QAAQ;gBAAE,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAA;YAErD;;;;cAIE;YACF,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC;gBAAE,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAA;YAE1E,OAAO;gBACL,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAClC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC;gBAC1B,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC;gBAC9C,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC;gBAC9B,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC;gBACpC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC;gBACxC,WAAW,EAAE,oBAAoB,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC;gBAC1D,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC;gBACpC,cAAc,EAAE,QAAQ,CAAC,gBAAgB,CAAC;gBAC1C,SAAS,EAAE,oBAAoB,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;gBACtD,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC;gBAChC,aAAa,EAAE,QAAQ,CAAC,eAAe,CAAC;gBACxC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC;aACjC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forumone/throughline-approvals",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Conversational approval workflow server for Throughline. Provides the approval resolver the publishing server consumes plus MCP tools for requesting and responding to approvals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"zod": "^3.23.0",
|
|
45
|
-
"@forumone/throughline-core": "0.9.
|
|
45
|
+
"@forumone/throughline-core": "0.9.1",
|
|
46
46
|
"@forumone/throughline-plugin-contract": "0.4.1",
|
|
47
|
-
"@forumone/throughline-publishing": "0.9.
|
|
47
|
+
"@forumone/throughline-publishing": "0.9.4"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^24.13.2",
|