run402 2.24.0 → 2.25.0

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.
@@ -339,6 +339,49 @@ export function scanFileContent(content, opts = {}) {
339
339
  });
340
340
  }
341
341
 
342
+ // 8) Tenant-assertion session-mint call without the declared capability.
343
+ // `auth.sessions.createResponseFromTenantAssertion(...)` mints a browser
344
+ // session from a tenant's vouching. It works ONLY in a function whose
345
+ // deploy/apply spec declares `capabilities: ["auth.sessionMint"]`
346
+ // (FunctionSpec.capabilities — sibling to `config`, since the platform
347
+ // has no code-export metadata channel). Service-key presence is NOT
348
+ // sufficient. Without the capability the gateway returns
349
+ // R402_AUTH_UNTRUSTED_CONTEXT at runtime and mints no session.
350
+ //
351
+ // The pure file scanner can't see the per-function spec, so the caller
352
+ // threads `opts.declaredCapabilities` (the union of capabilities declared
353
+ // across run402.config.json function entries — see readDeclaredCapabilities).
354
+ // We suppress the finding when "auth.sessionMint" is present anywhere in
355
+ // that union. Global-union (not per-file) is a deliberate precision
356
+ // trade-off: the file→function-entry mapping isn't reliable from source,
357
+ // and the runtime gate catches the rare "function A declared it, function
358
+ // B forgot" case. WARN severity (never block deploy): an inline/SDK spec
359
+ // the doctor can't read might declare the capability.
360
+ const declaredCaps =
361
+ opts.declaredCapabilities instanceof Set
362
+ ? opts.declaredCapabilities
363
+ : new Set(Array.isArray(opts.declaredCapabilities) ? opts.declaredCapabilities : []);
364
+ if (!declaredCaps.has("auth.sessionMint")) {
365
+ const mintCallRegex = /\bcreateResponseFromTenantAssertion\s*\(/g;
366
+ let mintMatch;
367
+ while ((mintMatch = mintCallRegex.exec(content)) !== null) {
368
+ findings.push({
369
+ code: "R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING",
370
+ severity: SCAN_SEVERITY.WARN,
371
+ file: filePath,
372
+ line: lineNumberFor(content, mintMatch.index),
373
+ message:
374
+ "createResponseFromTenantAssertion (tenant-assertion session mint) requires the " +
375
+ '"auth.sessionMint" capability, which no function declares in run402.config.json. ' +
376
+ "Without it the gateway returns R402_AUTH_UNTRUSTED_CONTEXT at runtime and mints no session.",
377
+ fix:
378
+ 'Add "capabilities": ["auth.sessionMint"] to this function\'s entry in run402.config.json ' +
379
+ '(under functions.replace.<name>, a sibling to "config"). A service key is NOT sufficient.',
380
+ docs: "https://docs.run402.com/auth/tenant-assertion#capability",
381
+ });
382
+ }
383
+ }
384
+
342
385
  return findings;
343
386
  }
344
387
 
@@ -347,6 +390,10 @@ export function scanFileContent(content, opts = {}) {
347
390
  * line for stable output. */
348
391
  export function scanSourceTree(srcDir, opts = {}) {
349
392
  const findings = [];
393
+ // Capability picture for the tenant-assertion mint check (#8). Read from
394
+ // run402.config.json unless the caller passed it explicitly (tests do).
395
+ const declaredCapabilities =
396
+ opts.declaredCapabilities ?? readDeclaredCapabilities(opts.cwd ?? srcDir);
350
397
  walk(srcDir, (filePath) => {
351
398
  if (!SCANNED_EXTENSIONS.has(extname(filePath))) return;
352
399
  let content;
@@ -362,7 +409,10 @@ export function scanSourceTree(srcDir, opts = {}) {
362
409
  return;
363
410
  }
364
411
  findings.push(
365
- ...scanFileContent(content, { filePath: relative(opts.cwd ?? srcDir, filePath) }),
412
+ ...scanFileContent(content, {
413
+ filePath: relative(opts.cwd ?? srcDir, filePath),
414
+ declaredCapabilities,
415
+ }),
366
416
  );
367
417
  });
368
418
  findings.sort((a, b) => {
@@ -410,6 +460,38 @@ export function _testOnly_authProperties() {
410
460
  return HALLUCINATED_AUTH_PROPERTIES.slice();
411
461
  }
412
462
 
463
+ /** Read the union of `capabilities` declared across all function entries in
464
+ * `run402.config.json` (the apply spec). Used by the tenant-assertion mint
465
+ * check (#8) to suppress the warning when "auth.sessionMint" is declared.
466
+ *
467
+ * Functions live under `functions.replace.<name>` / `functions.set.<name>`
468
+ * with `capabilities?: string[]` as a sibling to `config`. Best-effort:
469
+ * a missing or malformed config returns an empty set (the scanner then
470
+ * warns, which is the safe default — the runtime gate is the hard
471
+ * enforcement). Returns a `Set<string>`. */
472
+ export function readDeclaredCapabilities(cwd = process.cwd()) {
473
+ const caps = new Set();
474
+ let parsed;
475
+ try {
476
+ parsed = JSON.parse(readFileSync(join(cwd, "run402.config.json"), "utf8"));
477
+ } catch {
478
+ return caps; // no config / unreadable / malformed → nothing declared
479
+ }
480
+ const fns = parsed?.functions;
481
+ if (!fns || typeof fns !== "object") return caps;
482
+ for (const bucket of ["replace", "set", "patch"]) {
483
+ const entries = fns[bucket];
484
+ if (!entries || typeof entries !== "object") continue;
485
+ for (const entry of Object.values(entries)) {
486
+ const declared = entry?.capabilities;
487
+ if (Array.isArray(declared)) {
488
+ for (const cap of declared) if (typeof cap === "string") caps.add(cap);
489
+ }
490
+ }
491
+ }
492
+ return caps;
493
+ }
494
+
413
495
  /** Resolve the project's src/ directory. Astro convention is `<root>/src`;
414
496
  * bare Node projects use `<root>/src` or `<root>`. We prefer `src/` if
415
497
  * it exists. */
@@ -1,8 +1,13 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
3
6
 
4
7
  import {
5
8
  scanFileContent,
9
+ scanSourceTree,
10
+ readDeclaredCapabilities,
6
11
  SCAN_SEVERITY,
7
12
  _testOnly_hallucinatedNames,
8
13
  _testOnly_authProperties,
@@ -316,3 +321,97 @@ describe("scanFileContent — line numbers + file paths", () => {
316
321
  assert.equal(findings[0].file, "src/pages/account.astro");
317
322
  });
318
323
  });
324
+
325
+ describe("scanFileContent — tenant-assertion session-mint capability (#8, §5.3 / 7.9)", () => {
326
+ const MINT = 'auth.sessions.createResponseFromTenantAssertion({ tenant, user, method: "password" });';
327
+
328
+ it("flags a mint call when no capability is declared (default opts)", () => {
329
+ const content = [
330
+ 'import { auth } from "@run402/functions";',
331
+ "export default async (req) =>",
332
+ ` ${MINT}`,
333
+ ].join("\n");
334
+ const findings = scanFileContent(content, { filePath: "src/pages/api/login.ts" });
335
+ const f = findings.find(
336
+ (x) => x.code === "R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING",
337
+ );
338
+ assert.ok(f, "should flag the mint call");
339
+ assert.equal(f.severity, SCAN_SEVERITY.WARN);
340
+ assert.equal(f.line, 3);
341
+ assert.equal(f.file, "src/pages/api/login.ts");
342
+ assert.match(f.fix, /auth\.sessionMint/);
343
+ assert.match(f.message, /R402_AUTH_UNTRUSTED_CONTEXT/);
344
+ });
345
+
346
+ it("suppresses when declaredCapabilities (array) includes auth.sessionMint", () => {
347
+ const findings = scanFileContent(MINT, {
348
+ declaredCapabilities: ["auth.sessionMint"],
349
+ });
350
+ assert.ok(
351
+ !findings.some((f) => f.code === "R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING"),
352
+ );
353
+ });
354
+
355
+ it("suppresses when declaredCapabilities (Set) includes auth.sessionMint", () => {
356
+ const findings = scanFileContent(MINT, {
357
+ declaredCapabilities: new Set(["auth.sessionMint"]),
358
+ });
359
+ assert.ok(
360
+ !findings.some((f) => f.code === "R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING"),
361
+ );
362
+ });
363
+
364
+ it("does NOT flag the distinct createResponseFromIdentity proof path", () => {
365
+ const content =
366
+ "auth.sessions.createResponseFromIdentity({ provider, subject, proof, amr });";
367
+ const findings = scanFileContent(content);
368
+ assert.ok(
369
+ !findings.some((f) => f.code === "R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING"),
370
+ );
371
+ });
372
+ });
373
+
374
+ describe("readDeclaredCapabilities — run402.config.json capability union", () => {
375
+ function writeConfig(obj) {
376
+ const dir = mkdtempSync(join(tmpdir(), "r402-doctor-cap-"));
377
+ writeFileSync(join(dir, "run402.config.json"), JSON.stringify(obj));
378
+ return dir;
379
+ }
380
+
381
+ it("collects capabilities across functions.replace + functions.set", () => {
382
+ const dir = writeConfig({
383
+ functions: {
384
+ replace: { api: { capabilities: ["auth.sessionMint"] } },
385
+ set: { cron: { capabilities: ["other.cap"] } },
386
+ },
387
+ });
388
+ const caps = readDeclaredCapabilities(dir);
389
+ assert.ok(caps.has("auth.sessionMint"));
390
+ assert.ok(caps.has("other.cap"));
391
+ });
392
+
393
+ it("returns an empty set when no config / no capabilities", () => {
394
+ const emptyDir = mkdtempSync(join(tmpdir(), "r402-doctor-nocfg-"));
395
+ assert.equal(readDeclaredCapabilities(emptyDir).size, 0);
396
+ const noCapDir = writeConfig({ functions: { replace: { api: { config: {} } } } });
397
+ assert.equal(readDeclaredCapabilities(noCapDir).size, 0);
398
+ });
399
+
400
+ it("scanSourceTree suppresses the mint warning when the config declares it", () => {
401
+ const dir = mkdtempSync(join(tmpdir(), "r402-doctor-tree-"));
402
+ mkdirSync(join(dir, "src"));
403
+ writeFileSync(
404
+ join(dir, "src", "login.ts"),
405
+ "export default async () => auth.sessions.createResponseFromTenantAssertion({});",
406
+ );
407
+ writeFileSync(
408
+ join(dir, "run402.config.json"),
409
+ JSON.stringify({ functions: { replace: { api: { capabilities: ["auth.sessionMint"] } } } }),
410
+ );
411
+ const findings = scanSourceTree(join(dir, "src"), { cwd: dir });
412
+ assert.ok(
413
+ !findings.some((f) => f.code === "R402_DOCTOR_AUTH_SESSION_MINT_CAPABILITY_MISSING"),
414
+ "config-declared capability should suppress the tree-scan warning",
415
+ );
416
+ });
417
+ });
package/lib/email.mjs CHANGED
@@ -13,8 +13,10 @@ Subcommands:
13
13
  info [--project <id>] Show mailbox info (ID, address, slug)
14
14
  status [--project <id>] Alias for 'info' (prefer 'info')
15
15
  send --to <email> [mode flags] Send an email (template or raw HTML)
16
- list [--limit <n>] [--after <cursor>] [--project <id>]
17
- List sent/received messages (paginated)
16
+ list [--limit <n>] [--after <cursor>] [--direction <inbound|outbound>] [--project <id>]
17
+ List messages (paginated). Returns BOTH
18
+ sent + received by default; --direction
19
+ inbound is the reconciliation backstop.
18
20
  get <message_id> [--project <id>] Get a message with replies
19
21
  get-raw <message_id> --output <file> [--project <id>]
20
22
  Fetch raw RFC-822 bytes (inbound only).
@@ -35,6 +37,10 @@ Webhook subcommands:
35
37
  Update a webhook
36
38
  webhooks register --url <url> --events <e1,e2> [--project <id>]
37
39
  Register a new webhook
40
+ webhooks deliveries [--status <s>] [--project <id>]
41
+ List durable delivery rows (DLQ visibility)
42
+ webhooks redrive <delivery_id> [--project <id>]
43
+ Re-queue a dead-lettered delivery
38
44
 
39
45
  Send modes:
40
46
  Template: --template <name> --var key=value [--var ...] OR --vars '{"k":"v",...}'
@@ -286,16 +292,18 @@ async function send(args) {
286
292
  }
287
293
 
288
294
  async function list(args) {
289
- const valueFlags = ["--project", "--limit", "--after", "--mailbox"];
295
+ const valueFlags = ["--project", "--limit", "--after", "--mailbox", "--direction"];
290
296
  validateArgs(args, valueFlags);
291
297
  const projectId = resolveProjectId(strictFlagValue(args, "--project"));
292
298
  const limit = strictFlagValue(args, "--limit");
293
299
  const after = strictFlagValue(args, "--after");
294
300
  const mailbox = strictFlagValue(args, "--mailbox");
301
+ const direction = strictFlagValue(args, "--direction");
295
302
  try {
296
303
  const data = await getSdk().email.list(projectId, {
297
304
  limit: limit ? parseIntegerFlag("--limit", limit) : undefined,
298
305
  after: after ?? undefined,
306
+ direction: direction ?? undefined,
299
307
  mailbox: mailbox ?? undefined,
300
308
  });
301
309
  console.log(JSON.stringify(data, null, 2));
@@ -264,6 +264,9 @@ const { user, role } = await auth.requireRole("admin");
264
264
  const { user, membership } = await auth.requireMembership("member");
265
265
  await auth.requireFresh({ maxAge: "10m", amr: ["passkey"] });
266
266
 
267
+ // Rich, ownership-qualified account-security read (backs <AccountSecurity>):
268
+ const sec = await auth.account.getSecurity(); // AccountSecurity | null
269
+
267
270
  // CSRF for hosted forms (server-side, in <form> rendering):
268
271
  const field = auth.csrfField();
269
272
  // → <input type="hidden" name="_csrf" value="..." />
@@ -301,7 +304,7 @@ routes (\`/auth/v1/sign-in\` etc.) with the CSRF token already wired:
301
304
 
302
305
  \`\`\`astro
303
306
  ---
304
- import { SignIn, SignUp, UserButton, SignedIn, SignedOut } from "@run402/astro";
307
+ import { SignIn, SignUp, UserButton, AccountSecurity, SignedIn, SignedOut } from "@run402/astro";
305
308
  ---
306
309
 
307
310
  <SignedIn>
@@ -310,10 +313,21 @@ import { SignIn, SignUp, UserButton, SignedIn, SignedOut } from "@run402/astro";
310
313
  <SignedOut>
311
314
  <SignIn returnTo="/dashboard" />
312
315
  </SignedOut>
316
+
317
+ <!-- On an account page (gate on <SignedIn>): change password, manage
318
+ passkeys, list/revoke sessions, sign out everywhere, link/unlink OAuth. -->
319
+ <SignedIn>
320
+ <AccountSecurity sections={["password", "passkeys", "sessions", "identities"]} />
321
+ </SignedIn>
313
322
  \`\`\`
314
323
 
315
- Do NOT roll your own sign-in form. The hosted routes handle CSRF, returnTo
316
- validation, OAuth provider bridges, and passkey ceremonies.
324
+ The four hosted-auth components \`<SignIn>\`, \`<SignUp>\`, \`<UserButton>\`,
325
+ \`<AccountSecurity>\` plus the \`<SignedIn>\`/\`<SignedOut>\` gates emit forms
326
+ posting to the platform's hosted routes with the CSRF token already wired.
327
+ Each accepts a default \`<slot>\` for extras (OAuth buttons, links, panels)
328
+ without losing the zero-config default. Do NOT roll your own — the hosted
329
+ routes handle CSRF, returnTo validation, OAuth provider bridges, and passkey
330
+ ceremonies.
317
331
 
318
332
  ## Rendering-mode quick map
319
333
 
package/lib/webhooks.mjs CHANGED
@@ -9,13 +9,22 @@ Usage:
9
9
  run402 email webhooks <action> [args...]
10
10
 
11
11
  Actions:
12
- list [--mailbox <slug|id>] [--project <id>] List webhooks
13
- get <webhook_id> [--mailbox <slug|id>] [--project <id>] Get a webhook
14
- delete <webhook_id> [--mailbox <slug|id>] [--project <id>] Delete a webhook
15
- update <webhook_id> [--url <url>] [--events <e1,e2>] [--mailbox <slug|id>] Update a webhook
16
- register --url <url> --events <e1,e2> [--mailbox <slug|id>] [--project <id>] Register a new webhook
12
+ list [--mailbox <slug|id>] [--project <id>] List webhooks
13
+ get <webhook_id> [--mailbox <slug|id>] [--project <id>] Get a webhook
14
+ delete <webhook_id> [--mailbox <slug|id>] [--project <id>] Delete a webhook
15
+ update <webhook_id> [--url <url>] [--events <e1,e2>] [--mailbox <slug|id>] Update a webhook
16
+ register --url <url> --events <e1,e2> [--mailbox <slug|id>] [--project <id>] Register a new webhook
17
+ deliveries [--status <s>] [--mailbox <slug|id>] [--project <id>] List durable delivery rows (DLQ visibility)
18
+ redrive <delivery_id> [--mailbox <slug|id>] [--project <id>] Re-queue a dead-lettered delivery
17
19
 
18
20
  Valid events: delivery, bounced, complained, reply_received
21
+ Delivery statuses: pending, in_flight, delivered, failed_permanent (the DLQ)
22
+
23
+ Webhook delivery is durable + at-least-once: failures retry with backoff, then
24
+ land in failed_permanent (the dead-letter queue). The delivered body is the
25
+ canonical envelope { id, type, created_at, schema_version, idempotency_key,
26
+ payload } — consumers MUST dedupe on idempotency_key. Use 'deliveries' to
27
+ inspect what was lost and 'redrive' to replay a dead-lettered delivery.
19
28
 
20
29
  Pass --mailbox <slug|id> to target a specific mailbox when the project has more than one.
21
30
 
@@ -24,6 +33,8 @@ Examples:
24
33
  run402 email webhooks register --url https://example.com/hook --events delivery,bounced
25
34
  run402 email webhooks update whk_123 --url https://new.example.com/hook
26
35
  run402 email webhooks delete whk_123
36
+ run402 email webhooks deliveries --status failed_permanent
37
+ run402 email webhooks redrive wd_123
27
38
  `;
28
39
 
29
40
  const SUB_HELP = {
@@ -196,16 +207,60 @@ async function register(args) {
196
207
  }
197
208
  }
198
209
 
210
+ async function deliveries(args) {
211
+ const valueFlags = ["--project", "--mailbox", "--status", "--limit", "--after"];
212
+ validateArgs(args, valueFlags);
213
+ const projectId = resolveProjectId(strictFlagValue(args, "--project"));
214
+ const mailbox = strictFlagValue(args, "--mailbox");
215
+ const status = strictFlagValue(args, "--status");
216
+ const limitRaw = strictFlagValue(args, "--limit");
217
+ const after = strictFlagValue(args, "--after");
218
+ try {
219
+ const data = await getSdk().email.webhooks.listDeliveries(projectId, {
220
+ status: status ?? undefined,
221
+ limit: limitRaw ? Number(limitRaw) : undefined,
222
+ after: after ?? undefined,
223
+ mailbox: mailbox ?? undefined,
224
+ });
225
+ console.log(JSON.stringify(data, null, 2));
226
+ } catch (err) {
227
+ reportSdkError(err);
228
+ }
229
+ }
230
+
231
+ async function redrive(args) {
232
+ const valueFlags = ["--project", "--mailbox"];
233
+ validateArgs(args, valueFlags);
234
+ const deliveryId = positionalArgs(args, valueFlags)[0] ?? null;
235
+ const projectId = resolveProjectId(strictFlagValue(args, "--project"));
236
+ const mailbox = strictFlagValue(args, "--mailbox");
237
+ if (!deliveryId) {
238
+ fail({
239
+ code: "BAD_USAGE",
240
+ message: "Missing delivery_id.",
241
+ hint: "run402 email webhooks redrive <delivery_id>",
242
+ });
243
+ }
244
+ try {
245
+ const data = await getSdk().email.webhooks.redriveDelivery(projectId, deliveryId, { mailbox: mailbox ?? undefined });
246
+ console.log(JSON.stringify(data, null, 2));
247
+ } catch (err) {
248
+ reportSdkError(err);
249
+ }
250
+ }
251
+
199
252
  export async function run(sub, args) {
200
253
  args = normalizeArgv(args);
201
254
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
202
255
  if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
203
256
  switch (sub) {
204
- case "list": await list(args); break;
205
- case "get": await get(args); break;
206
- case "delete": await del(args); break;
207
- case "update": await update(args); break;
208
- case "register": await register(args); break;
257
+ case "list": await list(args); break;
258
+ case "get": await get(args); break;
259
+ case "delete": await del(args); break;
260
+ case "update": await update(args); break;
261
+ case "register": await register(args); break;
262
+ case "deliveries": await deliveries(args); break;
263
+ case "redrive": await redrive(args); break;
209
264
  default:
210
265
  console.error(`Unknown webhooks action: ${sub}\n`);
211
266
  console.log(HELP);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "2.24.0",
3
+ "version": "2.25.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"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"}
@@ -21,7 +21,7 @@
21
21
  import { isCiSessionCredentials } from "../ci-credentials.js";
22
22
  import { assertCiDeployableSpec } from "./ci.js";
23
23
  import { ROUTE_HTTP_METHODS, normalizeDeployResolveRequest, } from "./deploy.types.js";
24
- import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, } from "../errors.js";
24
+ import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, isTransferFreezeError, } from "../errors.js";
25
25
  import { assertAssetMetadata, assertExifPolicy, } from "./assets-validation.js";
26
26
  // ─── Constants ───────────────────────────────────────────────────────────────
27
27
  const PLAN_BODY_LIMIT_BYTES = 5 * 1024 * 1024;
@@ -3091,6 +3091,31 @@ function translateDeployError(err, phase, planId, operationId) {
3091
3091
  context: err.context,
3092
3092
  });
3093
3093
  }
3094
+ // 409 transfer-freeze (PROJECT_HAS_PENDING_TRANSFER) arrives as a dedicated
3095
+ // TransferFreezeError, not an ApiError. Route it through the same
3096
+ // gateway-envelope path so the structured code, details (transfer_id), and
3097
+ // next_actions (cancel/view transfer) survive instead of flattening to
3098
+ // INTERNAL_ERROR. Use the structural guard so the check holds across
3099
+ // duplicate SDK copies / realm boundaries (V8-isolate code-mode).
3100
+ if (isTransferFreezeError(err)) {
3101
+ const body = err.body && typeof err.body === "object" && !Array.isArray(err.body)
3102
+ ? err.body
3103
+ : null;
3104
+ const gw = body ? extractGatewayError(body) : null;
3105
+ if (gw) {
3106
+ return translateGatewayError(gw, phase, planId, operationId);
3107
+ }
3108
+ return new Run402DeployError(err.message, {
3109
+ code: "PROJECT_HAS_PENDING_TRANSFER",
3110
+ phase,
3111
+ retryable: false,
3112
+ operationId,
3113
+ planId,
3114
+ status: err.status,
3115
+ body: err.body,
3116
+ context: err.context,
3117
+ });
3118
+ }
3094
3119
  if (err instanceof NetworkError) {
3095
3120
  return new Run402DeployError(err.message, {
3096
3121
  code: "NETWORK_ERROR",