@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/cli",
3
- "version": "1.52.1",
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.1",
43
- "@frontera-sdk/core": "1.52.1",
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.1",
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.capability
85
- ? `the signed-in person needs ${opts.capability} — ask an administrator for a role that grants it`
86
- : PERSON_FORBIDDEN_HINT,
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
  }
@@ -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
@@ -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}).`, {
@@ -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 the organization-key lane's refusals, keyed by the `detail`
117
- * discriminator the service attaches to a 403.
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 ORG_KEY_REFUSAL_HINTS: Record<string, string> = {
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 = ORG_KEY_REFUSAL_HINTS[detail]
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