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.
- package/lib/doctor-source-scan.mjs +83 -1
- package/lib/doctor-source-scan.test.mjs +99 -0
- package/lib/email.mjs +11 -3
- package/lib/init-astro.mjs +17 -3
- package/lib/webhooks.mjs +65 -10
- package/package.json +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +26 -1
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/email.d.ts +58 -1
- package/sdk/dist/namespaces/email.d.ts.map +1 -1
- package/sdk/dist/namespaces/email.js +43 -0
- package/sdk/dist/namespaces/email.js.map +1 -1
- package/sdk/dist/scoped.d.ts +3 -1
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +6 -0
- package/sdk/dist/scoped.js.map +1 -1
|
@@ -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, {
|
|
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
|
|
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));
|
package/lib/init-astro.mjs
CHANGED
|
@@ -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
|
-
|
|
316
|
-
|
|
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
|
|
13
|
-
get
|
|
14
|
-
delete
|
|
15
|
-
update
|
|
16
|
-
register
|
|
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":
|
|
205
|
-
case "get":
|
|
206
|
-
case "delete":
|
|
207
|
-
case "update":
|
|
208
|
-
case "register":
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/namespaces/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;
|
|
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",
|