@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 @@ import { c as loadWorkspaceAppInfo, f as appKey, l as loadWorkspaceConfig, r as
4
4
  import { E as TEST_STAGE, t as allConstructGlobs } from "./workspace-DOmIhiau.mjs";
5
5
  import { i as toEmbeddableSecrets } from "./storage-BzaUxmqe.mjs";
6
6
  import { i as FileSecretsStore, n as secretsStoreFor } from "./store-DeGATCS-.mjs";
7
- import { c as readFakes } from "./ready-BXmLS4VB.mjs";
7
+ import { c as readFakes } from "./ready-D8UXsP6q.mjs";
8
8
  import { existsSync } from "node:fs";
9
9
  import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
10
10
  import { networkInterfaces } from "node:os";
@@ -15,7 +15,11 @@ import { createServer } from "node:net";
15
15
  import { config } from "dotenv";
16
16
  import { createHash } from "node:crypto";
17
17
  import { ownerRole, readerRole, roleStatements } from "@geekmidas/db/pg/roles";
18
+ import * as snsUrl from "@geekmidas/events/sns/url";
19
+ import * as sqsUrl from "@geekmidas/events/sqs/url";
18
20
  import { CreateBucketCommand, GetBucketPolicyCommand, HeadBucketCommand, PutBucketPolicyCommand, S3Client } from "@aws-sdk/client-s3";
21
+ import { CreateTopicCommand, GetTopicAttributesCommand, SNSClient } from "@aws-sdk/client-sns";
22
+ import { CreateQueueCommand, GetQueueUrlCommand, SQSClient } from "@aws-sdk/client-sqs";
19
23
  import { Client } from "pg";
20
24
  import { stringify } from "yaml";
21
25
  import { promisify } from "node:util";
@@ -612,6 +616,39 @@ const GENERATED = `# Generated by gkm from the construct manifest — do not edi
612
616
  # from code; a browser needs the root trusted once.
613
617
  `;
614
618
  //#endregion
619
+ //#region src/reconcile/emulator.ts
620
+ /**
621
+ * The AWS emulator (floci, LocalStack-compatible) the `sns` backend runs on
622
+ * locally — the facts the compose file, the env writer and the applier must
623
+ * agree on.
624
+ *
625
+ * The emulator has one account and answers with deterministic ARNs and queue
626
+ * URLs, which is what lets a connection string be composed before the topic
627
+ * or queue it names has been created.
628
+ */
629
+ /** The emulator's single account. */
630
+ const EMULATOR_ACCOUNT = "000000000000";
631
+ const EMULATOR_REGION = "us-east-1";
632
+ /**
633
+ * The emulator's credential. The `LSIA` prefix is what LocalStack required on
634
+ * issued keys; floci does not care, and keeping it means a project that pins
635
+ * the old image still works.
636
+ */
637
+ const EMULATOR_CREDENTIALS = {
638
+ accessKeyId: "LSIAQAAAAAAVNCBMPNSG",
639
+ secretAccessKey: "geekmidas"
640
+ };
641
+ /** The emulator's address from the host, on the port it was published on. */
642
+ function emulatorEndpoint(port) {
643
+ return `http://localhost:${port}`;
644
+ }
645
+ function emulatorTopicArn(name) {
646
+ return `arn:aws:sns:${EMULATOR_REGION}:${EMULATOR_ACCOUNT}:${name}`;
647
+ }
648
+ function emulatorQueueUrl(name, port) {
649
+ return `${emulatorEndpoint(port)}/${EMULATOR_ACCOUNT}/${name}`;
650
+ }
651
+ //#endregion
615
652
  //#region src/workspace/rootSite.ts
616
653
  /**
617
654
  * Which site the base domain points at — deployed and locally alike.
@@ -1153,10 +1190,8 @@ function brokerEnv(plan, ports) {
1153
1190
  const carrier = plan.resources.find((r) => r.kind === "queue" || r.kind === "topic");
1154
1191
  const publisher = carrier ? urlFor(carrier, plan, ports, "") : plan.workerBroker ? workerBroker(plan, ports) : void 0;
1155
1192
  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
- };
1193
+ if (plan.events === "sns") return {};
1194
+ return { EVENT_PUBLISHER_CONNECTION_STRING: publisher };
1160
1195
  }
1161
1196
  /**
1162
1197
  * The broker a worker schedules its crons through, when nothing else declared
@@ -1226,7 +1261,7 @@ function urlFor(resource, plan, ports, project, addresses = {}) {
1226
1261
  default: return `http://:${LOCAL_TOKEN$1}@${LOCAL_HOST}:${port}`;
1227
1262
  }
1228
1263
  case "queue":
1229
- case "topic": return broker(plan, port);
1264
+ case "topic": return broker(plan, port, resource);
1230
1265
  default: return;
1231
1266
  }
1232
1267
  }
@@ -1237,7 +1272,7 @@ function urlFor(resource, plan, ports, project, addresses = {}) {
1237
1272
  * same `.publish()` reaches pg-boss here and SQS deployed because the string it
1238
1273
  * was handed said so.
1239
1274
  */
1240
- function broker(plan, port) {
1275
+ function broker(plan, port, resource) {
1241
1276
  switch (plan.events) {
1242
1277
  case "pgboss": {
1243
1278
  const database = plan.resources.find((r) => r.kind === "database");
@@ -1245,7 +1280,23 @@ function broker(plan, port) {
1245
1280
  return `pgboss://${LOCAL_USER$2}:${LOCAL_USER$2}@${LOCAL_HOST}:${port}/${database.name}?schema=${PGBOSS_SCHEMA}`;
1246
1281
  }
1247
1282
  case "rabbitmq": return `rabbitmq://${LOCAL_USER$2}:${LOCAL_USER$2}@${LOCAL_HOST}:${port}?exchange=${RABBITMQ_EXCHANGE}`;
1248
- case "sns": throw new UnprovisionedEventsBackend("sns");
1283
+ case "sns": {
1284
+ if (!resource) throw new UnprovisionedEventsBackend("sns");
1285
+ const endpoint = emulatorEndpoint(port);
1286
+ const address = resource.kind === "topic" ? snsUrl.build({
1287
+ topicArn: emulatorTopicArn(resource.name),
1288
+ region: EMULATOR_REGION,
1289
+ endpoint
1290
+ }) : sqsUrl.build({
1291
+ queueUrl: emulatorQueueUrl(resource.name, port),
1292
+ region: EMULATOR_REGION,
1293
+ endpoint
1294
+ });
1295
+ const url = new URL(address);
1296
+ url.searchParams.set("accessKeyId", EMULATOR_CREDENTIALS.accessKeyId);
1297
+ url.searchParams.set("secretAccessKey", EMULATOR_CREDENTIALS.secretAccessKey);
1298
+ return url.toString();
1299
+ }
1249
1300
  }
1250
1301
  }
1251
1302
  /** A local Postgres URL, master credential and all. */
@@ -1320,16 +1371,15 @@ function schemaOf(resource, plan) {
1320
1371
  }
1321
1372
  }
1322
1373
  /**
1323
- * A backend whose local addresses cannot be derived yet.
1374
+ * A broker address asked for without the topic or queue it would name.
1324
1375
  *
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.
1376
+ * On SNS each topic and queue is its own address, so there is no project-wide
1377
+ * one to hand out — a cron's schedule store, say, cannot live there.
1328
1378
  */
1329
1379
  var UnprovisionedEventsBackend = class extends Error {
1330
1380
  backend;
1331
1381
  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.`);
1382
+ 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
1383
  this.backend = backend;
1334
1384
  this.name = "UnprovisionedEventsBackend";
1335
1385
  }
@@ -1438,20 +1488,23 @@ function appEnvKeys(manifest, appName, runnables = {}) {
1438
1488
  ...declaration.kind === "rest-api" ? declaration.endpoints.flatMap((endpoint) => endpoint.requires ?? []) : []
1439
1489
  ]);
1440
1490
  const edges = [...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(...dependenciesOf(other).map((edge) => edge.target), ...runnables[otherId] ?? []);
1444
- keys.add("EVENT_PUBLISHER_CONNECTION_STRING");
1445
- keys.add("EVENT_SUBSCRIBER_CONNECTION_STRING");
1491
+ if (declaration.kind === "rest-api" && declaration.endpoints.length === 0) {
1492
+ const runsWorkers = Object.values(manifest).some((other) => other.kind === "worker");
1493
+ for (const [otherId, other] of Object.entries(manifest)) {
1494
+ if (other.kind !== "worker") continue;
1495
+ edges.push(...dependenciesOf(other).map((edge) => edge.target), ...runnables[otherId] ?? []);
1496
+ keys.add("EVENT_PUBLISHER_CONNECTION_STRING");
1497
+ }
1498
+ if (runsWorkers) for (const other of Object.values(manifest)) {
1499
+ if (other.kind !== "queue" && other.kind !== "topic") continue;
1500
+ for (const key of other.provides ?? []) keys.add(key);
1501
+ }
1446
1502
  }
1447
1503
  for (const id of edges) {
1448
1504
  const target = manifest[id];
1449
1505
  if (!target) continue;
1450
1506
  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
- }
1507
+ if (target.kind === "queue" || target.kind === "topic") keys.add("EVENT_PUBLISHER_CONNECTION_STRING");
1455
1508
  if (target.kind === "objects") {
1456
1509
  keys.add("AWS_ACCESS_KEY_ID");
1457
1510
  keys.add("AWS_SECRET_ACCESS_KEY");
@@ -1577,6 +1630,40 @@ function bucketClient(port) {
1577
1630
  }
1578
1631
  };
1579
1632
  }
1633
+ /** A topic and queue client pointed at the local AWS emulator. */
1634
+ function carrierClient(port) {
1635
+ const config = {
1636
+ region: EMULATOR_REGION,
1637
+ endpoint: emulatorEndpoint(port),
1638
+ credentials: EMULATOR_CREDENTIALS
1639
+ };
1640
+ const sns = new SNSClient(config);
1641
+ const sqs = new SQSClient(config);
1642
+ return {
1643
+ async topicExists(name) {
1644
+ try {
1645
+ await sns.send(new GetTopicAttributesCommand({ TopicArn: emulatorTopicArn(name) }));
1646
+ return true;
1647
+ } catch {
1648
+ return false;
1649
+ }
1650
+ },
1651
+ async createTopic(name) {
1652
+ await sns.send(new CreateTopicCommand({ Name: name }));
1653
+ },
1654
+ async queueExists(name) {
1655
+ try {
1656
+ await sqs.send(new GetQueueUrlCommand({ QueueName: name }));
1657
+ return true;
1658
+ } catch {
1659
+ return false;
1660
+ }
1661
+ },
1662
+ async createQueue(name) {
1663
+ await sqs.send(new CreateQueueCommand({ QueueName: name }));
1664
+ }
1665
+ };
1666
+ }
1580
1667
  //#endregion
1581
1668
  //#region src/reconcile/compose.ts
1582
1669
  /**
@@ -1815,18 +1902,19 @@ function define(container, image, published) {
1815
1902
  image,
1816
1903
  restart: "unless-stopped",
1817
1904
  ports: published,
1905
+ extra_hosts: ["host.docker.internal:host-gateway"],
1818
1906
  environment: {
1819
- AWS_DEFAULT_REGION: "us-east-1",
1820
- AWS_ACCESS_KEY_ID: "LSIAQAAAAAAVNCBMPNSG",
1821
- AWS_SECRET_ACCESS_KEY: LOCAL_USER
1907
+ AWS_DEFAULT_REGION: EMULATOR_REGION,
1908
+ AWS_ACCESS_KEY_ID: EMULATOR_CREDENTIALS.accessKeyId,
1909
+ AWS_SECRET_ACCESS_KEY: EMULATOR_CREDENTIALS.secretAccessKey
1822
1910
  },
1823
1911
  volumes: ["localstack-data:/var/lib/localstack"],
1824
1912
  healthcheck: {
1825
1913
  test: [
1826
1914
  "CMD",
1827
- "curl",
1828
- "-f",
1829
- "http://localhost:4566/_localstack/health"
1915
+ "bash",
1916
+ "-c",
1917
+ "exec 3<>/dev/tcp/localhost/4566"
1830
1918
  ],
1831
1919
  interval: "10s",
1832
1920
  timeout: "5s",
@@ -2341,6 +2429,47 @@ function bucketPolicy(bucket, open) {
2341
2429
  });
2342
2430
  }
2343
2431
  /**
2432
+ * The topics and queues to create on the AWS emulator.
2433
+ *
2434
+ * Only on the `sns` backend: pg-boss makes its own tables on first connect and
2435
+ * a RabbitMQ exchange is declared by the client, but an SNS topic and an SQS
2436
+ * queue must exist before anything can publish to them.
2437
+ */
2438
+ function carrierNames(plan) {
2439
+ if (plan.events !== "sns") return {
2440
+ topics: [],
2441
+ queues: []
2442
+ };
2443
+ const named = (kind) => plan.resources.filter((r) => r.kind === kind).map((r) => r.name);
2444
+ return {
2445
+ topics: named("topic"),
2446
+ queues: named("queue")
2447
+ };
2448
+ }
2449
+ /** Create each topic and queue the plan names, skipping those that exist. */
2450
+ async function applyCarriers(client, carriers) {
2451
+ const applied = [];
2452
+ for (const name of carriers.topics) {
2453
+ const created = !await client.topicExists(name);
2454
+ if (created) await client.createTopic(name);
2455
+ applied.push({
2456
+ id: name,
2457
+ describe: `topic ${name}`,
2458
+ created
2459
+ });
2460
+ }
2461
+ for (const name of carriers.queues) {
2462
+ const created = !await client.queueExists(name);
2463
+ if (created) await client.createQueue(name);
2464
+ applied.push({
2465
+ id: name,
2466
+ describe: `queue ${name}`,
2467
+ created
2468
+ });
2469
+ }
2470
+ return applied;
2471
+ }
2472
+ /**
2344
2473
  * Apply each served bucket's open-path policy, skipping those already correct.
2345
2474
  *
2346
2475
  * Compared as parsed JSON rather than as text: the same policy re-serialised
@@ -2525,7 +2654,7 @@ const CADDY_ROOT_IN_CONTAINER = "/data/caddy/pki/authorities/local/root.crt";
2525
2654
  * hash and one health check.
2526
2655
  */
2527
2656
  async function reconcile(options) {
2528
- const { root, project, manifest, stage, start = true, provision = true, docker = dockerCli, probe = isPortFree, sql = pgClient, buckets = bucketClient } = options;
2657
+ const { root, project, manifest, stage, start = true, provision = true, docker = dockerCli, probe = isPortFree, sql = pgClient, buckets = bucketClient, carriers = carrierClient } = options;
2529
2658
  const plan = planFor(manifest, stage, provisionOrder(manifest), {
2530
2659
  localStage: options.localStage,
2531
2660
  events: options.events,
@@ -2587,7 +2716,7 @@ async function reconcile(options) {
2587
2716
  if (start && plan.containers.length > 0) await docker.up(composePath, plan.containers);
2588
2717
  if (start && plan.containers.includes("caddy")) await docker.reload(composePath, "caddy").catch(() => {});
2589
2718
  if (start && plan.containers.includes("caddy")) await docker.copyOut(composePath, "caddy", CADDY_ROOT_IN_CONTAINER, join(root, LOCAL_CA_PATH)).catch(() => {});
2590
- const provisioned = start && provision ? await create(plan, ports, sql, buckets, project) : [];
2719
+ const provisioned = start && provision ? await create(plan, ports, sql, buckets, carriers, project) : [];
2591
2720
  await saveState(root, {
2592
2721
  hash,
2593
2722
  stage
@@ -2599,12 +2728,12 @@ async function reconcile(options) {
2599
2728
  };
2600
2729
  }
2601
2730
  /**
2602
- * Create the databases, schemas, and buckets the plan names.
2731
+ * Create the databases, schemas, buckets, topics and queues the plan names.
2603
2732
  *
2604
2733
  * Idempotent throughout, so this runs on every non-converged reconcile rather
2605
2734
  * than being something to remember.
2606
2735
  */
2607
- async function create(plan, ports, sql, buckets, project) {
2736
+ async function create(plan, ports, sql, buckets, emulatorCarriers, project) {
2608
2737
  const applied = [];
2609
2738
  const postgresPort = ports[primaryPortKey("postgres")];
2610
2739
  const statements = postgresStatements(plan, project);
@@ -2617,6 +2746,9 @@ async function create(plan, ports, sql, buckets, project) {
2617
2746
  if (names.length > 0) applied.push(...await applyBuckets(client, names));
2618
2747
  if (policies.length > 0) applied.push(...await applyPolicies(client, policies));
2619
2748
  }
2749
+ const emulatorPort = ports[primaryPortKey("localstack")];
2750
+ const carriers = carrierNames(plan);
2751
+ if (emulatorPort !== void 0 && carriers.topics.length + carriers.queues.length > 0) applied.push(...await applyCarriers(emulatorCarriers(emulatorPort), carriers));
2620
2752
  return applied;
2621
2753
  }
2622
2754
  /**
@@ -2821,4 +2953,4 @@ function localAddress(port) {
2821
2953
  //#endregion
2822
2954
  export { findAvailablePort as C, prepareEntryCredentials as D, loadSecretsForApp as E, createEntryWrapper as S, loadEnvFiles as T, cacheBackendFor as _, reconcileWorkspace as a, storageBackendFor as b, LOCAL_CA_PATH as c, quoteIdentifier as d, pgClient as f, primaryPortKey as g, fakeKey as h, derivedContainers as i, forgetState as l, rootSite as m, backendsOf as n, workspace_exports as o, resourceName as p, constructGlobs as r, COMPOSE_PATH as s, FAKE_ENV as t, applyPostgres as u, eventsBackendFor as v, isPortAvailable as w, createCredentialsPreload as x, providerOf as y };
2823
2955
 
2824
- //# sourceMappingURL=workspace-CrFbItPz.mjs.map
2956
+ //# sourceMappingURL=workspace-DwS1JK5u.mjs.map