@geekmidas/cli 10.0.0-alpha.39 → 10.0.0-alpha.40

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.
Files changed (47) hide show
  1. package/dist/{OpenApiTsGenerator-CeEI4ll9.cjs → OpenApiTsGenerator-B4Kj_qNU.cjs} +12 -5
  2. package/dist/{OpenApiTsGenerator-CeEI4ll9.cjs.map → OpenApiTsGenerator-B4Kj_qNU.cjs.map} +1 -1
  3. package/dist/{OpenApiTsGenerator-DQqOuSW8.mjs → OpenApiTsGenerator-CEh3EKN3.mjs} +12 -5
  4. package/dist/{OpenApiTsGenerator-DQqOuSW8.mjs.map → OpenApiTsGenerator-CEh3EKN3.mjs.map} +1 -1
  5. package/dist/config.d.cts +2 -2
  6. package/dist/config.d.mts +2 -2
  7. package/dist/discover-B0f_kuNN.cjs.map +1 -1
  8. package/dist/discover-BLhf7V_J.mjs.map +1 -1
  9. package/dist/{index-CLH1DgsO.d.cts → index-B3PeGOQp.d.cts} +2 -2
  10. package/dist/{index-CLH1DgsO.d.cts.map → index-B3PeGOQp.d.cts.map} +1 -1
  11. package/dist/{index-CST2jOPH.d.mts → index-BOyaGcCm.d.mts} +2 -2
  12. package/dist/{index-CST2jOPH.d.mts.map → index-BOyaGcCm.d.mts.map} +1 -1
  13. package/dist/index.cjs +206 -172
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.mjs +206 -172
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{migrate-BJR912RE.mjs → migrate-BVI3yqcE.mjs} +2 -2
  18. package/dist/{migrate-BJR912RE.mjs.map → migrate-BVI3yqcE.mjs.map} +1 -1
  19. package/dist/{migrate-Bu8POqsR.cjs → migrate-CFOUApUt.cjs} +2 -2
  20. package/dist/{migrate-Bu8POqsR.cjs.map → migrate-CFOUApUt.cjs.map} +1 -1
  21. package/dist/openapi.cjs +1 -1
  22. package/dist/openapi.d.cts +1 -1
  23. package/dist/openapi.d.mts +1 -1
  24. package/dist/openapi.mjs +1 -1
  25. package/dist/{ready-BXmLS4VB.mjs → ready-D8UXsP6q.mjs} +2 -2
  26. package/dist/{ready-BXmLS4VB.mjs.map → ready-D8UXsP6q.mjs.map} +1 -1
  27. package/dist/{ready-B0WVtkjR.cjs → ready-mcxamf73.cjs} +2 -2
  28. package/dist/{ready-B0WVtkjR.cjs.map → ready-mcxamf73.cjs.map} +1 -1
  29. package/dist/reconcile/public.d.cts +1 -1
  30. package/dist/reconcile/public.d.mts +1 -1
  31. package/dist/{types-B0EVAF6U.d.mts → types-0R124hrY.d.mts} +1 -2
  32. package/dist/types-0R124hrY.d.mts.map +1 -0
  33. package/dist/{types-CHKCUKM0.d.cts → types-DfdYe31u.d.cts} +1 -2
  34. package/dist/types-DfdYe31u.d.cts.map +1 -0
  35. package/dist/vitest.cjs +1 -1
  36. package/dist/vitest.mjs +1 -1
  37. package/dist/workspace/index.d.cts +2 -2
  38. package/dist/workspace/index.d.mts +2 -2
  39. package/dist/{workspace-DZ1G4zow.cjs → workspace-BIqeeqcR.cjs} +167 -33
  40. package/dist/workspace-BIqeeqcR.cjs.map +1 -0
  41. package/dist/{workspace-CrFbItPz.mjs → workspace-DwS1JK5u.mjs} +165 -33
  42. package/dist/workspace-DwS1JK5u.mjs.map +1 -0
  43. package/package.json +16 -14
  44. package/dist/types-B0EVAF6U.d.mts.map +0 -1
  45. package/dist/types-CHKCUKM0.d.cts.map +0 -1
  46. package/dist/workspace-CrFbItPz.mjs.map +0 -1
  47. package/dist/workspace-DZ1G4zow.cjs.map +0 -1
@@ -4,7 +4,7 @@ const require_config = require("./config-CJ-PAb7M.cjs");
4
4
  const require_workspace = require("./workspace-RYSYtHP7.cjs");
5
5
  const require_storage = require("./storage-CSGoC3bK.cjs");
6
6
  const require_store = require("./store-DQgxT_WR.cjs");
7
- const require_ready = require("./ready-B0WVtkjR.cjs");
7
+ const require_ready = require("./ready-mcxamf73.cjs");
8
8
  let node_fs = require("node:fs");
9
9
  let node_fs_promises = require("node:fs/promises");
10
10
  let node_os = require("node:os");
@@ -15,7 +15,13 @@ let node_net = require("node:net");
15
15
  let dotenv = require("dotenv");
16
16
  let node_crypto = require("node:crypto");
17
17
  let _geekmidas_db_pg_roles = require("@geekmidas/db/pg/roles");
18
+ let _geekmidas_events_sns_url = require("@geekmidas/events/sns/url");
19
+ _geekmidas_events_sns_url = require_rolldown_runtime.__toESM(_geekmidas_events_sns_url, 1);
20
+ let _geekmidas_events_sqs_url = require("@geekmidas/events/sqs/url");
21
+ _geekmidas_events_sqs_url = require_rolldown_runtime.__toESM(_geekmidas_events_sqs_url, 1);
18
22
  let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
23
+ let _aws_sdk_client_sns = require("@aws-sdk/client-sns");
24
+ let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
19
25
  let pg = require("pg");
20
26
  let yaml = require("yaml");
21
27
  let node_util = require("node:util");
@@ -612,6 +618,39 @@ const GENERATED = `# Generated by gkm from the construct manifest — do not edi
612
618
  # from code; a browser needs the root trusted once.
613
619
  `;
614
620
  //#endregion
621
+ //#region src/reconcile/emulator.ts
622
+ /**
623
+ * The AWS emulator (floci, LocalStack-compatible) the `sns` backend runs on
624
+ * locally — the facts the compose file, the env writer and the applier must
625
+ * agree on.
626
+ *
627
+ * The emulator has one account and answers with deterministic ARNs and queue
628
+ * URLs, which is what lets a connection string be composed before the topic
629
+ * or queue it names has been created.
630
+ */
631
+ /** The emulator's single account. */
632
+ const EMULATOR_ACCOUNT = "000000000000";
633
+ const EMULATOR_REGION = "us-east-1";
634
+ /**
635
+ * The emulator's credential. The `LSIA` prefix is what LocalStack required on
636
+ * issued keys; floci does not care, and keeping it means a project that pins
637
+ * the old image still works.
638
+ */
639
+ const EMULATOR_CREDENTIALS = {
640
+ accessKeyId: "LSIAQAAAAAAVNCBMPNSG",
641
+ secretAccessKey: "geekmidas"
642
+ };
643
+ /** The emulator's address from the host, on the port it was published on. */
644
+ function emulatorEndpoint(port) {
645
+ return `http://localhost:${port}`;
646
+ }
647
+ function emulatorTopicArn(name) {
648
+ return `arn:aws:sns:${EMULATOR_REGION}:${EMULATOR_ACCOUNT}:${name}`;
649
+ }
650
+ function emulatorQueueUrl(name, port) {
651
+ return `${emulatorEndpoint(port)}/${EMULATOR_ACCOUNT}/${name}`;
652
+ }
653
+ //#endregion
615
654
  //#region src/workspace/rootSite.ts
616
655
  /**
617
656
  * Which site the base domain points at — deployed and locally alike.
@@ -1153,10 +1192,8 @@ function brokerEnv(plan, ports) {
1153
1192
  const carrier = plan.resources.find((r) => r.kind === "queue" || r.kind === "topic");
1154
1193
  const publisher = carrier ? urlFor(carrier, plan, ports, "") : plan.workerBroker ? workerBroker(plan, ports) : void 0;
1155
1194
  if (!publisher) return {};
1156
- return {
1157
- EVENT_PUBLISHER_CONNECTION_STRING: publisher,
1158
- EVENT_SUBSCRIBER_CONNECTION_STRING: plan.events === "sns" ? publisher.replace(/^sns:/, "sqs:") : publisher
1159
- };
1195
+ if (plan.events === "sns") return {};
1196
+ return { EVENT_PUBLISHER_CONNECTION_STRING: publisher };
1160
1197
  }
1161
1198
  /**
1162
1199
  * The broker a worker schedules its crons through, when nothing else declared
@@ -1226,7 +1263,7 @@ function urlFor(resource, plan, ports, project, addresses = {}) {
1226
1263
  default: return `http://:${LOCAL_TOKEN$1}@${LOCAL_HOST}:${port}`;
1227
1264
  }
1228
1265
  case "queue":
1229
- case "topic": return broker(plan, port);
1266
+ case "topic": return broker(plan, port, resource);
1230
1267
  default: return;
1231
1268
  }
1232
1269
  }
@@ -1237,7 +1274,7 @@ function urlFor(resource, plan, ports, project, addresses = {}) {
1237
1274
  * same `.publish()` reaches pg-boss here and SQS deployed because the string it
1238
1275
  * was handed said so.
1239
1276
  */
1240
- function broker(plan, port) {
1277
+ function broker(plan, port, resource) {
1241
1278
  switch (plan.events) {
1242
1279
  case "pgboss": {
1243
1280
  const database = plan.resources.find((r) => r.kind === "database");
@@ -1245,7 +1282,23 @@ function broker(plan, port) {
1245
1282
  return `pgboss://${LOCAL_USER$2}:${LOCAL_USER$2}@${LOCAL_HOST}:${port}/${database.name}?schema=${PGBOSS_SCHEMA}`;
1246
1283
  }
1247
1284
  case "rabbitmq": return `rabbitmq://${LOCAL_USER$2}:${LOCAL_USER$2}@${LOCAL_HOST}:${port}?exchange=${RABBITMQ_EXCHANGE}`;
1248
- case "sns": throw new UnprovisionedEventsBackend("sns");
1285
+ case "sns": {
1286
+ if (!resource) throw new UnprovisionedEventsBackend("sns");
1287
+ const endpoint = emulatorEndpoint(port);
1288
+ const address = resource.kind === "topic" ? _geekmidas_events_sns_url.build({
1289
+ topicArn: emulatorTopicArn(resource.name),
1290
+ region: EMULATOR_REGION,
1291
+ endpoint
1292
+ }) : _geekmidas_events_sqs_url.build({
1293
+ queueUrl: emulatorQueueUrl(resource.name, port),
1294
+ region: EMULATOR_REGION,
1295
+ endpoint
1296
+ });
1297
+ const url = new URL(address);
1298
+ url.searchParams.set("accessKeyId", EMULATOR_CREDENTIALS.accessKeyId);
1299
+ url.searchParams.set("secretAccessKey", EMULATOR_CREDENTIALS.secretAccessKey);
1300
+ return url.toString();
1301
+ }
1249
1302
  }
1250
1303
  }
1251
1304
  /** A local Postgres URL, master credential and all. */
@@ -1320,16 +1373,15 @@ function schemaOf(resource, plan) {
1320
1373
  }
1321
1374
  }
1322
1375
  /**
1323
- * A backend whose local addresses cannot be derived yet.
1376
+ * A broker address asked for without the topic or queue it would name.
1324
1377
  *
1325
- * SNS and SQS are addressed by ARN, which means the topic and queue have to be
1326
- * created in LocalStack before a URL can name them. Until that provisioning
1327
- * lands, saying so is better than handing out a string that fails on first use.
1378
+ * On SNS each topic and queue is its own address, so there is no project-wide
1379
+ * one to hand out — a cron's schedule store, say, cannot live there.
1328
1380
  */
1329
1381
  var UnprovisionedEventsBackend = class extends Error {
1330
1382
  backend;
1331
1383
  constructor(backend) {
1332
- super(`The local target cannot derive addresses for '${backend}' yet — its topics and queues are named by ARN, which nothing creates locally. Use 'pgboss' (the default) or 'rabbitmq' for local development.`);
1384
+ super(`'${backend}' has no single broker address: each topic and queue is addressed by its own ARN. Something asked for one without naming the topic or queue.`);
1333
1385
  this.backend = backend;
1334
1386
  this.name = "UnprovisionedEventsBackend";
1335
1387
  }
@@ -1438,20 +1490,23 @@ function appEnvKeys(manifest, appName, runnables = {}) {
1438
1490
  ...declaration.kind === "rest-api" ? declaration.endpoints.flatMap((endpoint) => endpoint.requires ?? []) : []
1439
1491
  ]);
1440
1492
  const edges = [...(0, _geekmidas_manifest.dependenciesOf)(declaration).map((edge) => edge.target), ...runnables[id] ?? []];
1441
- if (declaration.kind === "rest-api" && declaration.endpoints.length === 0) for (const [otherId, other] of Object.entries(manifest)) {
1442
- if (other.kind !== "worker") continue;
1443
- edges.push(...(0, _geekmidas_manifest.dependenciesOf)(other).map((edge) => edge.target), ...runnables[otherId] ?? []);
1444
- keys.add("EVENT_PUBLISHER_CONNECTION_STRING");
1445
- keys.add("EVENT_SUBSCRIBER_CONNECTION_STRING");
1493
+ if (declaration.kind === "rest-api" && declaration.endpoints.length === 0) {
1494
+ const runsWorkers = Object.values(manifest).some((other) => other.kind === "worker");
1495
+ for (const [otherId, other] of Object.entries(manifest)) {
1496
+ if (other.kind !== "worker") continue;
1497
+ edges.push(...(0, _geekmidas_manifest.dependenciesOf)(other).map((edge) => edge.target), ...runnables[otherId] ?? []);
1498
+ keys.add("EVENT_PUBLISHER_CONNECTION_STRING");
1499
+ }
1500
+ if (runsWorkers) for (const other of Object.values(manifest)) {
1501
+ if (other.kind !== "queue" && other.kind !== "topic") continue;
1502
+ for (const key of other.provides ?? []) keys.add(key);
1503
+ }
1446
1504
  }
1447
1505
  for (const id of edges) {
1448
1506
  const target = manifest[id];
1449
1507
  if (!target) continue;
1450
1508
  for (const key of target.provides ?? []) keys.add(key);
1451
- if (target.kind === "queue" || target.kind === "topic") {
1452
- keys.add("EVENT_PUBLISHER_CONNECTION_STRING");
1453
- keys.add("EVENT_SUBSCRIBER_CONNECTION_STRING");
1454
- }
1509
+ if (target.kind === "queue" || target.kind === "topic") keys.add("EVENT_PUBLISHER_CONNECTION_STRING");
1455
1510
  if (target.kind === "objects") {
1456
1511
  keys.add("AWS_ACCESS_KEY_ID");
1457
1512
  keys.add("AWS_SECRET_ACCESS_KEY");
@@ -1577,6 +1632,40 @@ function bucketClient(port) {
1577
1632
  }
1578
1633
  };
1579
1634
  }
1635
+ /** A topic and queue client pointed at the local AWS emulator. */
1636
+ function carrierClient(port) {
1637
+ const config = {
1638
+ region: EMULATOR_REGION,
1639
+ endpoint: emulatorEndpoint(port),
1640
+ credentials: EMULATOR_CREDENTIALS
1641
+ };
1642
+ const sns = new _aws_sdk_client_sns.SNSClient(config);
1643
+ const sqs = new _aws_sdk_client_sqs.SQSClient(config);
1644
+ return {
1645
+ async topicExists(name) {
1646
+ try {
1647
+ await sns.send(new _aws_sdk_client_sns.GetTopicAttributesCommand({ TopicArn: emulatorTopicArn(name) }));
1648
+ return true;
1649
+ } catch {
1650
+ return false;
1651
+ }
1652
+ },
1653
+ async createTopic(name) {
1654
+ await sns.send(new _aws_sdk_client_sns.CreateTopicCommand({ Name: name }));
1655
+ },
1656
+ async queueExists(name) {
1657
+ try {
1658
+ await sqs.send(new _aws_sdk_client_sqs.GetQueueUrlCommand({ QueueName: name }));
1659
+ return true;
1660
+ } catch {
1661
+ return false;
1662
+ }
1663
+ },
1664
+ async createQueue(name) {
1665
+ await sqs.send(new _aws_sdk_client_sqs.CreateQueueCommand({ QueueName: name }));
1666
+ }
1667
+ };
1668
+ }
1580
1669
  //#endregion
1581
1670
  //#region src/reconcile/compose.ts
1582
1671
  /**
@@ -1815,18 +1904,19 @@ function define(container, image, published) {
1815
1904
  image,
1816
1905
  restart: "unless-stopped",
1817
1906
  ports: published,
1907
+ extra_hosts: ["host.docker.internal:host-gateway"],
1818
1908
  environment: {
1819
- AWS_DEFAULT_REGION: "us-east-1",
1820
- AWS_ACCESS_KEY_ID: "LSIAQAAAAAAVNCBMPNSG",
1821
- AWS_SECRET_ACCESS_KEY: LOCAL_USER
1909
+ AWS_DEFAULT_REGION: EMULATOR_REGION,
1910
+ AWS_ACCESS_KEY_ID: EMULATOR_CREDENTIALS.accessKeyId,
1911
+ AWS_SECRET_ACCESS_KEY: EMULATOR_CREDENTIALS.secretAccessKey
1822
1912
  },
1823
1913
  volumes: ["localstack-data:/var/lib/localstack"],
1824
1914
  healthcheck: {
1825
1915
  test: [
1826
1916
  "CMD",
1827
- "curl",
1828
- "-f",
1829
- "http://localhost:4566/_localstack/health"
1917
+ "bash",
1918
+ "-c",
1919
+ "exec 3<>/dev/tcp/localhost/4566"
1830
1920
  ],
1831
1921
  interval: "10s",
1832
1922
  timeout: "5s",
@@ -2341,6 +2431,47 @@ function bucketPolicy(bucket, open) {
2341
2431
  });
2342
2432
  }
2343
2433
  /**
2434
+ * The topics and queues to create on the AWS emulator.
2435
+ *
2436
+ * Only on the `sns` backend: pg-boss makes its own tables on first connect and
2437
+ * a RabbitMQ exchange is declared by the client, but an SNS topic and an SQS
2438
+ * queue must exist before anything can publish to them.
2439
+ */
2440
+ function carrierNames(plan) {
2441
+ if (plan.events !== "sns") return {
2442
+ topics: [],
2443
+ queues: []
2444
+ };
2445
+ const named = (kind) => plan.resources.filter((r) => r.kind === kind).map((r) => r.name);
2446
+ return {
2447
+ topics: named("topic"),
2448
+ queues: named("queue")
2449
+ };
2450
+ }
2451
+ /** Create each topic and queue the plan names, skipping those that exist. */
2452
+ async function applyCarriers(client, carriers) {
2453
+ const applied = [];
2454
+ for (const name of carriers.topics) {
2455
+ const created = !await client.topicExists(name);
2456
+ if (created) await client.createTopic(name);
2457
+ applied.push({
2458
+ id: name,
2459
+ describe: `topic ${name}`,
2460
+ created
2461
+ });
2462
+ }
2463
+ for (const name of carriers.queues) {
2464
+ const created = !await client.queueExists(name);
2465
+ if (created) await client.createQueue(name);
2466
+ applied.push({
2467
+ id: name,
2468
+ describe: `queue ${name}`,
2469
+ created
2470
+ });
2471
+ }
2472
+ return applied;
2473
+ }
2474
+ /**
2344
2475
  * Apply each served bucket's open-path policy, skipping those already correct.
2345
2476
  *
2346
2477
  * Compared as parsed JSON rather than as text: the same policy re-serialised
@@ -2525,7 +2656,7 @@ const CADDY_ROOT_IN_CONTAINER = "/data/caddy/pki/authorities/local/root.crt";
2525
2656
  * hash and one health check.
2526
2657
  */
2527
2658
  async function reconcile(options) {
2528
- const { root, project, manifest, stage, start = true, provision = true, docker = dockerCli, probe = isPortFree, sql = pgClient, buckets = bucketClient } = options;
2659
+ const { root, project, manifest, stage, start = true, provision = true, docker = dockerCli, probe = isPortFree, sql = pgClient, buckets = bucketClient, carriers = carrierClient } = options;
2529
2660
  const plan = planFor(manifest, stage, (0, _geekmidas_manifest.provisionOrder)(manifest), {
2530
2661
  localStage: options.localStage,
2531
2662
  events: options.events,
@@ -2587,7 +2718,7 @@ async function reconcile(options) {
2587
2718
  if (start && plan.containers.length > 0) await docker.up(composePath, plan.containers);
2588
2719
  if (start && plan.containers.includes("caddy")) await docker.reload(composePath, "caddy").catch(() => {});
2589
2720
  if (start && plan.containers.includes("caddy")) await docker.copyOut(composePath, "caddy", CADDY_ROOT_IN_CONTAINER, (0, node_path.join)(root, LOCAL_CA_PATH)).catch(() => {});
2590
- const provisioned = start && provision ? await create(plan, ports, sql, buckets, project) : [];
2721
+ const provisioned = start && provision ? await create(plan, ports, sql, buckets, carriers, project) : [];
2591
2722
  await saveState(root, {
2592
2723
  hash,
2593
2724
  stage
@@ -2599,12 +2730,12 @@ async function reconcile(options) {
2599
2730
  };
2600
2731
  }
2601
2732
  /**
2602
- * Create the databases, schemas, and buckets the plan names.
2733
+ * Create the databases, schemas, buckets, topics and queues the plan names.
2603
2734
  *
2604
2735
  * Idempotent throughout, so this runs on every non-converged reconcile rather
2605
2736
  * than being something to remember.
2606
2737
  */
2607
- async function create(plan, ports, sql, buckets, project) {
2738
+ async function create(plan, ports, sql, buckets, emulatorCarriers, project) {
2608
2739
  const applied = [];
2609
2740
  const postgresPort = ports[primaryPortKey("postgres")];
2610
2741
  const statements = postgresStatements(plan, project);
@@ -2617,6 +2748,9 @@ async function create(plan, ports, sql, buckets, project) {
2617
2748
  if (names.length > 0) applied.push(...await applyBuckets(client, names));
2618
2749
  if (policies.length > 0) applied.push(...await applyPolicies(client, policies));
2619
2750
  }
2751
+ const emulatorPort = ports[primaryPortKey("localstack")];
2752
+ const carriers = carrierNames(plan);
2753
+ if (emulatorPort !== void 0 && carriers.topics.length + carriers.queues.length > 0) applied.push(...await applyCarriers(emulatorCarriers(emulatorPort), carriers));
2620
2754
  return applied;
2621
2755
  }
2622
2756
  /**
@@ -2982,4 +3116,4 @@ Object.defineProperty(exports, "workspace_exports", {
2982
3116
  }
2983
3117
  });
2984
3118
 
2985
- //# sourceMappingURL=workspace-DZ1G4zow.cjs.map
3119
+ //# sourceMappingURL=workspace-BIqeeqcR.cjs.map