@frontera-sdk/cli 1.52.1 → 1.52.2
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 +4 -4
- package/src/api/blueprint-authoring-api.ts +1 -0
- package/src/api/credential-failure.ts +10 -4
- package/src/api/dataset-api.ts +1 -1
- package/src/api/governed-action-api.ts +3 -2
- package/src/api/media-api.ts +1 -1
- package/src/api/workflow-api.ts +1 -1
- package/src/exit.ts +8 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.52.
|
|
3
|
+
"version": "1.52.2",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/claude-agent-sdk": "^0.3.251",
|
|
42
|
-
"@frontera-sdk/functions": "1.52.
|
|
43
|
-
"@frontera-sdk/core": "1.52.
|
|
42
|
+
"@frontera-sdk/functions": "1.52.2",
|
|
43
|
+
"@frontera-sdk/core": "1.52.2",
|
|
44
44
|
"ai": "^6.0.116",
|
|
45
45
|
"gray-matter": "^4.0.3",
|
|
46
46
|
"yaml": "^2.9.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@frontera-sdk/forge-contracts": "1.52.
|
|
49
|
+
"@frontera-sdk/forge-contracts": "1.52.2",
|
|
50
50
|
"@types/bun": "^1.3.14",
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
}
|
|
@@ -70,6 +70,7 @@ export class BlueprintAuthoringApi {
|
|
|
70
70
|
const credential = credentialFailure(res.status, payload?.message, {
|
|
71
71
|
token: this.token,
|
|
72
72
|
code: payload?.code,
|
|
73
|
+
details: payload?.details,
|
|
73
74
|
whenSilent: "The key is not permitted to author this organization's Blueprint.",
|
|
74
75
|
})
|
|
75
76
|
if (credential) throw credential
|
|
@@ -28,6 +28,10 @@ import { orgScopedServiceNouns } from '../scopes'
|
|
|
28
28
|
export const PERSON_FORBIDDEN_HINT =
|
|
29
29
|
'the signed-in person lacks the permission this needs — ask an administrator for a role that grants it'
|
|
30
30
|
|
|
31
|
+
/** A session that named an organization the person does not belong to: no role helps, the selection does. */
|
|
32
|
+
export const ORGANIZATION_NOT_MEMBER_HINT =
|
|
33
|
+
'pass --org with the slug of an organization you belong to, or unset FRONTERA_ORG_ID'
|
|
34
|
+
|
|
31
35
|
/** The lane split, which is the first thing to check and the easiest to miss. */
|
|
32
36
|
function laneNote(token: string): string {
|
|
33
37
|
// A workspace key is refused organization scope BY DESIGN — it is not a
|
|
@@ -52,7 +56,7 @@ function laneNote(token: string): string {
|
|
|
52
56
|
export function credentialFailure(
|
|
53
57
|
status: number,
|
|
54
58
|
message: string | undefined,
|
|
55
|
-
opts: { token: string; code?: string; capability?: string; whenSilent?: string },
|
|
59
|
+
opts: { token: string; code?: string; details?: unknown; capability?: string; whenSilent?: string },
|
|
56
60
|
): CliError | null {
|
|
57
61
|
// A person's session, not a key: "the key may not carry the capability"
|
|
58
62
|
// sends its holder hunting for a credential they were never meant to use.
|
|
@@ -81,9 +85,11 @@ export function credentialFailure(
|
|
|
81
85
|
message ?? 'The signed-in person is not permitted to reach that resource.',
|
|
82
86
|
{
|
|
83
87
|
code: 'FORBIDDEN',
|
|
84
|
-
hint: opts.
|
|
85
|
-
?
|
|
86
|
-
:
|
|
88
|
+
hint: (opts.details as { detail?: unknown } | null | undefined)?.detail === 'organization-not-member'
|
|
89
|
+
? ORGANIZATION_NOT_MEMBER_HINT
|
|
90
|
+
: opts.capability
|
|
91
|
+
? `the signed-in person needs ${opts.capability} — ask an administrator for a role that grants it`
|
|
92
|
+
: PERSON_FORBIDDEN_HINT,
|
|
87
93
|
},
|
|
88
94
|
)
|
|
89
95
|
}
|
package/src/api/dataset-api.ts
CHANGED
|
@@ -106,7 +106,7 @@ export class DatasetApi {
|
|
|
106
106
|
// Datasets and sources are organization-scoped, so a workspace key is
|
|
107
107
|
// refused here by design — and this client used to answer that with a
|
|
108
108
|
// bare "Insufficient permissions" and no route out.
|
|
109
|
-
const credential = credentialFailure(response.status, payload?.message, { token: this.token, code: payload?.code })
|
|
109
|
+
const credential = credentialFailure(response.status, payload?.message, { token: this.token, code: payload?.code, details: payload?.details })
|
|
110
110
|
if (credential) throw credential
|
|
111
111
|
// `details` is TypeBox's own error array. Stringified it was unreadable,
|
|
112
112
|
// and this is the last gate before a Source exists.
|
|
@@ -94,7 +94,7 @@ export class GovernedActionApi {
|
|
|
94
94
|
|
|
95
95
|
if (!response.ok) {
|
|
96
96
|
const body = payload as { message?: string; code?: string; details?: unknown } | null
|
|
97
|
-
const credential = credentialFailure(response.status, body?.message, { token: this.token, code: body?.code })
|
|
97
|
+
const credential = credentialFailure(response.status, body?.message, { token: this.token, code: body?.code, details: body?.details })
|
|
98
98
|
if (credential) throw credential
|
|
99
99
|
throw new CliError(body?.message ?? `${response.status} from ${path}`, {
|
|
100
100
|
code: body?.code ?? 'FAILURE',
|
|
@@ -145,12 +145,13 @@ export class GovernedActionApi {
|
|
|
145
145
|
try { payload = text ? JSON.parse(text) : null } catch { payload = null }
|
|
146
146
|
|
|
147
147
|
if (!response.ok) {
|
|
148
|
-
const body = payload as { message?: string; code?: string } | null
|
|
148
|
+
const body = payload as { message?: string; code?: string; details?: unknown } | null
|
|
149
149
|
// The capability is named here because this route knows which one it
|
|
150
150
|
// needs; the shared mapper adds the lane note the bare hint was missing.
|
|
151
151
|
const credential = credentialFailure(response.status, body?.message, {
|
|
152
152
|
token: this.token,
|
|
153
153
|
code: body?.code,
|
|
154
|
+
details: body?.details,
|
|
154
155
|
capability: 'actionRequest:read',
|
|
155
156
|
})
|
|
156
157
|
if (credential) throw credential
|
package/src/api/media-api.ts
CHANGED
|
@@ -96,7 +96,7 @@ export class MediaApi {
|
|
|
96
96
|
try { payload = text ? JSON.parse(text) : null } catch { payload = null }
|
|
97
97
|
|
|
98
98
|
if (!response.ok) {
|
|
99
|
-
const credential = credentialFailure(response.status, payload?.message, { token: this.token, code: payload?.code })
|
|
99
|
+
const credential = credentialFailure(response.status, payload?.message, { token: this.token, code: payload?.code, details: payload?.details })
|
|
100
100
|
if (credential) throw credential
|
|
101
101
|
const detail = formatValidationDetails(payload?.details)
|
|
102
102
|
throw new CliError(payload?.message ?? `Request failed (${response.status}).`, {
|
package/src/api/workflow-api.ts
CHANGED
|
@@ -178,7 +178,7 @@ export class WorkflowApi {
|
|
|
178
178
|
+ 'credential runs as is still open, so no capability on an organization key opens this.',
|
|
179
179
|
})
|
|
180
180
|
}
|
|
181
|
-
const credential = credentialFailure(response.status, body?.message, { token: this.token, code: body?.code })
|
|
181
|
+
const credential = credentialFailure(response.status, body?.message, { token: this.token, code: body?.code, details: body?.details })
|
|
182
182
|
if (credential) throw credential
|
|
183
183
|
// A rejected definition carries its reasons in `details.errors`, and they
|
|
184
184
|
// are the whole answer — a caller told only "Invalid workflow definition"
|
package/src/exit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PERSON_FORBIDDEN_HINT } from './api/credential-failure'
|
|
1
|
+
import { ORGANIZATION_NOT_MEMBER_HINT, PERSON_FORBIDDEN_HINT } from './api/credential-failure'
|
|
2
2
|
import { CliError, UsageError } from './errors'
|
|
3
3
|
import { commandLabel, commandScope } from './scopes'
|
|
4
4
|
|
|
@@ -113,8 +113,9 @@ const SERVICE_HINTS: Record<string, string> = {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Hints for
|
|
117
|
-
*
|
|
116
|
+
* Hints for refusals keyed by the `detail` discriminator the service attaches
|
|
117
|
+
* to a 403: a session naming an organization the person is not in, and the
|
|
118
|
+
* organization-key lane's refusals.
|
|
118
119
|
*
|
|
119
120
|
* These exist because the FORBIDDEN hint above is wrong for them, not merely
|
|
120
121
|
* vague. It sends the operator to an admin, and for a lane refusal there is no
|
|
@@ -129,7 +130,8 @@ const SERVICE_HINTS: Record<string, string> = {
|
|
|
129
130
|
* fall through to the FORBIDDEN hint untouched rather than acquire one of its
|
|
130
131
|
* own.
|
|
131
132
|
*/
|
|
132
|
-
const
|
|
133
|
+
const REFUSAL_HINTS: Record<string, string> = {
|
|
134
|
+
'organization-not-member': ORGANIZATION_NOT_MEMBER_HINT,
|
|
133
135
|
'org-key-route-closed':
|
|
134
136
|
'this route is not open to organization keys — use a session login or a workspace key, '
|
|
135
137
|
+ 'or open the lane (docs/specs/2026-08-25-org-key-lane-coverage-spec.md)',
|
|
@@ -181,7 +183,7 @@ function refusalDetail(details: unknown): string | undefined {
|
|
|
181
183
|
function hintFor(code: string, details: unknown): string | undefined {
|
|
182
184
|
const detail = refusalDetail(details)
|
|
183
185
|
if (detail !== undefined) {
|
|
184
|
-
const specific =
|
|
186
|
+
const specific = REFUSAL_HINTS[detail]
|
|
185
187
|
if (specific) return specific
|
|
186
188
|
}
|
|
187
189
|
return SERVICE_HINTS[code]
|
|
@@ -328,6 +330,7 @@ export function sessionLaneFailure(
|
|
|
328
330
|
token: string,
|
|
329
331
|
err: unknown,
|
|
330
332
|
): CliError | null {
|
|
333
|
+
if (isFronteraError(err) && refusalDetail(err.details) === 'organization-not-member') return null
|
|
331
334
|
const failure = toEnvelope(err)
|
|
332
335
|
const hint = sessionLaneHint(meta, token, failure.code)
|
|
333
336
|
if (!hint) return null
|