@wraps.dev/cli 3.11.2 → 3.12.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/dist/cli.js CHANGED
@@ -660,6 +660,9 @@ function parsePulumiError(error) {
660
660
  if (message.includes("stack is currently locked")) {
661
661
  return { code: "STACK_LOCKED" };
662
662
  }
663
+ if (message.includes("pending operations") || message.includes("pending_operations")) {
664
+ return { code: "PENDING_OPERATIONS" };
665
+ }
663
666
  if (/ENOENT[:\s].*\bpulumi\b|spawn pulumi ENOENT/.test(message)) {
664
667
  return { code: "NOT_INSTALLED" };
665
668
  }
@@ -955,6 +958,8 @@ function pulumiErrorToWrapsError(code, iamAction, service, resourceName, resourc
955
958
  );
956
959
  case "STACK_LOCKED":
957
960
  return errors.stackLocked();
961
+ case "PENDING_OPERATIONS":
962
+ return errors.pendingOperations();
958
963
  case "NOT_INSTALLED":
959
964
  return errors.pulumiNotInstalled();
960
965
  case "SES_PERMISSION_DENIED":
@@ -1071,7 +1076,7 @@ Saved regions: ${savedRegions.join(", ")}` : "Pass --region or set AWS_REGION.\n
1071
1076
  pulumiError: (message) => new WrapsError(
1072
1077
  `Infrastructure deployment failed: ${message}`,
1073
1078
  "PULUMI_ERROR",
1074
- "Check your AWS permissions and try again",
1079
+ "The message above is Pulumi's own output. If it is not self-explanatory, re-run the command \u2014 some failures are transient \u2014 and see the troubleshooting guide if it persists.",
1075
1080
  "https://wraps.dev/docs/guides/aws-setup/troubleshooting"
1076
1081
  ),
1077
1082
  noStack: () => new WrapsError(
@@ -1092,6 +1097,12 @@ Saved regions: ${savedRegions.join(", ")}` : "Pass --region or set AWS_REGION.\n
1092
1097
  "This happens when a previous deployment was interrupted.\n\nFor local state, run:\n rm -rf ~/.wraps/pulumi/.pulumi/locks\n\nFor S3 state, delete the lock object in your wraps-state-* bucket under .pulumi/locks/\n\nThen try your command again.",
1093
1098
  "https://wraps.dev/docs/guides/aws-setup/troubleshooting"
1094
1099
  ),
1100
+ pendingOperations: () => new WrapsError(
1101
+ "The Pulumi stack has unresolved operations from a previous deployment",
1102
+ "PENDING_OPERATIONS",
1103
+ "This happens when a previous deployment was interrupted (Ctrl-C, a crashed CI job, lost network) before it could record whether its changes finished.\n\nThose operations may have partially completed, so the safe first move is to reconcile state against what actually exists in AWS:\n pulumi refresh\n\nTo inspect what Pulumi thinks is pending before doing anything:\n pulumi stack export\n\nThen try your command again.\n\nIf a deployment is genuinely still running elsewhere, wait for it to finish instead of clearing anything.",
1104
+ "https://wraps.dev/docs/guides/aws-setup/troubleshooting"
1105
+ ),
1095
1106
  // SMS-specific errors
1096
1107
  smsNotConfigured: () => new WrapsError(
1097
1108
  "SMS infrastructure not found",
@@ -32844,6 +32855,7 @@ var init_dynamodb_metrics = __esm({
32844
32855
  // src/commands/update.ts
32845
32856
  var update_exports = {};
32846
32857
  __export(update_exports, {
32858
+ installRelease: () => installRelease,
32847
32859
  update: () => update
32848
32860
  });
32849
32861
  import { execFileSync } from "child_process";
@@ -32852,6 +32864,7 @@ import {
32852
32864
  createWriteStream,
32853
32865
  mkdirSync as mkdirSync3,
32854
32866
  mkdtempSync,
32867
+ readdirSync as readdirSync3,
32855
32868
  rmSync
32856
32869
  } from "fs";
32857
32870
  import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
@@ -32882,6 +32895,15 @@ async function fetchLatestVersion() {
32882
32895
  }
32883
32896
  return null;
32884
32897
  }
32898
+ function installRelease(source, installDir) {
32899
+ for (const entry of readdirSync3(source)) {
32900
+ const target = join24(installDir, entry);
32901
+ rmSync(target, { recursive: true, force: true });
32902
+ execFileSync("cp", ["-R", join24(source, entry), target]);
32903
+ }
32904
+ chmodSync(join24(installDir, "bin", "wraps"), 493);
32905
+ chmodSync(join24(installDir, "runtime", "node"), 493);
32906
+ }
32885
32907
  function detectPlatformArch() {
32886
32908
  const platform3 = process.platform === "darwin" ? "darwin" : "linux";
32887
32909
  const arch = process.arch === "arm64" ? "arm64" : "x64";
@@ -32978,13 +33000,7 @@ async function update(currentVersion) {
32978
33000
  const extractDir = join24(tmp, "extract");
32979
33001
  mkdirSync3(extractDir, { recursive: true });
32980
33002
  execFileSync("tar", ["xzf", tarballPath, "-C", extractDir]);
32981
- const source = join24(extractDir, "wraps");
32982
- for (const dir of ["bin", "runtime", "lib"]) {
32983
- rmSync(join24(INSTALL_DIR, dir), { recursive: true, force: true });
32984
- execFileSync("cp", ["-R", join24(source, dir), join24(INSTALL_DIR, dir)]);
32985
- }
32986
- chmodSync(join24(INSTALL_DIR, "bin", "wraps"), 493);
32987
- chmodSync(join24(INSTALL_DIR, "runtime", "node"), 493);
33003
+ installRelease(join24(extractDir, "wraps"), INSTALL_DIR);
32988
33004
  });
32989
33005
  console.log();
32990
33006
  progress.succeed(
@@ -38204,7 +38220,7 @@ Run ${pc17.cyan("wraps email init")} to deploy infrastructure first.
38204
38220
  `IAM role ${pc17.cyan(roleName)} not found \u2014 will create it using stored externalId`
38205
38221
  );
38206
38222
  }
38207
- if (!options.force) {
38223
+ if (!(options.force || options.yes)) {
38208
38224
  progress.stop();
38209
38225
  const actionLabel = roleExists2 ? "Update" : "Create";
38210
38226
  const shouldContinue = await confirm6({
@@ -38224,6 +38240,8 @@ Run ${pc17.cyan("wraps email init")} to deploy infrastructure first.
38224
38240
  const smsEnabled = !!smsConfig;
38225
38241
  const smsSendingEnabled = smsConfig && smsConfig.sendingEnabled !== false;
38226
38242
  const smsEventTracking = smsConfig?.eventTracking;
38243
+ let permissionsUpdated = false;
38244
+ let trustPolicyRepaired = false;
38227
38245
  if (!roleExists2 && externalId) {
38228
38246
  await progress.execute("Creating IAM role", async () => {
38229
38247
  const trustPolicy = {
@@ -38263,6 +38281,8 @@ Run ${pc17.cyan("wraps email init")} to deploy infrastructure first.
38263
38281
  })
38264
38282
  );
38265
38283
  });
38284
+ permissionsUpdated = true;
38285
+ trustPolicyRepaired = true;
38266
38286
  } else {
38267
38287
  await progress.execute("Updating IAM role permissions", async () => {
38268
38288
  const { PutRolePolicyCommand: PutRolePolicyCommand2 } = await import("@aws-sdk/client-iam");
@@ -38274,6 +38294,7 @@ Run ${pc17.cyan("wraps email init")} to deploy infrastructure first.
38274
38294
  })
38275
38295
  );
38276
38296
  });
38297
+ permissionsUpdated = true;
38277
38298
  if (externalId) {
38278
38299
  const trustPolicy = {
38279
38300
  Version: "2012-10-17",
@@ -38300,10 +38321,19 @@ Run ${pc17.cyan("wraps email init")} to deploy infrastructure first.
38300
38321
  })
38301
38322
  );
38302
38323
  });
38324
+ trustPolicyRepaired = true;
38325
+ } else {
38326
+ log15.warn(
38327
+ `Trust policy was ${pc17.bold("not")} touched: no stored externalId found for this control plane.`
38328
+ );
38329
+ console.log(
38330
+ ` If ${pc17.cyan(selfhosted ? "wraps selfhost connect" : "wraps platform connect")} still cannot assume ${pc17.cyan(roleName)}, re-run it to repair the trust policy.`
38331
+ );
38303
38332
  }
38304
38333
  }
38305
38334
  progress.stop();
38306
38335
  const actionVerb = roleExists2 ? "updated" : "created";
38336
+ const externalIdPresent = !!externalId;
38307
38337
  trackCommand("platform:update-role", {
38308
38338
  success: true,
38309
38339
  duration_ms: Date.now() - startTime,
@@ -38313,12 +38343,23 @@ Run ${pc17.cyan("wraps email init")} to deploy infrastructure first.
38313
38343
  jsonSuccess("platform.update-role", {
38314
38344
  updated: true,
38315
38345
  action: actionVerb,
38316
- roleName
38346
+ roleName,
38347
+ permissionsUpdated,
38348
+ trustPolicyRepaired,
38349
+ externalIdPresent
38317
38350
  });
38318
38351
  return;
38319
38352
  }
38320
38353
  outro10(pc17.green(`\u2713 Platform access role ${actionVerb} successfully`));
38321
38354
  console.log(`
38355
+ ${pc17.bold("Summary:")}`);
38356
+ console.log(
38357
+ ` ${pc17.green("\u2713")} permissions: ${permissionsUpdated ? "updated" : "unchanged"}`
38358
+ );
38359
+ console.log(
38360
+ trustPolicyRepaired ? ` ${pc17.green("\u2713")} trust policy: repaired` : ` ${pc17.yellow("!")} trust policy: unchanged (no stored externalId)`
38361
+ );
38362
+ console.log(`
38322
38363
  ${pc17.bold("Permissions:")}`);
38323
38364
  console.log(`
38324
38365
  ${pc17.bold(pc17.cyan("Email:"))}`);
@@ -39338,6 +39379,7 @@ async function wrapsDoctor(options) {
39338
39379
 
39339
39380
  // src/commands/email/agent.ts
39340
39381
  init_esm_shims();
39382
+ import { promises as dns } from "dns";
39341
39383
  import * as clack18 from "@clack/prompts";
39342
39384
  import * as pulumi16 from "@pulumi/pulumi";
39343
39385
  import pc20 from "picocolors";
@@ -40770,7 +40812,12 @@ async function createAgentApiClient(tokenFlag) {
40770
40812
  },
40771
40813
  ...body ? { body: JSON.stringify(body) } : {}
40772
40814
  });
40773
- return { ok: true, get: call("GET"), post: call("POST") };
40815
+ return {
40816
+ ok: true,
40817
+ get: call("GET"),
40818
+ post: call("POST"),
40819
+ patch: call("PATCH")
40820
+ };
40774
40821
  }
40775
40822
  async function parseAgentApiError(resp) {
40776
40823
  const data = await resp.json().catch(() => ({}));
@@ -40825,6 +40872,31 @@ var DEFAULT_AGENT_POLICY = {
40825
40872
  allowedRecipientDomains: []
40826
40873
  };
40827
40874
  var AGENT_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,51}$/;
40875
+ var MX_LOOKUP_TIMEOUT_MS = 3e3;
40876
+ async function domainAcceptsMail(domain) {
40877
+ let timer;
40878
+ const timedOut = /* @__PURE__ */ Symbol("mx-lookup-timeout");
40879
+ const timeout = new Promise((resolve) => {
40880
+ timer = setTimeout(() => resolve(timedOut), MX_LOOKUP_TIMEOUT_MS);
40881
+ });
40882
+ try {
40883
+ const result = await Promise.race([dns.resolveMx(domain), timeout]);
40884
+ if (result === timedOut) {
40885
+ return null;
40886
+ }
40887
+ return result.length > 0;
40888
+ } catch (error) {
40889
+ const code = error?.code;
40890
+ if (code === "ENODATA" || code === "ENOTFOUND") {
40891
+ return false;
40892
+ }
40893
+ return null;
40894
+ } finally {
40895
+ if (timer) {
40896
+ clearTimeout(timer);
40897
+ }
40898
+ }
40899
+ }
40828
40900
  async function agentCreate(options) {
40829
40901
  if (!isJsonMode()) {
40830
40902
  clack18.intro(pc20.bold("Create Agent Mailbox"));
@@ -41202,6 +41274,12 @@ ${suggestion}
41202
41274
  return await resp.json();
41203
41275
  });
41204
41276
  const syncFailed = sync.syncStatus === "failed";
41277
+ let mailAccepted = null;
41278
+ try {
41279
+ mailAccepted = await domainAcceptsMail(domain);
41280
+ } catch {
41281
+ mailAccepted = null;
41282
+ }
41205
41283
  if (isJsonMode()) {
41206
41284
  jsonSuccess("email.agent.create", {
41207
41285
  id: created.id,
@@ -41222,7 +41300,11 @@ ${suggestion}
41222
41300
  // the credential doesn't have to be smuggled through an error payload a
41223
41301
  // scripted caller isn't looking at. syncStatus is the field to branch on.
41224
41302
  syncStatus: sync.syncStatus ?? "unknown",
41225
- warning: sync.warning ?? null
41303
+ warning: sync.warning ?? null,
41304
+ // true/false when the apex MX lookup resolved definitively, null when it
41305
+ // could not be determined (timeout, network issue, …) — null must never
41306
+ // be read as "no MX".
41307
+ domainAcceptsMail: mailAccepted
41226
41308
  });
41227
41309
  return;
41228
41310
  }
@@ -41238,6 +41320,15 @@ ${suggestion}
41238
41320
  console.log(` ${pc20.dim("Agent ID:")} ${pc20.cyan(created.id)}`);
41239
41321
  console.log(` ${pc20.dim("Enforcer:")} ${pc20.cyan(aliasArn)}`);
41240
41322
  console.log();
41323
+ if (mailAccepted === false) {
41324
+ clack18.log.warn(
41325
+ `Replies to ${pc20.cyan(emailAddress)} will bounce \u2014 ${pc20.cyan(domain)} has no MX record.`
41326
+ );
41327
+ console.log(
41328
+ ` Set ${pc20.cyan("replyTo")} on every send to a mailbox that can receive, or run ${pc20.cyan("wraps email inbound")} to make ${domain} receive mail.`
41329
+ );
41330
+ console.log();
41331
+ }
41241
41332
  const mcpEnv = [
41242
41333
  `WRAPS_AGENT_ID=${created.id}`,
41243
41334
  `WRAPS_AGENT_ENFORCER_ARN=${aliasArn}`,
@@ -41400,6 +41491,166 @@ async function agentKill(options) {
41400
41491
  }
41401
41492
  clack18.log.success(`Killed ${pc20.cyan(target.emailAddress)}.`);
41402
41493
  }
41494
+ function toList(value) {
41495
+ if (value === void 0) {
41496
+ return [];
41497
+ }
41498
+ return Array.isArray(value) ? value.map(String) : [String(value)];
41499
+ }
41500
+ function dedupePreserveOrder(base, additions) {
41501
+ const seen = new Set(base);
41502
+ const result = [...base];
41503
+ for (const item of additions) {
41504
+ if (!seen.has(item)) {
41505
+ seen.add(item);
41506
+ result.push(item);
41507
+ }
41508
+ }
41509
+ return result;
41510
+ }
41511
+ function sameStringArray(a6, b6) {
41512
+ return a6.length === b6.length && a6.every((v, i6) => v === b6[i6]);
41513
+ }
41514
+ async function agentPolicy(options) {
41515
+ const api = await createAgentApiClient(options.token);
41516
+ if (!api.ok) {
41517
+ if (isJsonMode()) {
41518
+ jsonError("email.agent.policy", {
41519
+ code: "NOT_AUTHENTICATED",
41520
+ message: "No API token found.",
41521
+ suggestion: "Run: wraps auth login"
41522
+ });
41523
+ } else {
41524
+ clack18.log.error("No API token found. Run: wraps auth login");
41525
+ }
41526
+ return;
41527
+ }
41528
+ const listResp = await api.get("/v1/agents");
41529
+ if (!listResp.ok) {
41530
+ const message = await parseAgentApiError(listResp);
41531
+ if (isJsonMode()) {
41532
+ jsonError("email.agent.policy", { code: "API_ERROR", message });
41533
+ } else {
41534
+ clack18.log.error(`Failed to list agents: ${message}`);
41535
+ }
41536
+ return;
41537
+ }
41538
+ const { agents } = await listResp.json();
41539
+ const target = options.name ? agents.find((a6) => a6.name === options.name || a6.id === options.name) : void 0;
41540
+ if (!target) {
41541
+ const message = options.name ? `No agent named "${options.name}".` : "Agent name is required.";
41542
+ if (isJsonMode()) {
41543
+ jsonError("email.agent.policy", { code: "NOT_FOUND", message });
41544
+ } else {
41545
+ clack18.log.error(message);
41546
+ }
41547
+ return;
41548
+ }
41549
+ if (target.status === "KILLED") {
41550
+ const message = `Agent "${target.name}" is killed \u2014 its policy can no longer be changed.`;
41551
+ if (isJsonMode()) {
41552
+ jsonError("email.agent.policy", { code: "AGENT_KILLED", message });
41553
+ } else {
41554
+ clack18.log.error(message);
41555
+ }
41556
+ return;
41557
+ }
41558
+ const clearAllowlist = options.clearAllowlist === true;
41559
+ const allowRecipientAdditions = toList(options.allowRecipient);
41560
+ const allowDomainAdditions = toList(options.allowDomain);
41561
+ if (clearAllowlist && (allowRecipientAdditions.length > 0 || allowDomainAdditions.length > 0)) {
41562
+ const message = "--clear-allowlist cannot be combined with --allow-recipient or --allow-domain.";
41563
+ if (isJsonMode()) {
41564
+ jsonError("email.agent.policy", { code: "VALIDATION", message });
41565
+ } else {
41566
+ clack18.log.error(message);
41567
+ }
41568
+ return;
41569
+ }
41570
+ const current = target.policy;
41571
+ const maxPerHour = options.maxPerHour === void 0 ? current.maxPerHour : Number(options.maxPerHour);
41572
+ const maxPerDay = options.maxPerDay === void 0 ? current.maxPerDay : Number(options.maxPerDay);
41573
+ if (!(Number.isInteger(maxPerHour) && maxPerHour >= 0)) {
41574
+ const message = "--max-per-hour must be a whole number >= 0.";
41575
+ if (isJsonMode()) {
41576
+ jsonError("email.agent.policy", { code: "VALIDATION", message });
41577
+ } else {
41578
+ clack18.log.error(message);
41579
+ }
41580
+ return;
41581
+ }
41582
+ if (!(Number.isInteger(maxPerDay) && maxPerDay >= 0)) {
41583
+ const message = "--max-per-day must be a whole number >= 0.";
41584
+ if (isJsonMode()) {
41585
+ jsonError("email.agent.policy", { code: "VALIDATION", message });
41586
+ } else {
41587
+ clack18.log.error(message);
41588
+ }
41589
+ return;
41590
+ }
41591
+ const allowedRecipients = clearAllowlist ? [] : dedupePreserveOrder(current.allowedRecipients, allowRecipientAdditions);
41592
+ const allowedRecipientDomains = clearAllowlist ? [] : dedupePreserveOrder(
41593
+ current.allowedRecipientDomains,
41594
+ allowDomainAdditions
41595
+ );
41596
+ const nextPolicy = {
41597
+ maxPerHour,
41598
+ maxPerDay,
41599
+ allowedRecipients,
41600
+ allowedRecipientDomains
41601
+ };
41602
+ const noChange = nextPolicy.maxPerHour === current.maxPerHour && nextPolicy.maxPerDay === current.maxPerDay && sameStringArray(nextPolicy.allowedRecipients, current.allowedRecipients) && sameStringArray(
41603
+ nextPolicy.allowedRecipientDomains,
41604
+ current.allowedRecipientDomains
41605
+ );
41606
+ if (noChange) {
41607
+ if (isJsonMode()) {
41608
+ jsonSuccess("email.agent.policy", {
41609
+ agent: target,
41610
+ syncStatus: "skipped",
41611
+ noChange: true
41612
+ });
41613
+ } else {
41614
+ clack18.log.info("No changes.");
41615
+ }
41616
+ return;
41617
+ }
41618
+ const resp = await api.patch(`/v1/agents/${target.id}/policy`, nextPolicy);
41619
+ if (!resp.ok) {
41620
+ const message = await parseAgentApiError(resp);
41621
+ if (isJsonMode()) {
41622
+ jsonError("email.agent.policy", { code: "API_ERROR", message });
41623
+ } else {
41624
+ clack18.log.error(`Failed to update policy: ${message}`);
41625
+ }
41626
+ return;
41627
+ }
41628
+ const body = await resp.json();
41629
+ const syncFailed = body.syncStatus === "failed";
41630
+ if (isJsonMode()) {
41631
+ jsonSuccess("email.agent.policy", {
41632
+ agent: body.agent,
41633
+ syncStatus: body.syncStatus,
41634
+ warning: body.warning ?? null
41635
+ });
41636
+ if (syncFailed) {
41637
+ process.exit(1);
41638
+ }
41639
+ return;
41640
+ }
41641
+ const before = `${current.maxPerHour}/hr \xB7 ${current.maxPerDay}/day`;
41642
+ const after = `${body.agent.policy.maxPerHour}/hr \xB7 ${body.agent.policy.maxPerDay}/day`;
41643
+ clack18.log.success(`${before} \u2192 ${after}`);
41644
+ console.log(
41645
+ ` Allowlist: ${body.agent.policy.allowedRecipients.length} recipient(s), ${body.agent.policy.allowedRecipientDomains.length} domain(s)`
41646
+ );
41647
+ if (syncFailed) {
41648
+ if (body.warning) {
41649
+ clack18.log.warn(body.warning);
41650
+ }
41651
+ process.exit(1);
41652
+ }
41653
+ }
41403
41654
 
41404
41655
  // src/commands/email/check.ts
41405
41656
  init_esm_shims();
@@ -41408,7 +41659,7 @@ import * as clack19 from "@clack/prompts";
41408
41659
 
41409
41660
  // ../email-check/dist/index.js
41410
41661
  init_esm_shims();
41411
- import { promises as dns, Resolver } from "dns";
41662
+ import { promises as dns2, Resolver } from "dns";
41412
41663
  import { URL as URL2 } from "url";
41413
41664
  import * as net from "net";
41414
41665
  import * as tls from "tls";
@@ -41731,7 +41982,7 @@ function createNodeDnsProvider(options = {}) {
41731
41982
  async function resolveTxt(domain) {
41732
41983
  try {
41733
41984
  const records = await withTimeout2(
41734
- dns.resolveTxt(domain),
41985
+ dns2.resolveTxt(domain),
41735
41986
  `TXT lookup for ${domain}`
41736
41987
  );
41737
41988
  return records;
@@ -41745,7 +41996,7 @@ function createNodeDnsProvider(options = {}) {
41745
41996
  async function resolveMx(domain) {
41746
41997
  try {
41747
41998
  const records = await withTimeout2(
41748
- dns.resolveMx(domain),
41999
+ dns2.resolveMx(domain),
41749
42000
  `MX lookup for ${domain}`
41750
42001
  );
41751
42002
  return records.sort((a6, b6) => a6.priority - b6.priority);
@@ -41759,7 +42010,7 @@ function createNodeDnsProvider(options = {}) {
41759
42010
  async function resolveA(domain) {
41760
42011
  try {
41761
42012
  const records = await withTimeout2(
41762
- dns.resolve4(domain),
42013
+ dns2.resolve4(domain),
41763
42014
  `A lookup for ${domain}`
41764
42015
  );
41765
42016
  return records;
@@ -41773,7 +42024,7 @@ function createNodeDnsProvider(options = {}) {
41773
42024
  async function resolveAaaa(domain) {
41774
42025
  try {
41775
42026
  const records = await withTimeout2(
41776
- dns.resolve6(domain),
42027
+ dns2.resolve6(domain),
41777
42028
  `AAAA lookup for ${domain}`
41778
42029
  );
41779
42030
  return records;
@@ -41787,7 +42038,7 @@ function createNodeDnsProvider(options = {}) {
41787
42038
  async function resolvePtr(ip) {
41788
42039
  try {
41789
42040
  const records = await withTimeout2(
41790
- dns.reverse(ip),
42041
+ dns2.reverse(ip),
41791
42042
  `PTR lookup for ${ip}`
41792
42043
  );
41793
42044
  return records;
@@ -41801,7 +42052,7 @@ function createNodeDnsProvider(options = {}) {
41801
42052
  async function resolveCaa(domain) {
41802
42053
  try {
41803
42054
  const records = await withTimeout2(
41804
- dns.resolveCaa(domain),
42055
+ dns2.resolveCaa(domain),
41805
42056
  `CAA lookup for ${domain}`
41806
42057
  );
41807
42058
  return records.map((r6) => {
@@ -41833,7 +42084,7 @@ function createNodeDnsProvider(options = {}) {
41833
42084
  async function resolveCname(domain) {
41834
42085
  try {
41835
42086
  const records = await withTimeout2(
41836
- dns.resolveCname(domain),
42087
+ dns2.resolveCname(domain),
41837
42088
  `CNAME lookup for ${domain}`
41838
42089
  );
41839
42090
  return records;
@@ -48939,7 +49190,7 @@ ${pc27.dim(`Distribution ${trackingDistributionId} is disabled; delete it from t
48939
49190
  // src/commands/email/inbound.ts
48940
49191
  init_esm_shims();
48941
49192
  import { randomBytes as randomBytes4 } from "crypto";
48942
- import { promises as dns2 } from "dns";
49193
+ import { promises as dns3 } from "dns";
48943
49194
  import * as clack27 from "@clack/prompts";
48944
49195
  import * as pulumi20 from "@pulumi/pulumi";
48945
49196
  import pc28 from "picocolors";
@@ -49837,7 +50088,7 @@ Enable it: ${pc28.cyan("wraps email inbound init")}
49837
50088
  `Checking MX record for ${receivingDomain}`,
49838
50089
  async () => {
49839
50090
  try {
49840
- const records = await dns2.resolveMx(receivingDomain);
50091
+ const records = await dns3.resolveMx(receivingDomain);
49841
50092
  const hasSES = records.some(
49842
50093
  (r6) => r6.exchange.includes("inbound-smtp")
49843
50094
  );
@@ -49866,7 +50117,7 @@ Enable it: ${pc28.cyan("wraps email inbound init")}
49866
50117
  `Checking SPF record for ${receivingDomain}`,
49867
50118
  async () => {
49868
50119
  try {
49869
- const records = await dns2.resolveTxt(receivingDomain);
50120
+ const records = await dns3.resolveTxt(receivingDomain);
49870
50121
  const flat = records.map((r6) => r6.join(""));
49871
50122
  const spf = flat.find((r6) => r6.startsWith("v=spf1"));
49872
50123
  const hasSES = spf?.includes("amazonses.com") ?? false;
@@ -51863,7 +52114,7 @@ async function emailPlan(options) {
51863
52114
  init_esm_shims();
51864
52115
  init_dist();
51865
52116
  import { randomBytes as randomBytes5 } from "crypto";
51866
- import { promises as dns3 } from "dns";
52117
+ import { promises as dns4 } from "dns";
51867
52118
  import {
51868
52119
  GetParameterCommand,
51869
52120
  ParameterNotFound,
@@ -52486,7 +52737,7 @@ Enable it: ${pc35.cyan("wraps email reply init --domain yourapp.com")}
52486
52737
  const receiptRuleHasDomain = ruleDomains.includes(`r.mail.${entry.domain}`);
52487
52738
  let mxResolves = false;
52488
52739
  try {
52489
- const records = await dns3.resolveMx(`r.mail.${entry.domain}`);
52740
+ const records = await dns4.resolveMx(`r.mail.${entry.domain}`);
52490
52741
  mxResolves = records.some((r6) => r6.exchange.includes("inbound-smtp"));
52491
52742
  } catch (err) {
52492
52743
  const code = err?.code;
@@ -59916,6 +60167,50 @@ async function deployEventBridge(metadata, region, identity, webhookSecret, prog
59916
60167
  progress.succeed("Event streaming configured");
59917
60168
  }
59918
60169
  var WRAPS_PLATFORM_ACCOUNT_ID = "905130073023";
60170
+ async function verifyTrustPolicyExternalId(iam13, roleName, externalId, progress) {
60171
+ const readCurrentExternalId = async () => {
60172
+ const roleResult = await iam13.send(
60173
+ new GetRoleCommand2({ RoleName: roleName })
60174
+ );
60175
+ const rawDocument = roleResult.Role?.AssumeRolePolicyDocument;
60176
+ if (!rawDocument) {
60177
+ throw new Error(
60178
+ "GetRole response did not include AssumeRolePolicyDocument"
60179
+ );
60180
+ }
60181
+ const parsed = JSON.parse(decodeURIComponent(rawDocument));
60182
+ return parsed.Statement?.[0]?.Condition?.StringEquals?.["sts:ExternalId"];
60183
+ };
60184
+ const reportUnverifiable = (error) => {
60185
+ const errName = error && typeof error === "object" && "name" in error ? error.name : "Unknown";
60186
+ const errMsg = error instanceof Error ? error.message : String(error);
60187
+ progress.info(
60188
+ `Could not verify the trust policy write (${errName}: ${errMsg}) \u2014 assuming it succeeded.`
60189
+ );
60190
+ };
60191
+ let current;
60192
+ try {
60193
+ current = await readCurrentExternalId();
60194
+ } catch (error) {
60195
+ reportUnverifiable(error);
60196
+ return;
60197
+ }
60198
+ if (current === externalId) {
60199
+ return;
60200
+ }
60201
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
60202
+ try {
60203
+ current = await readCurrentExternalId();
60204
+ } catch (error) {
60205
+ reportUnverifiable(error);
60206
+ return;
60207
+ }
60208
+ if (current !== externalId) {
60209
+ throw new Error(
60210
+ `Trust policy verification failed: role ${roleName} does not carry the expected externalId after the write.`
60211
+ );
60212
+ }
60213
+ }
59919
60214
  async function updatePlatformRole(metadata, progress, externalId, selfhosted) {
59920
60215
  const roleName = selfhosted ? SELFHOST_CONSOLE_ACCESS_ROLE_NAME : CONSOLE_ACCESS_ROLE_NAME;
59921
60216
  const iam13 = new IAMClient4({ region: "us-east-1" });
@@ -59972,6 +60267,10 @@ async function updatePlatformRole(metadata, progress, externalId, selfhosted) {
59972
60267
  })
59973
60268
  );
59974
60269
  });
60270
+ await progress.execute(
60271
+ "Verifying trust policy",
60272
+ () => verifyTrustPolicyExternalId(iam13, roleName, externalId, progress)
60273
+ );
59975
60274
  }
59976
60275
  progress.succeed("Platform access role updated");
59977
60276
  } else if (externalId) {
@@ -60013,6 +60312,10 @@ async function updatePlatformRole(metadata, progress, externalId, selfhosted) {
60013
60312
  })
60014
60313
  );
60015
60314
  });
60315
+ await progress.execute(
60316
+ "Verifying trust policy",
60317
+ () => verifyTrustPolicyExternalId(iam13, roleName, externalId, progress)
60318
+ );
60016
60319
  progress.succeed("Platform access role created");
60017
60320
  } else {
60018
60321
  progress.info(
@@ -60020,13 +60323,52 @@ async function updatePlatformRole(metadata, progress, externalId, selfhosted) {
60020
60323
  );
60021
60324
  }
60022
60325
  }
60023
- async function resolveOrganization(orgs) {
60326
+ async function resolveOrganization(orgs, options) {
60024
60327
  if (!orgs || orgs.length === 0) {
60025
60328
  return null;
60026
60329
  }
60330
+ if (options.org) {
60331
+ const needle = options.org.toLowerCase();
60332
+ const match = orgs.find((o3) => o3.slug.toLowerCase() === needle) || orgs.find((o3) => o3.id.toLowerCase() === needle);
60333
+ if (match) {
60334
+ return match;
60335
+ }
60336
+ const available = orgs.map((o3) => o3.slug).join(", ");
60337
+ if (isJsonMode()) {
60338
+ jsonError("platform.connect", {
60339
+ code: "ORG_NOT_FOUND",
60340
+ message: `No organization matches "${options.org}".`,
60341
+ suggestion: `Available organizations: ${available}`
60342
+ });
60343
+ } else {
60344
+ log43.error(`No organization matches ${pc48.cyan(options.org)}.`);
60345
+ console.log(`
60346
+ Available organizations: ${available}
60347
+ `);
60348
+ }
60349
+ process.exit(1);
60350
+ }
60027
60351
  if (orgs.length === 1) {
60028
60352
  return orgs[0];
60029
60353
  }
60354
+ if (options.json || options.yes) {
60355
+ const available = orgs.map((o3) => o3.slug).join(", ");
60356
+ if (isJsonMode()) {
60357
+ jsonError("platform.connect", {
60358
+ code: "ORG_AMBIGUOUS",
60359
+ message: "Multiple organizations found \u2014 pass --org <slug> to choose one non-interactively.",
60360
+ suggestion: `Available organizations: ${available}`
60361
+ });
60362
+ } else {
60363
+ log43.error(
60364
+ "Multiple organizations found. Pass --org <slug> to choose one non-interactively."
60365
+ );
60366
+ console.log(`
60367
+ Available organizations: ${available}
60368
+ `);
60369
+ }
60370
+ process.exit(1);
60371
+ }
60030
60372
  const selected = await select21({
60031
60373
  message: "Which organization should this AWS account connect to?",
60032
60374
  options: orgs.map((org) => ({
@@ -60135,7 +60477,11 @@ Run ${pc48.cyan("wraps selfhost login")} first.
60135
60477
  token = saasToken;
60136
60478
  organizations = (await readAuthConfig())?.auth?.organizations;
60137
60479
  }
60138
- const org = await resolveOrganization(organizations);
60480
+ const org = await resolveOrganization(organizations, {
60481
+ org: options.org,
60482
+ json: options.json,
60483
+ yes: options.yes
60484
+ });
60139
60485
  if (!org) {
60140
60486
  progress.stop();
60141
60487
  log43.error(
@@ -60213,6 +60559,7 @@ You can try the manual flow: ${pc48.cyan("wraps auth logout")} then ${pc48.cyan(
60213
60559
  process.exit(1);
60214
60560
  }
60215
60561
  progress.succeed("Connection registered");
60562
+ const connectionRegistered = true;
60216
60563
  if (selfhosted) {
60217
60564
  metadata.selfhostPlatform = {
60218
60565
  externalId: result.externalId,
@@ -60245,16 +60592,7 @@ You can try the manual flow: ${pc48.cyan("wraps auth logout")} then ${pc48.cyan(
60245
60592
  }
60246
60593
  }
60247
60594
  await saveConnectionMetadata(metadata);
60248
- if (hasEmail && !adopted) {
60249
- await deployEventBridge(
60250
- metadata,
60251
- region,
60252
- identity,
60253
- result.webhookSecret,
60254
- progress,
60255
- selfhosted ? { url: apiBaseUrl } : void 0
60256
- );
60257
- }
60595
+ let roleUpdated = false;
60258
60596
  try {
60259
60597
  await updatePlatformRole(
60260
60598
  metadata,
@@ -60262,6 +60600,7 @@ You can try the manual flow: ${pc48.cyan("wraps auth logout")} then ${pc48.cyan(
60262
60600
  result.externalId,
60263
60601
  selfhosted
60264
60602
  );
60603
+ roleUpdated = true;
60265
60604
  } catch (error) {
60266
60605
  const errName = error && typeof error === "object" && "name" in error ? error.name : "Unknown";
60267
60606
  const errMsg = error instanceof Error ? error.message : String(error);
@@ -60271,6 +60610,29 @@ You can try the manual flow: ${pc48.cyan("wraps auth logout")} then ${pc48.cyan(
60271
60610
  Run ${pc48.cyan("wraps platform update-role")} to retry.`
60272
60611
  );
60273
60612
  }
60613
+ let eventStreamingOk = null;
60614
+ if (hasEmail && !adopted) {
60615
+ try {
60616
+ await deployEventBridge(
60617
+ metadata,
60618
+ region,
60619
+ identity,
60620
+ result.webhookSecret,
60621
+ progress,
60622
+ selfhosted ? { url: apiBaseUrl } : void 0
60623
+ );
60624
+ eventStreamingOk = true;
60625
+ } catch (error) {
60626
+ eventStreamingOk = false;
60627
+ const errName = error && typeof error === "object" && "name" in error ? error.name : "Unknown";
60628
+ const errMsg = error instanceof Error ? error.message : String(error);
60629
+ log43.warn(
60630
+ `Could not configure event streaming (${errName}): ${errMsg}
60631
+ The connection is registered and the access role is up to date \u2014 the dashboard will work.
60632
+ Only SES event delivery is missing. Run ${pc48.cyan("wraps email config")} to retry.`
60633
+ );
60634
+ }
60635
+ }
60274
60636
  await saveConnectionMetadata(metadata);
60275
60637
  progress.stop();
60276
60638
  if (isJsonMode()) {
@@ -60281,16 +60643,32 @@ You can try the manual flow: ${pc48.cyan("wraps auth logout")} then ${pc48.cyan(
60281
60643
  connectionId: result.connectionId,
60282
60644
  webhookConnected: !adopted,
60283
60645
  selfhosted,
60284
- adopted
60646
+ adopted,
60647
+ connectionRegistered,
60648
+ roleUpdated,
60649
+ eventStreamingOk
60285
60650
  });
60286
60651
  } else if (adopted) {
60287
- outro27(pc48.green("AWS account adopted!"));
60288
- console.log();
60289
- console.log(
60290
- pc48.dim(
60291
- "Registered this account and repaired the IAM trust policy so the dashboard can reach it."
60292
- )
60293
- );
60652
+ if (roleUpdated) {
60653
+ outro27(pc48.green("AWS account adopted!"));
60654
+ console.log();
60655
+ console.log(
60656
+ pc48.dim(
60657
+ "Registered this account and repaired the IAM trust policy so the dashboard can reach it."
60658
+ )
60659
+ );
60660
+ } else {
60661
+ outro27(
60662
+ pc48.yellow("AWS account adopted, but the IAM role was NOT repaired.")
60663
+ );
60664
+ console.log();
60665
+ console.log(
60666
+ pc48.dim(
60667
+ "Registered this account, but the dashboard CANNOT reach it yet \u2014 the IAM trust policy was not repaired."
60668
+ )
60669
+ );
60670
+ console.log(` Run ${pc48.cyan("wraps platform update-role")} to retry.`);
60671
+ }
60294
60672
  console.log(
60295
60673
  pc48.dim(
60296
60674
  "No infrastructure was deployed and no event wiring was changed."
@@ -60299,17 +60677,38 @@ You can try the manual flow: ${pc48.cyan("wraps auth logout")} then ${pc48.cyan(
60299
60677
  console.log(` Dashboard: ${pc48.cyan(dashboardUrl)}`);
60300
60678
  console.log();
60301
60679
  } else {
60680
+ const fullySucceeded = roleUpdated && eventStreamingOk !== false;
60302
60681
  outro27(
60303
- pc48.green(
60682
+ fullySucceeded ? pc48.green(
60304
60683
  selfhosted ? "Self-hosted connection complete!" : "Platform connection complete!"
60684
+ ) : pc48.yellow(
60685
+ selfhosted ? "Self-hosted connection registered, with issues." : "Platform connection registered, with issues."
60305
60686
  )
60306
60687
  );
60307
60688
  console.log();
60308
60689
  console.log(
60309
- pc48.dim(
60310
- "Events from your AWS infrastructure will stream to the dashboard."
60311
- )
60690
+ ` ${roleUpdated ? pc48.green("\u2713") : pc48.red("\u2717")} Access role: ${roleUpdated ? "up to date" : "NOT updated \u2014 dashboard cannot reach this account"}`
60312
60691
  );
60692
+ if (eventStreamingOk !== null) {
60693
+ console.log(
60694
+ ` ${eventStreamingOk ? pc48.green("\u2713") : pc48.yellow("!")} Event streaming: ${eventStreamingOk ? "configured" : "not configured"}`
60695
+ );
60696
+ }
60697
+ if (!roleUpdated) {
60698
+ console.log(` Run ${pc48.cyan("wraps platform update-role")} to retry.`);
60699
+ }
60700
+ if (eventStreamingOk === false) {
60701
+ console.log(
60702
+ ` Run ${pc48.cyan("wraps email config")} to retry event streaming.`
60703
+ );
60704
+ }
60705
+ if (fullySucceeded) {
60706
+ console.log(
60707
+ pc48.dim(
60708
+ "Events from your AWS infrastructure will stream to the dashboard."
60709
+ )
60710
+ );
60711
+ }
60313
60712
  console.log(` Dashboard: ${pc48.cyan(dashboardUrl)}`);
60314
60713
  console.log();
60315
60714
  }
@@ -60319,6 +60718,9 @@ You can try the manual flow: ${pc48.cyan("wraps auth logout")} then ${pc48.cyan(
60319
60718
  duration_ms: duration,
60320
60719
  authenticated: true
60321
60720
  });
60721
+ if (!roleUpdated) {
60722
+ process.exit(1);
60723
+ }
60322
60724
  } catch (error) {
60323
60725
  progress.stop();
60324
60726
  const duration = Date.now() - startTime;
@@ -63413,7 +63815,7 @@ function createMetricsRouter(config2) {
63413
63815
  init_esm_shims();
63414
63816
  init_config_set_slug();
63415
63817
  init_metadata();
63416
- import dns4 from "dns/promises";
63818
+ import dns5 from "dns/promises";
63417
63819
  import { Router as createRouter6 } from "express";
63418
63820
 
63419
63821
  // src/console/services/settings-service.ts
@@ -63577,7 +63979,7 @@ function createSettingsRouter(config2) {
63577
63979
  }
63578
63980
  console.log(`[Verify] Checking CNAME for: ${domain}`);
63579
63981
  console.log(`[Verify] Expected target: ${expectedTarget}`);
63580
- const records = await dns4.resolveCname(domain);
63982
+ const records = await dns5.resolveCname(domain);
63581
63983
  console.log("[Verify] CNAME records found:", records);
63582
63984
  const verified = records.some(
63583
63985
  (record) => record.toLowerCase().includes(expectedTarget.toLowerCase())
@@ -63610,7 +64012,7 @@ function createSettingsRouter(config2) {
63610
64012
  }
63611
64013
  const dmarcDomain = `_dmarc.${domain}`;
63612
64014
  console.log(`[Verify] Checking DMARC for: ${dmarcDomain}`);
63613
- const records = await dns4.resolveTxt(dmarcDomain);
64015
+ const records = await dns5.resolveTxt(dmarcDomain);
63614
64016
  console.log("[Verify] TXT records found:", records);
63615
64017
  const hasDmarc = records.some((record) => {
63616
64018
  const value = record.join("");
@@ -69818,7 +70220,10 @@ function showHelp() {
69818
70220
  );
69819
70221
  console.log(` ${pc72.cyan("email agent list")} List agents`);
69820
70222
  console.log(
69821
- ` ${pc72.cyan("email agent kill")} Kill an agent (revoke sending)
70223
+ ` ${pc72.cyan("email agent kill")} Kill an agent (revoke sending)`
70224
+ );
70225
+ console.log(
70226
+ ` ${pc72.cyan("email agent policy")} Change an agent's caps or allowlist
69822
70227
  `
69823
70228
  );
69824
70229
  console.log("Template Commands:");
@@ -70377,11 +70782,24 @@ async function run() {
70377
70782
  json: flags.json
70378
70783
  });
70379
70784
  break;
70785
+ case "policy":
70786
+ await agentPolicy({
70787
+ // baseline:allow-no-region — API-backed like agentList/agentKill, not direct AWS
70788
+ name: sub[3] || flags.name,
70789
+ maxPerHour: flags["max-per-hour"],
70790
+ maxPerDay: flags["max-per-day"],
70791
+ allowRecipient: flags["allow-recipient"],
70792
+ allowDomain: flags["allow-domain"],
70793
+ clearAllowlist: flags["clear-allowlist"] === true,
70794
+ token: flags.token,
70795
+ json: flags.json
70796
+ });
70797
+ break;
70380
70798
  default:
70381
70799
  throw errors.unknownCommand(
70382
70800
  "agent command",
70383
70801
  agentSubCommand,
70384
- "Available commands: create, list, kill"
70802
+ "Available commands: create, list, kill, policy"
70385
70803
  );
70386
70804
  }
70387
70805
  break;
@@ -70734,13 +71152,15 @@ async function run() {
70734
71152
  force: flags.force,
70735
71153
  yes: flags.yes,
70736
71154
  json: flags.json,
70737
- selfhosted: true
71155
+ selfhosted: true,
71156
+ org: flags.org
70738
71157
  });
70739
71158
  break;
70740
71159
  case "update-role":
70741
71160
  await updateRole({
70742
71161
  region: flags.region,
70743
71162
  force: flags.force,
71163
+ yes: flags.yes,
70744
71164
  json: flags.json,
70745
71165
  selfhosted: true
70746
71166
  });
@@ -70946,7 +71366,8 @@ async function run() {
70946
71366
  region: flags.region,
70947
71367
  force: flags.force,
70948
71368
  yes: flags.yes,
70949
- json: flags.json
71369
+ json: flags.json,
71370
+ org: flags.org
70950
71371
  });
70951
71372
  break;
70952
71373
  case "disconnect":
@@ -70961,6 +71382,7 @@ async function run() {
70961
71382
  await updateRole({
70962
71383
  region: flags.region,
70963
71384
  force: flags.force,
71385
+ yes: flags.yes,
70964
71386
  json: flags.json
70965
71387
  });
70966
71388
  break;