@deployfoundation/foundation-deploy 0.2.51 → 0.2.53

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/README.md CHANGED
@@ -54,9 +54,12 @@ bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
54
54
  | `github-app-create` | Create the instance's GitHub App from the shipped manifest template. |
55
55
  | `slack-manifest` | Print the instance's Slack app manifest as JSON (the Slack CLI's `get-manifest` hook contract). |
56
56
  | `stage-customization` | Validate and stage a tenant-owned runtime config. |
57
+ | `crm-key` | Write the company CRM's `crm/1` key, piped in with `--stdin`, into `<secretPrefix>/crm/http` (`crm.provider` kind `http`). |
58
+ | `crm-check` | Run the `crm/1` conformance suite: the read cases against the instance's CRM with its stored key, or, with `--base-url --tenant --key-stdin` and no instance, any server, write cases included. |
59
+ | `crm-replay` | Replay the agent's CRM writes (JSONL) into the instance's own table after a rollback to the DynamoDB provider. |
57
60
 
58
61
  `--instance <path>` is required for every command (or `FOUNDATION_INSTANCE_FILE`
59
- in a pipeline). There is no instance *name*, no `instances/` directory and no
62
+ in a pipeline), except `crm-check --base-url`, which checks a server rather than a deployment. There is no instance *name*, no `instances/` directory and no
60
63
  default: a deploy that guessed its target would be a deploy into the wrong
61
64
  account.
62
65
 
package/dist/bin/app.js CHANGED
@@ -8,14 +8,17 @@ import {
8
8
  FoundationPages,
9
9
  FoundationPipeline,
10
10
  NewsletterStack
11
- } from "../chunk-efqdhd2e.js";
11
+ } from "../chunk-spq5kr1t.js";
12
+ import"../chunk-pxhftba1.js";
13
+ import"../chunk-dz91avrb.js";
12
14
  import {
13
15
  adminsFor,
14
16
  instanceNames,
15
17
  loadInstanceFile,
16
18
  provisionsIntegration
17
- } from "../chunk-gnz747z2.js";
18
- import"../chunk-dz91avrb.js";
19
+ } from "../chunk-fjdgxw8h.js";
20
+ import"../chunk-96fjaysn.js";
21
+ import"../chunk-5wgwscd0.js";
19
22
 
20
23
  // bin/app.ts
21
24
  import * as cdk from "aws-cdk-lib";
@@ -1,297 +1,74 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
3
  import {
4
- BASE_SLACK_EVENTS,
5
- CAPABILITY_IDS,
6
- CUSTOMIZATION_ARTIFACT_NAME,
7
4
  RELEASE_BUCKET_ENV,
8
5
  RELEASE_VERSION_RE,
6
+ manifestKey,
7
+ releaseCacheDir,
8
+ skillsKey,
9
+ verifyManifest
10
+ } from "../chunk-pxhftba1.js";
11
+ import {
12
+ DEFAULT_RELEASE_BUCKET,
13
+ expectedKeyArn
14
+ } from "../chunk-dz91avrb.js";
15
+ import {
16
+ argv,
17
+ aws,
18
+ awsContext,
19
+ awsMutate,
20
+ callerAccountId,
21
+ cdkEnv,
22
+ createSecretString,
23
+ instanceBanner,
24
+ loadInstanceContext,
25
+ putSecretJson,
26
+ putSecretString,
27
+ readSecretJson,
28
+ readSecretString,
29
+ resolveInstanceFilePath,
30
+ run,
31
+ runCapture,
32
+ secretExists,
33
+ stackExists,
34
+ stackOutput
35
+ } from "../chunk-nvsj7bbw.js";
36
+ import {
37
+ CUSTOMIZATION_ARTIFACT_NAME,
9
38
  instanceNames,
39
+ mongoCollectionPolicyEnv,
40
+ slackCommandPrefix,
41
+ versionedConfigKey
42
+ } from "../chunk-fjdgxw8h.js";
43
+ import"../chunk-m76jt5rc.js";
44
+ import"../chunk-3614v9rr.js";
45
+ import {
46
+ BASE_SLACK_EVENTS,
47
+ CAPABILITY_IDS,
10
48
  isScheduledJob,
11
49
  isWebhookJob,
12
50
  jobWebhookKey,
13
- loadInstanceFile,
14
- manifestKey,
15
51
  parseInstanceConfig,
16
52
  parseSchedule,
17
- releaseCacheDir,
18
53
  requiredGithubPermissions,
19
54
  requiredSlackCommands,
20
- requiredSlackScopes,
21
- skillsKey,
22
- slackCommandPrefix,
23
- verifyManifest,
24
- versionedConfigKey
25
- } from "../chunk-gnz747z2.js";
55
+ requiredSlackScopes
56
+ } from "../chunk-96fjaysn.js";
26
57
  import {
27
- DEFAULT_RELEASE_BUCKET,
28
- expectedKeyArn
29
- } from "../chunk-dz91avrb.js";
58
+ __require
59
+ } from "../chunk-5wgwscd0.js";
30
60
 
31
61
  // src/deploy/cli.ts
32
62
  import { readFileSync as readFileSync10 } from "node:fs";
33
63
 
34
- // src/deploy/aws.ts
35
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
36
- import { tmpdir } from "node:os";
37
- import { join } from "node:path";
38
-
39
- // src/deploy/instance.ts
40
- import { dirname } from "node:path";
41
- var INSTANCE_FILE_ENV = "FOUNDATION_INSTANCE_FILE";
42
- function resolveInstanceFilePath(argv = [], env = {}) {
43
- for (let i = 0;i < argv.length; i++) {
44
- const arg = argv[i] ?? "";
45
- if (arg === "--instance") {
46
- const next = argv[i + 1];
47
- if (next === undefined || next.startsWith("-"))
48
- throw new Error("--instance needs a path, e.g. --instance .foundation/instance.yaml");
49
- return next;
50
- }
51
- if (arg.startsWith("--instance=")) {
52
- const value = arg.slice("--instance=".length);
53
- if (value === "")
54
- throw new Error("--instance needs a path");
55
- return value;
56
- }
57
- }
58
- const fromEnv = env[INSTANCE_FILE_ENV];
59
- if (fromEnv !== undefined && fromEnv !== "")
60
- return fromEnv;
61
- throw new Error(`--instance <path> is required (or set ${INSTANCE_FILE_ENV}); it is the path to the deployment's instance YAML`);
62
- }
63
- function loadInstanceContext(path, cwd = process.cwd()) {
64
- const file = loadInstanceFile(path, cwd);
65
- return { ...file, root: dirname(file.path) };
66
- }
67
- function loadInstanceFromArgs(argv = process.argv.slice(2), env = process.env) {
68
- return loadInstanceContext(resolveInstanceFilePath(argv, env));
69
- }
70
- function instanceBanner(context) {
71
- const { instance } = context;
72
- return `instance: ${instance.name} (account ${instance.aws.account}, region ${instance.aws.region})
73
- file: ${context.path}
74
- config: ${context.configPath}`;
75
- }
76
-
77
- // src/deploy/sh.ts
78
- import { spawn as nodeSpawn } from "node:child_process";
79
- function formatCommand(cmd, env) {
80
- const prefix = Object.entries(env ?? {}).map(([k, v]) => `${k}=${quote(v)}`).join(" ");
81
- const body = cmd.map(quote).join(" ");
82
- return prefix === "" ? body : `${prefix} ${body}`;
83
- }
84
- function quote(value) {
85
- return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
86
- }
87
- function start(cmd, opts, stdout) {
88
- const [bin, ...rest] = cmd;
89
- if (bin === undefined)
90
- throw new Error("run: empty command");
91
- const child = nodeSpawn(bin, rest, {
92
- cwd: opts.cwd,
93
- env: { ...process.env, ...opts.env },
94
- stdio: [
95
- opts.stdin === undefined ? "ignore" : "pipe",
96
- stdout,
97
- stdout === "inherit" ? "inherit" : "pipe"
98
- ]
99
- });
100
- if (opts.stdin !== undefined && child.stdin !== null) {
101
- child.stdin.end(opts.stdin);
102
- }
103
- return child;
104
- }
105
- function exited(child) {
106
- return new Promise((resolve, reject) => {
107
- child.once("error", reject);
108
- child.once("close", (code, signal) => resolve(code ?? (signal === null ? 1 : 128)));
109
- });
110
- }
111
- function collect(stream) {
112
- if (stream === null)
113
- return Promise.resolve("");
114
- return new Promise((resolve, reject) => {
115
- const chunks = [];
116
- stream.on("data", (c) => chunks.push(c));
117
- stream.once("error", reject);
118
- stream.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
119
- });
120
- }
121
- async function run(cmd, opts = {}) {
122
- if (opts.dryRun === true) {
123
- console.log(` $ ${formatCommand(cmd, opts.env)}`);
124
- return;
125
- }
126
- const code = await exited(start(cmd, opts, "inherit"));
127
- if (code !== 0)
128
- throw new Error(`${formatCommand(cmd)} exited ${code}`);
129
- }
130
- async function runCapture(cmd, opts = {}) {
131
- const child = start(cmd, opts, "pipe");
132
- const [out, err, code] = await Promise.all([
133
- collect(child.stdout),
134
- collect(child.stderr),
135
- exited(child)
136
- ]);
137
- if (code !== 0)
138
- throw new Error(`${formatCommand(cmd)} exited ${code}: ${err.trim()}`);
139
- return out.trim();
140
- }
141
-
142
- // src/deploy/aws.ts
143
- function resolveProfile(fallback, explicit, env = process.env) {
144
- if (env.FOUNDATION_NO_PROFILE === "1")
145
- return "";
146
- if (explicit !== undefined)
147
- return explicit;
148
- if (env.AWS_PROFILE !== undefined && env.AWS_PROFILE !== "")
149
- return env.AWS_PROFILE;
150
- if (env.GITHUB_ACTIONS === "true")
151
- return "";
152
- return fallback;
153
- }
154
- function awsContext(overrides = {}) {
155
- const env = overrides.env ?? process.env;
156
- const paths = overrides.paths ?? loadInstanceFromArgs();
157
- const instance = paths.instance;
158
- return {
159
- profile: resolveProfile(instance.aws.profile, overrides.profile, env),
160
- region: overrides.region ?? env.AWS_REGION ?? instance.aws.region,
161
- dryRun: overrides.dryRun ?? false,
162
- instance,
163
- paths,
164
- names: instanceNames(instance)
165
- };
166
- }
167
- function argv(ctx, args) {
168
- const profile = ctx.profile === "" ? [] : ["--profile", ctx.profile];
169
- return ["aws", ...args, ...profile, "--region", ctx.region];
170
- }
171
- function cdkEnv(ctx) {
172
- return {
173
- ...ctx.profile === "" ? {} : { AWS_PROFILE: ctx.profile },
174
- AWS_REGION: ctx.region,
175
- CDK_DEFAULT_REGION: ctx.region
176
- };
177
- }
178
- async function aws(ctx, args, stdin) {
179
- return runCapture(argv(ctx, args), { stdin });
180
- }
181
- async function awsMutate(ctx, args, stdin) {
182
- if (ctx.dryRun === true) {
183
- console.log(` $ ${argv(ctx, args).join(" ")}${stdin === undefined ? "" : " < (stdin)"}`);
184
- return;
185
- }
186
- await run(argv(ctx, args), { stdin });
187
- }
188
- async function stackOutput(ctx, stack, key) {
189
- const value = await aws(ctx, [
190
- "cloudformation",
191
- "describe-stacks",
192
- "--stack-name",
193
- stack,
194
- "--query",
195
- `Stacks[0].Outputs[?OutputKey=='${key}'].OutputValue`,
196
- "--output",
197
- "text"
198
- ]);
199
- if (value === "" || value === "None")
200
- throw new Error(`stack ${stack} has no output ${key} — has it been deployed?`);
201
- return value;
202
- }
203
- async function stackExists(ctx, stack) {
204
- try {
205
- await aws(ctx, [
206
- "cloudformation",
207
- "describe-stacks",
208
- "--stack-name",
209
- stack,
210
- "--output",
211
- "text"
212
- ]);
213
- return true;
214
- } catch {
215
- return false;
216
- }
217
- }
218
- async function secretExists(ctx, secretId) {
219
- try {
220
- await aws(ctx, [
221
- "secretsmanager",
222
- "describe-secret",
223
- "--secret-id",
224
- secretId,
225
- "--output",
226
- "text"
227
- ]);
228
- return true;
229
- } catch {
230
- return false;
231
- }
232
- }
233
- async function readSecretString(ctx, secretId) {
234
- return aws(ctx, [
235
- "secretsmanager",
236
- "get-secret-value",
237
- "--secret-id",
238
- secretId,
239
- "--query",
240
- "SecretString",
241
- "--output",
242
- "text"
243
- ]);
244
- }
245
- async function readSecretJson(ctx, secretId) {
246
- return JSON.parse(await readSecretString(ctx, secretId));
247
- }
248
- async function putSecretString(ctx, secretId, value) {
249
- await throughSecretFile(value, (file) => awsMutate(ctx, [
250
- "secretsmanager",
251
- "put-secret-value",
252
- "--secret-id",
253
- secretId,
254
- "--secret-string",
255
- `file://${file}`
256
- ]));
257
- }
258
- async function createSecretString(ctx, secretId, value, description) {
259
- await throughSecretFile(value, (file) => awsMutate(ctx, [
260
- "secretsmanager",
261
- "create-secret",
262
- "--name",
263
- secretId,
264
- "--description",
265
- description,
266
- "--secret-string",
267
- `file://${file}`
268
- ]));
269
- }
270
- async function throughSecretFile(value, body) {
271
- const dir = mkdtempSync(join(tmpdir(), "foundation-secret-"));
272
- const file = join(dir, "value.json");
273
- try {
274
- writeFileSync(file, value, { mode: 384 });
275
- await body(file);
276
- } finally {
277
- rmSync(dir, { recursive: true, force: true });
278
- }
279
- }
280
- async function putSecretJson(ctx, secretId, value) {
281
- await putSecretString(ctx, secretId, JSON.stringify(value));
282
- }
283
- async function callerAccountId(ctx) {
284
- return aws(ctx, ["sts", "get-caller-identity", "--query", "Account", "--output", "text"]);
285
- }
286
-
287
64
  // src/deploy/config-sync.ts
288
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync } from "node:fs";
289
- import { tmpdir as tmpdir2 } from "node:os";
290
- import { join as join3, resolve as resolve3 } from "node:path";
65
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, readdirSync } from "node:fs";
66
+ import { tmpdir } from "node:os";
67
+ import { join as join2, resolve as resolve3 } from "node:path";
291
68
 
292
69
  // src/deploy/paths.ts
293
70
  import { existsSync, readFileSync } from "node:fs";
294
- import { dirname as dirname2, resolve } from "node:path";
71
+ import { dirname, resolve } from "node:path";
295
72
  import { fileURLToPath } from "node:url";
296
73
  var PACKAGE_NAME = "@deployfoundation/foundation-deploy";
297
74
  function packageRoot(from) {
@@ -305,20 +82,20 @@ function packageRoot(from) {
305
82
  return dir;
306
83
  } catch {}
307
84
  }
308
- const parent = dirname2(dir);
85
+ const parent = dirname(dir);
309
86
  if (parent === dir)
310
87
  throw new Error(`cannot find the ${PACKAGE_NAME} package root above ${from}`);
311
88
  dir = parent;
312
89
  }
313
90
  }
314
- var INFRA_ROOT = packageRoot(dirname2(fileURLToPath(import.meta.url)));
91
+ var INFRA_ROOT = packageRoot(dirname(fileURLToPath(import.meta.url)));
315
92
  var FOUNDATION_ROOT = resolve(INFRA_ROOT, "..", "..");
316
93
  var AGENT_DOCKERFILE = "packages/infra/agent-image/Dockerfile";
317
94
  var PACKAGE_ASSETS = resolve(INFRA_ROOT, "src", "deploy", "assets");
318
95
 
319
96
  // src/deploy/release.ts
320
97
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
321
- import { dirname as dirname3, isAbsolute, join as join2, resolve as resolve2 } from "node:path";
98
+ import { dirname as dirname2, isAbsolute, join, resolve as resolve2 } from "node:path";
322
99
 
323
100
  // src/release/kms.ts
324
101
  import { KMSClient, SignCommand, VerifyCommand } from "@aws-sdk/client-kms";
@@ -357,14 +134,14 @@ function flag(args, name) {
357
134
  return value ?? (inline === undefined ? undefined : inline.slice(name.length + 1));
358
135
  }
359
136
  function toolVersion(infraRoot = INFRA_ROOT) {
360
- const { version } = JSON.parse(readFileSync2(join2(infraRoot, "package.json"), "utf8"));
137
+ const { version } = JSON.parse(readFileSync2(join(infraRoot, "package.json"), "utf8"));
361
138
  return version;
362
139
  }
363
140
  function foundationVersion(release) {
364
141
  return release?.version ?? `v${toolVersion()}`;
365
142
  }
366
143
  function hasFoundationWorkspace(foundationRoot = FOUNDATION_ROOT) {
367
- return existsSync2(join2(foundationRoot, "packages", "gateway", "package.json"));
144
+ return existsSync2(join(foundationRoot, "packages", "gateway", "package.json"));
368
145
  }
369
146
  function releaseRequest(args, options = {}) {
370
147
  const env = options.env ?? process.env;
@@ -387,7 +164,7 @@ function releaseRequest(args, options = {}) {
387
164
  return { version, bucket };
388
165
  }
389
166
  async function s3Download(ctx, bucket, key, dest) {
390
- mkdirSync(dirname3(dest), { recursive: true });
167
+ mkdirSync(dirname2(dest), { recursive: true });
391
168
  await aws(ctx, ["s3", "cp", `s3://${bucket}/${key}`, dest]);
392
169
  }
393
170
  async function verifyRelease(ctx, request, deps = {}) {
@@ -397,7 +174,7 @@ async function verifyRelease(ctx, request, deps = {}) {
397
174
  const local = localManifestRef(request);
398
175
  const version = request.version;
399
176
  const cacheDir = deps.cacheDir ?? releaseCacheDir(version ?? "unversioned", env);
400
- const manifestPath = local ?? join2(cacheDir, "manifest.json");
177
+ const manifestPath = local ?? join(cacheDir, "manifest.json");
401
178
  if (local === undefined) {
402
179
  const source = s3ManifestRef(request);
403
180
  await download(ctx, source.bucket, source.key, manifestPath);
@@ -408,7 +185,7 @@ async function verifyRelease(ctx, request, deps = {}) {
408
185
  ...version === undefined ? {} : { expectVersion: version },
409
186
  ...deps.verifyArtifacts === false ? {} : {
410
187
  readArtifact: async (key) => {
411
- const dest = join2(cacheDir, "artifacts", key.split("/").slice(-1)[0] ?? "artifact");
188
+ const dest = join(cacheDir, "artifacts", key.split("/").slice(-1)[0] ?? "artifact");
412
189
  await download(ctx, request.bucket, key, dest);
413
190
  return readFileSync2(dest);
414
191
  }
@@ -425,7 +202,7 @@ function plannedRelease(request) {
425
202
  return {
426
203
  version,
427
204
  bucket: request.bucket,
428
- manifestPath: local ?? join2(releaseCacheDir(version), "manifest.json")
205
+ manifestPath: local ?? join(releaseCacheDir(version), "manifest.json")
429
206
  };
430
207
  }
431
208
  function localManifestRef(request) {
@@ -524,13 +301,13 @@ async function configSync(ctx, opts = {}) {
524
301
  }
525
302
  async function unpackReleaseSkills(ctx, release) {
526
303
  const key = release.manifest?.skills.key ?? skillsKey(release.version);
527
- const dir = mkdtempSync2(join3(tmpdir2(), "foundation-skills-"));
528
- const tarball = join3(dir, "skills.tar.gz");
304
+ const dir = mkdtempSync(join2(tmpdir(), "foundation-skills-"));
305
+ const tarball = join2(dir, "skills.tar.gz");
529
306
  await awsMutate(ctx, ["s3", "cp", `s3://${release.bucket}/${key}`, tarball]);
530
307
  return await extractSkillsTarball(tarball, dir, { dryRun: ctx.dryRun });
531
308
  }
532
309
  async function extractSkillsTarball(tarball, dir, opts = {}) {
533
- const contents = join3(dir, "skills");
310
+ const contents = join2(dir, "skills");
534
311
  await run(["tar", "-xzf", tarball, "-C", dir], { dryRun: opts.dryRun });
535
312
  if (opts.dryRun !== true && !existsSync3(contents))
536
313
  throw new Error(`${tarball} has no top-level skills/ directory`);
@@ -543,7 +320,7 @@ async function extractSkillsTarball(tarball, dir, opts = {}) {
543
320
  import { createHash } from "node:crypto";
544
321
  import { existsSync as existsSync4 } from "node:fs";
545
322
  import { createRequire } from "node:module";
546
- import { join as join5 } from "node:path";
323
+ import { join as join4 } from "node:path";
547
324
 
548
325
  // ../connectors/src/github.ts
549
326
  var GITHUB_TOKEN_RESPONSE_MAX_BYTES = 128 * 1024;
@@ -773,9 +550,6 @@ function isSafeOpaqueString(value, max) {
773
550
  function isPlainObject(value) {
774
551
  return typeof value === "object" && value !== null && !Array.isArray(value);
775
552
  }
776
- // ../connectors/src/crm.ts
777
- var CRM_MAX_REQUEST_BYTES = 512 * 1024;
778
- var CRM_MAX_RESPONSE_BYTES = 512 * 1024;
779
553
  // ../connectors/src/upwork.ts
780
554
  var UPWORK_MAX_RESPONSE_BYTES = 1024 * 1024;
781
555
  var UPWORK_MAX_ACCESS_TOKEN_TTL_SECONDS = 24 * 60 * 60;
@@ -962,8 +736,8 @@ function adminsCsvFromFile(path) {
962
736
 
963
737
  // src/deploy/endpoint.ts
964
738
  import { mkdtemp, readFile, rm } from "node:fs/promises";
965
- import { tmpdir as tmpdir3 } from "node:os";
966
- import { join as join4 } from "node:path";
739
+ import { tmpdir as tmpdir2 } from "node:os";
740
+ import { join as join3 } from "node:path";
967
741
  function cliRunner(ctx) {
968
742
  return {
969
743
  capture: (args) => aws(ctx, args),
@@ -1111,8 +885,8 @@ async function smokeInvoke(runner, opts) {
1111
885
  await runner.mutate([...args, "/dev/stdout"]);
1112
886
  return;
1113
887
  }
1114
- const dir = await mkdtemp(join4(tmpdir3(), "foundation-smoke-"));
1115
- const outFile = join4(dir, "response.json");
888
+ const dir = await mkdtemp(join3(tmpdir2(), "foundation-smoke-"));
889
+ const outFile = join3(dir, "response.json");
1116
890
  let out;
1117
891
  try {
1118
892
  const printed = await runner.capture([...args, outFile]);
@@ -1443,6 +1217,7 @@ function composeRuntimeSecret(inputs) {
1443
1217
  ...inputs.etradeProxyFunctionArn !== undefined && inputs.etradeProxyFunctionArn !== "" ? { FOUNDATION_ETRADE_PROXY_ARN: inputs.etradeProxyFunctionArn } : {},
1444
1218
  ...inputs.routineIngressFunctionArn !== undefined && inputs.routineIngressFunctionArn !== "" ? { FOUNDATION_ROUTINE_INGRESS_FUNCTION_ARN: inputs.routineIngressFunctionArn } : {},
1445
1219
  FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
1220
+ ...instance.mongodbReadonly.deniedCollections.length > 0 || instance.mongodbReadonly.unsafeReadDefaultDeniedCollections.length > 0 ? { FOUNDATION_MONGODB_COLLECTION_POLICY: mongoCollectionPolicyEnv(instance.mongodbReadonly) } : {},
1446
1221
  FOUNDATION_SLACK_TEAM_ID: slackApp.team_id !== undefined && slackApp.team_id !== "" ? slackApp.team_id : instance.slack.teamId
1447
1222
  };
1448
1223
  }
@@ -1453,11 +1228,11 @@ import {
1453
1228
  mkdirSync as mkdirSync3,
1454
1229
  readFileSync as readFileSync5,
1455
1230
  realpathSync,
1456
- rmSync as rmSync2,
1231
+ rmSync,
1457
1232
  statSync,
1458
- writeFileSync as writeFileSync2
1233
+ writeFileSync
1459
1234
  } from "node:fs";
1460
- import { dirname as dirname4, isAbsolute as isAbsolute2, posix, relative, resolve as resolve4, sep } from "node:path";
1235
+ import { dirname as dirname3, isAbsolute as isAbsolute2, posix, relative, resolve as resolve4, sep } from "node:path";
1461
1236
  import { isDeepStrictEqual } from "node:util";
1462
1237
  function sourceRootFor(sourceDir) {
1463
1238
  if (sourceDir === undefined || sourceDir === "")
@@ -1561,7 +1336,7 @@ function stageCustomization(options) {
1561
1336
  const platformConfigPath = options.paths.configPath;
1562
1337
  const stagedPath = stagedCustomizationPath(options.paths);
1563
1338
  if (options.dryRun !== true)
1564
- rmSync2(stagedPath, { force: true });
1339
+ rmSync(stagedPath, { force: true });
1565
1340
  const customization = instance.customization;
1566
1341
  if (customization === undefined)
1567
1342
  return { status: "not-configured", runtimeConfigPath: platformConfigPath };
@@ -1579,8 +1354,8 @@ function stageCustomization(options) {
1579
1354
  assertValidTenantJobs(tenantConfig);
1580
1355
  assertProtectedPolicy(platformConfig, tenantConfig);
1581
1356
  if (options.dryRun !== true) {
1582
- mkdirSync3(dirname4(stagedPath), { recursive: true });
1583
- writeFileSync2(stagedPath, yaml);
1357
+ mkdirSync3(dirname3(stagedPath), { recursive: true });
1358
+ writeFileSync(stagedPath, yaml);
1584
1359
  }
1585
1360
  return {
1586
1361
  status: "staged",
@@ -1657,10 +1432,10 @@ function cdkCommand() {
1657
1432
  }
1658
1433
  }
1659
1434
  function cdkAppCommand(infraRoot = INFRA_ROOT) {
1660
- const source = join5(infraRoot, "bin", "app.ts");
1435
+ const source = join4(infraRoot, "bin", "app.ts");
1661
1436
  if (existsSync4(source))
1662
1437
  return `bun run ${source}`;
1663
- return `${process.execPath} ${join5(infraRoot, "dist", "bin", "app.js")}`;
1438
+ return `${process.execPath} ${join4(infraRoot, "dist", "bin", "app.js")}`;
1664
1439
  }
1665
1440
  async function cdkDeploy(ctx, opts) {
1666
1441
  const context = [
@@ -1931,9 +1706,9 @@ import { createServer } from "node:http";
1931
1706
 
1932
1707
  // src/deploy/github-app-manifest.ts
1933
1708
  import { readFileSync as readFileSync6 } from "node:fs";
1934
- import { join as join6 } from "node:path";
1709
+ import { join as join5 } from "node:path";
1935
1710
  import { parse as parseYaml } from "yaml";
1936
- var GITHUB_APP_MANIFEST_PATH = join6(PACKAGE_ASSETS, "github-app-manifest.yml");
1711
+ var GITHUB_APP_MANIFEST_PATH = join5(PACKAGE_ASSETS, "github-app-manifest.yml");
1937
1712
  function manifestUrl(instance) {
1938
1713
  const repo = instance.github.defaultRepo ?? instanceNames(instance).defaultRepo;
1939
1714
  return `https://github.com/${repo === "" ? instance.github.org : repo}`;
@@ -2148,9 +1923,9 @@ import { resolve as resolve5 } from "node:path";
2148
1923
 
2149
1924
  // src/deploy/slack-manifest.ts
2150
1925
  import { readFileSync as readFileSync8 } from "node:fs";
2151
- import { join as join7 } from "node:path";
1926
+ import { join as join6 } from "node:path";
2152
1927
  import { parse as parse2 } from "yaml";
2153
- var SLACK_APP_MANIFEST_PATH = join7(PACKAGE_ASSETS, "slack-app-manifest.yml");
1928
+ var SLACK_APP_MANIFEST_PATH = join6(PACKAGE_ASSETS, "slack-app-manifest.yml");
2154
1929
  function buildSlackManifest(template, instance, env) {
2155
1930
  const events = env.EVENTS_REQUEST_URL;
2156
1931
  const commands = env.COMMANDS_REQUEST_URL ?? events?.replace(/\/events$/, "/commands");
@@ -2598,7 +2373,10 @@ var COMMANDS = [
2598
2373
  "github-app-create",
2599
2374
  "slack-manifest",
2600
2375
  "stage-customization",
2601
- "job-key"
2376
+ "job-key",
2377
+ "crm-key",
2378
+ "crm-check",
2379
+ "crm-replay"
2602
2380
  ];
2603
2381
  var USAGE = `usage: foundation-deploy <command> --instance <path> [options]
2604
2382
 
@@ -2617,6 +2395,12 @@ commands
2617
2395
  slack-manifest print the instance's Slack app manifest as JSON
2618
2396
  stage-customization validate and stage a tenant-owned runtime config
2619
2397
  job-key <id> print a webhook job's URL and signing key as JSON
2398
+ crm-key write the company CRM's crm/1 key, read from stdin, into
2399
+ the instance's <secretPrefix>/crm/http secret
2400
+ crm-check run the crm/1 conformance suite against the instance's
2401
+ CRM (crm.provider kind http) with its stored key
2402
+ crm-replay replay the agent's CRM writes (JSONL) into the
2403
+ instance's own table after a rollback to dynamodb
2620
2404
 
2621
2405
  options
2622
2406
  --instance <path> REQUIRED: path to the deployment's instance YAML
@@ -2652,6 +2436,24 @@ job-key options
2652
2436
  --put-secret <id> write {url, key} into this Secrets Manager secret
2653
2437
  (created if absent) instead of printing the key
2654
2438
 
2439
+ crm-key options
2440
+ --stdin REQUIRED: read the key from a pipe, never argv, e.g.
2441
+ pbpaste | foundation-deploy crm-key --instance <path> --stdin
2442
+
2443
+ crm-check options
2444
+ --allow-skips pass despite cases only a person can set up (they need
2445
+ the CRM's own harness); without it they count as failed
2446
+ --base-url <url> check any crm/1 server instead of the instance's: needs
2447
+ --tenant <id> the tenant its /meta names, and the key on stdin
2448
+ --key-stdin (--key-stdin). No --instance in this form.
2449
+ --write with --base-url only: also run the write cases, which
2450
+ create records and spend the week's outreach allowance;
2451
+ point it at a sandbox tenant
2452
+
2453
+ crm-replay options
2454
+ --from <file> REQUIRED: the writes to replay, one JSON object per line
2455
+ (docs/crm-provider-contract.md); --dry-run validates only
2456
+
2655
2457
  github-app-create options
2656
2458
  --org <org> GitHub org (default: the instance's github.org)
2657
2459
  --port <n> local callback port (default 8765)
@@ -2731,6 +2533,21 @@ ${USAGE}`);
2731
2533
  console.log(JSON.stringify(result));
2732
2534
  return 0;
2733
2535
  }
2536
+ if (command === "crm-check" && args.includes("--base-url")) {
2537
+ if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance=")))
2538
+ throw new Error("crm-check takes --instance or --base-url, not both");
2539
+ const baseUrl = flag2(args, "--base-url");
2540
+ const tenantId = flag2(args, "--tenant");
2541
+ if (baseUrl === undefined || tenantId === undefined || !args.includes("--key-stdin"))
2542
+ throw new Error("crm-check --base-url needs --tenant <id> and --key-stdin");
2543
+ const { crmCheckTarget, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2544
+ return crmCheckTarget({
2545
+ baseUrl,
2546
+ tenantId,
2547
+ write: args.includes("--write"),
2548
+ allowSkips: args.includes("--allow-skips")
2549
+ }, defaultCrmCommandDeps());
2550
+ }
2734
2551
  if (command === "github-app-create") {
2735
2552
  const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
2736
2553
  console.log(instanceBanner(paths));
@@ -2795,6 +2612,25 @@ ${USAGE}`);
2795
2612
  console.log(` ${result.status}: ${result.runtimeConfigPath}`);
2796
2613
  return 0;
2797
2614
  }
2615
+ case "crm-key": {
2616
+ const { crmKey, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2617
+ await crmKey(ctx, { stdin: args.includes("--stdin") }, defaultCrmCommandDeps());
2618
+ return 0;
2619
+ }
2620
+ case "crm-check": {
2621
+ if (args.includes("--write"))
2622
+ throw new Error("crm-check --write creates records and spends the week's outreach allowance, so it never runs against the instance's own tenant: use --base-url with a sandbox tenant");
2623
+ const { crmCheck, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2624
+ return crmCheck(ctx, { allowSkips: args.includes("--allow-skips") }, defaultCrmCommandDeps());
2625
+ }
2626
+ case "crm-replay": {
2627
+ const from = flag2(args, "--from");
2628
+ if (from === undefined || from.startsWith("-"))
2629
+ throw new Error("crm-replay needs --from <file>: the writes to replay, as JSONL");
2630
+ const { crmReplay, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
2631
+ const result = await crmReplay(ctx, { from }, defaultCrmCommandDeps());
2632
+ return result.refused.length > 0 ? 1 : 0;
2633
+ }
2798
2634
  default:
2799
2635
  console.error(`unhandled command: ${command}
2800
2636