@frontera-sdk/blueprint 1.50.84 → 1.51.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.
- package/package.json +2 -2
- package/src/action-client.ts +45 -0
- package/src/action-types.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/blueprint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.51.1",
|
|
4
4
|
"description": "React hooks for reading Blueprint data and invoking governed Actions from inside a Frontera app.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"smoke": "bun run scripts/smoke.ts"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@frontera-sdk/core": "1.
|
|
59
|
+
"@frontera-sdk/core": "1.51.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@tanstack/react-query": "^5.90.21",
|
package/src/action-client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FronteraClient } from '@frontera-sdk/core/client'
|
|
2
|
+
import { FronteraError } from '@frontera-sdk/core/errors'
|
|
2
3
|
import type {
|
|
3
4
|
ActionDescriptor,
|
|
4
5
|
ActionRequest,
|
|
@@ -8,6 +9,9 @@ import type {
|
|
|
8
9
|
|
|
9
10
|
const BASE = '/v1/blueprint/governed-actions'
|
|
10
11
|
|
|
12
|
+
/** The token of a person signed in to an externally hosted App. */
|
|
13
|
+
const APP_USER_TOKEN_PREFIX = 'sk-au-'
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* The governed write plane.
|
|
13
17
|
*
|
|
@@ -20,10 +24,36 @@ const BASE = '/v1/blueprint/governed-actions'
|
|
|
20
24
|
* A workspace key cannot invoke at all — its principal belongs to no
|
|
21
25
|
* organization member — which is why a scaffolded dev host, holding one, will
|
|
22
26
|
* read fine and refuse every write. See `README` on `frontera app init`.
|
|
27
|
+
*
|
|
28
|
+
* A person signed in to an EXTERNALLY HOSTED App is checked against the App's
|
|
29
|
+
* roles instead: `discover` lists the Actions their roles hold, and `request`
|
|
30
|
+
* reads a request they raised. `submit` is accepted only for records that a
|
|
31
|
+
* role listing that Action lets the person read. A role that only reads never
|
|
32
|
+
* widens what they can change. A referenced record need only be readable.
|
|
33
|
+
* `requests`, `decide` and `cancel` are for Frontera members and are refused
|
|
34
|
+
* here for such a person, before anything is sent.
|
|
23
35
|
*/
|
|
24
36
|
export class ActionClient {
|
|
25
37
|
constructor(private readonly client: FronteraClient) {}
|
|
26
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The refusal for a call only a Frontera member can make, when the session
|
|
41
|
+
* belongs to a person signed in to an externally hosted App.
|
|
42
|
+
*
|
|
43
|
+
* The service answers such a call as if no credential had been sent. Left to
|
|
44
|
+
* that, the App would see "Authentication required" and the session would
|
|
45
|
+
* sign the person in again over a call that can never succeed. Said here
|
|
46
|
+
* instead, by name.
|
|
47
|
+
*/
|
|
48
|
+
private membersOnly(what: string, instead: string): FronteraError | null {
|
|
49
|
+
const credential = this.client.config.credential
|
|
50
|
+
if (credential.kind !== 'token' || !credential.token.startsWith(APP_USER_TOKEN_PREFIX)) return null
|
|
51
|
+
return new FronteraError(
|
|
52
|
+
`${what} is not available to people signed in to an externally hosted App. ${instead}`,
|
|
53
|
+
{ code: 'FORBIDDEN', status: 403 },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
/**
|
|
28
58
|
* Actions this user may invoke, here, now.
|
|
29
59
|
*
|
|
@@ -67,6 +97,11 @@ export class ActionClient {
|
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
requests(lifecycle?: readonly ActionRequestLifecycle[]): Promise<ActionRequest[]> {
|
|
100
|
+
const refused = this.membersOnly(
|
|
101
|
+
'Listing Action requests',
|
|
102
|
+
'Keep the id `submit` returns and read that request with `request(id)` or `useActionRequest`.',
|
|
103
|
+
)
|
|
104
|
+
if (refused) return Promise.reject(refused)
|
|
70
105
|
return this.client.request<ActionRequest[]>(`${BASE}/requests`, {
|
|
71
106
|
query: lifecycle?.length ? { lifecycle: lifecycle.join(',') } : undefined,
|
|
72
107
|
})
|
|
@@ -82,6 +117,11 @@ export class ActionClient {
|
|
|
82
117
|
* from whoever submitted it.
|
|
83
118
|
*/
|
|
84
119
|
decide(requestId: string, decision: 'approve' | 'reject', reason: string): Promise<ActionRequest> {
|
|
120
|
+
const refused = this.membersOnly(
|
|
121
|
+
'Approving or rejecting an Action request',
|
|
122
|
+
'A Frontera member with approval permission decides it in Frontera.',
|
|
123
|
+
)
|
|
124
|
+
if (refused) return Promise.reject(refused)
|
|
85
125
|
return this.client.request<ActionRequest>(
|
|
86
126
|
`${BASE}/requests/${encodeURIComponent(requestId)}/approvals`,
|
|
87
127
|
{ method: 'POST', body: { decision, reason } },
|
|
@@ -94,6 +134,11 @@ export class ActionClient {
|
|
|
94
134
|
* rather than ignored, because the route accepts an exact key set.
|
|
95
135
|
*/
|
|
96
136
|
cancel(requestId: string): Promise<ActionRequest> {
|
|
137
|
+
const refused = this.membersOnly(
|
|
138
|
+
'Cancelling an Action request',
|
|
139
|
+
'A Frontera member with permission to cancel requests can cancel it in Frontera.',
|
|
140
|
+
)
|
|
141
|
+
if (refused) return Promise.reject(refused)
|
|
97
142
|
return this.client.request<ActionRequest>(
|
|
98
143
|
`${BASE}/requests/${encodeURIComponent(requestId)}/cancel`,
|
|
99
144
|
{ method: 'POST' },
|
package/src/action-types.ts
CHANGED
|
@@ -150,12 +150,26 @@ export interface ActionDescriptor {
|
|
|
150
150
|
* "raised by the Collections Agent, under Alia's authority" are different
|
|
151
151
|
* things to approve, and flattening them into one line hides the one an
|
|
152
152
|
* approver most needs to see.
|
|
153
|
+
*
|
|
154
|
+
* `applicationId` names the App a request was raised through: by the person
|
|
155
|
+
* directly, or by a Function they started from that App. It does not change
|
|
156
|
+
* `kind`. Absent from a platform that predates the field.
|
|
157
|
+
*
|
|
158
|
+
* `appUserId` and `appUserName` are set when the person is an App user: someone
|
|
159
|
+
* signed in to an externally hosted App, who is not a workspace member. `kind`
|
|
160
|
+
* is then `person`, `userId` is `null`, and `applicationId` is their App. Show
|
|
161
|
+
* `appUserName` when it is present; it can be `null` when the person's identity
|
|
162
|
+
* provider sent no name. Both are `null` for every other request, and absent
|
|
163
|
+
* from a platform that predates the fields.
|
|
153
164
|
*/
|
|
154
165
|
export interface ActionRequestRequester {
|
|
155
166
|
kind: 'agent' | 'person' | 'automation'
|
|
156
167
|
agentId: string | null
|
|
157
168
|
agentName: string | null
|
|
158
169
|
userId: string | null
|
|
170
|
+
applicationId?: string | null
|
|
171
|
+
appUserId?: string | null
|
|
172
|
+
appUserName?: string | null
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
/** How to read one submitted parameter value: its label, declared kind and whether it is withheld. */
|