@deployfoundation/foundation-deploy 0.2.50 → 0.2.52
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 +4 -1
- package/agent-image/Dockerfile +36 -1
- package/dist/bin/app.js +6 -3
- package/dist/bin/foundation-deploy.js +139 -305
- package/dist/bin/release-account.js +1 -0
- package/dist/chunk-3614v9rr.js +907 -0
- package/dist/{chunk-efqdhd2e.js → chunk-59dpqyap.js} +254 -90
- package/dist/chunk-5wgwscd0.js +4 -0
- package/dist/{chunk-gnz747z2.js → chunk-96fjaysn.js} +204 -884
- package/dist/chunk-9zx95xs6.js +259 -0
- package/dist/chunk-jf5rw6h4.js +1117 -0
- package/dist/chunk-m76jt5rc.js +376 -0
- package/dist/chunk-mh1206ne.js +1764 -0
- package/dist/chunk-p7zkm4pp.js +151 -0
- package/dist/chunk-pxhftba1.js +336 -0
- package/dist/chunk-rxetj2p9.js +466 -0
- package/dist/chunk-sgdcmme0.js +301 -0
- package/dist/chunk-tey0gh86.js +38 -0
- package/dist/src/index.js +17 -13
- package/package.json +3 -1
- package/src/deploy/cli.ts +71 -0
- package/src/deploy/crm.ts +491 -0
- package/src/stacks/agent-stack.ts +34 -11
- package/src/stacks/api-stack.ts +340 -87
- package/src/stacks/data-stack.ts +27 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
aws,
|
|
3
|
+
putSecretString,
|
|
4
|
+
readSecretString,
|
|
5
|
+
secretExists
|
|
6
|
+
} from "./chunk-9zx95xs6.js";
|
|
7
|
+
import"./chunk-rxetj2p9.js";
|
|
8
|
+
import {
|
|
9
|
+
CrmIdempotencyConflictError,
|
|
10
|
+
CrmRecordNotFoundError
|
|
11
|
+
} from "./chunk-tey0gh86.js";
|
|
12
|
+
import {
|
|
13
|
+
crmHttpBaseUrl,
|
|
14
|
+
isCrmHttpKey
|
|
15
|
+
} from "./chunk-m76jt5rc.js";
|
|
16
|
+
import {
|
|
17
|
+
CrmPolicyRefusedError
|
|
18
|
+
} from "./chunk-p7zkm4pp.js";
|
|
19
|
+
import {
|
|
20
|
+
CRM_MAX_ID_CHARS,
|
|
21
|
+
CrmInputError,
|
|
22
|
+
parseCrmLogActivityArgs,
|
|
23
|
+
parseCrmUpdateStatusArgs,
|
|
24
|
+
parseCrmUpsertRecordsArgs
|
|
25
|
+
} from "./chunk-3614v9rr.js";
|
|
26
|
+
import {
|
|
27
|
+
crmOutreachConfigured,
|
|
28
|
+
parseInstanceConfig
|
|
29
|
+
} from "./chunk-96fjaysn.js";
|
|
30
|
+
import {
|
|
31
|
+
__require
|
|
32
|
+
} from "./chunk-5wgwscd0.js";
|
|
33
|
+
|
|
34
|
+
// src/deploy/crm.ts
|
|
35
|
+
import { readFileSync } from "node:fs";
|
|
36
|
+
function httpProvider(ctx, command) {
|
|
37
|
+
const { instance } = ctx;
|
|
38
|
+
const provider = instance.crm.provider;
|
|
39
|
+
if (!instance.integrations.crm || provider.kind !== "http")
|
|
40
|
+
throw new Error(`${command} needs crm.provider kind http (and integrations.crm) in ${ctx.paths.path}; the DynamoDB provider has no CRM key or base URL`);
|
|
41
|
+
return provider;
|
|
42
|
+
}
|
|
43
|
+
async function crmKey(ctx, options, deps) {
|
|
44
|
+
httpProvider(ctx, "crm-key");
|
|
45
|
+
const secret = ctx.names.secretCrmHttp;
|
|
46
|
+
if (!options.stdin)
|
|
47
|
+
throw new Error("crm-key reads the key from stdin only, so it never lands in argv or shell history: pipe it in with --stdin, e.g. `pbpaste | foundation-deploy crm-key --instance <path> --stdin`");
|
|
48
|
+
const key = (await deps.readStdin()).trim();
|
|
49
|
+
if (!isCrmHttpKey(key))
|
|
50
|
+
throw new Error("stdin did not hold a usable crm/1 key: 16 to 1024 visible ASCII characters, no spaces, one per run");
|
|
51
|
+
if (!await deps.secretExists(ctx, secret))
|
|
52
|
+
throw new Error(`secret ${secret} does not exist yet: deploy with crm.provider kind http first, which creates it empty`);
|
|
53
|
+
await deps.putSecretString(ctx, secret, JSON.stringify({ api_key: key }));
|
|
54
|
+
deps.log(`wrote the crm/1 key to ${secret}. The CRM proxy reads it within 5 minutes, or at once after a 401. Check it with: foundation-deploy crm-check --instance ${ctx.paths.path}`);
|
|
55
|
+
return { secret };
|
|
56
|
+
}
|
|
57
|
+
async function crmCheck(ctx, options, deps) {
|
|
58
|
+
const provider = httpProvider(ctx, "crm-check");
|
|
59
|
+
const secret = ctx.names.secretCrmHttp;
|
|
60
|
+
let apiKey;
|
|
61
|
+
try {
|
|
62
|
+
const raw = JSON.parse(await deps.readSecretString(ctx, secret));
|
|
63
|
+
if (typeof raw === "object" && raw !== null && "api_key" in raw)
|
|
64
|
+
apiKey = raw.api_key;
|
|
65
|
+
} catch {}
|
|
66
|
+
if (!isCrmHttpKey(apiKey))
|
|
67
|
+
throw new Error(`${secret} holds no crm/1 key yet: run foundation-deploy crm-key first`);
|
|
68
|
+
const crm = crmCapability(instanceConfig(ctx, deps));
|
|
69
|
+
const report = await deps.runConformance({
|
|
70
|
+
baseUrl: provider.baseUrl,
|
|
71
|
+
apiKey,
|
|
72
|
+
tenantId: provider.expectedTenant,
|
|
73
|
+
strict: !options.allowSkips,
|
|
74
|
+
...crm === undefined ? {} : {
|
|
75
|
+
statusPolicy: crm.statusPolicy,
|
|
76
|
+
...crmOutreachConfigured(crm.outreach) ? { suppressedStatuses: crm.outreach.suppressedStatuses } : {}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return reportConformance(report, provider.baseUrl, deps.log);
|
|
80
|
+
}
|
|
81
|
+
async function crmCheckTarget(options, deps) {
|
|
82
|
+
let baseUrl;
|
|
83
|
+
try {
|
|
84
|
+
baseUrl = crmHttpBaseUrl(options.baseUrl);
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error("--base-url must be an HTTPS URL with no userinfo, query or fragment");
|
|
87
|
+
}
|
|
88
|
+
if (!/^[\x21-\x7e]{1,256}$/.test(options.tenantId))
|
|
89
|
+
throw new Error("--tenant must be the tenant id the server's /meta names");
|
|
90
|
+
const apiKey = (await deps.readStdin()).trim();
|
|
91
|
+
if (!isCrmHttpKey(apiKey))
|
|
92
|
+
throw new Error("stdin did not hold a usable crm/1 key (--key-stdin)");
|
|
93
|
+
const report = await deps.runConformance({
|
|
94
|
+
baseUrl,
|
|
95
|
+
apiKey,
|
|
96
|
+
tenantId: options.tenantId,
|
|
97
|
+
write: options.write,
|
|
98
|
+
strict: !options.allowSkips
|
|
99
|
+
});
|
|
100
|
+
return reportConformance(report, baseUrl, deps.log);
|
|
101
|
+
}
|
|
102
|
+
function reportConformance(report, baseUrl, log) {
|
|
103
|
+
log(`crm/1 conformance against ${baseUrl}`);
|
|
104
|
+
for (const entry of report.cases) {
|
|
105
|
+
const mark = entry.outcome === "fail" ? "FAIL" : entry.outcome;
|
|
106
|
+
log(` ${mark.padEnd(4)} ${entry.id}: ${entry.title}${entry.detail === undefined ? "" : ` (${entry.detail})`}`);
|
|
107
|
+
}
|
|
108
|
+
log(`${report.passed} passed, ${report.failed} failed, ${report.skipped} skipped`);
|
|
109
|
+
return report.failed > 0 ? 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
function instanceConfig(ctx, deps) {
|
|
112
|
+
return parseInstanceConfig(deps.readFile(ctx.paths.configPath));
|
|
113
|
+
}
|
|
114
|
+
function crmCapability(config) {
|
|
115
|
+
const crm = config.capabilities.crm;
|
|
116
|
+
return crm.enabled ? crm : undefined;
|
|
117
|
+
}
|
|
118
|
+
var REPLAY_OPS = ["upsert_records", "update_status", "log_activity"];
|
|
119
|
+
function parseCrmReplay(text, crm, recordIdFor) {
|
|
120
|
+
const writes = [];
|
|
121
|
+
const statuses = {
|
|
122
|
+
company: new Set(crm.statuses.company),
|
|
123
|
+
contact: new Set(crm.statuses.contact)
|
|
124
|
+
};
|
|
125
|
+
const activityTypes = new Set(crm.activityTypes);
|
|
126
|
+
text.split(`
|
|
127
|
+
`).forEach((raw, index) => {
|
|
128
|
+
const line = index + 1;
|
|
129
|
+
if (raw.trim() === "")
|
|
130
|
+
return;
|
|
131
|
+
let value;
|
|
132
|
+
try {
|
|
133
|
+
value = JSON.parse(raw);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(`line ${line} is not a JSON object`);
|
|
136
|
+
}
|
|
137
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
138
|
+
throw new Error(`line ${line} is not a JSON object`);
|
|
139
|
+
const { op, externalId, ...rest } = value;
|
|
140
|
+
if (typeof op !== "string" || !REPLAY_OPS.includes(op))
|
|
141
|
+
throw new Error(`line ${line}: op must be one of ${REPLAY_OPS.join(", ")}`);
|
|
142
|
+
try {
|
|
143
|
+
if (op === "upsert_records") {
|
|
144
|
+
if (externalId !== undefined)
|
|
145
|
+
throw new Error("upsert_records names records inside");
|
|
146
|
+
writes.push({ line, op, args: parseCrmUpsertRecordsArgs(rest, crm.maxBatchSize) });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (typeof externalId !== "string" || externalId.trim() === "" || externalId.length > CRM_MAX_ID_CHARS)
|
|
150
|
+
throw new Error(`externalId must be 1 to ${CRM_MAX_ID_CHARS} characters`);
|
|
151
|
+
if (Object.hasOwn(rest, "recordId"))
|
|
152
|
+
throw new Error("records are named by externalId, never by a provider's recordId");
|
|
153
|
+
const recordType = rest.recordType;
|
|
154
|
+
if (recordType !== "company" && recordType !== "contact")
|
|
155
|
+
throw new Error("recordType must be company or contact");
|
|
156
|
+
const named = { ...rest, recordId: recordIdFor(recordType, externalId) };
|
|
157
|
+
if (op === "update_status") {
|
|
158
|
+
const args2 = parseCrmUpdateStatusArgs(named);
|
|
159
|
+
if (!statuses[args2.recordType].has(args2.status))
|
|
160
|
+
throw new Error(`status is not a configured ${args2.recordType} status`);
|
|
161
|
+
writes.push({ line, op, args: args2 });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const args = parseCrmLogActivityArgs(named);
|
|
165
|
+
if (!activityTypes.has(args.activityType))
|
|
166
|
+
throw new Error("activityType is not a configured activity type");
|
|
167
|
+
writes.push({ line, op: "log_activity", args });
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw new Error(`line ${line}: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return writes;
|
|
173
|
+
}
|
|
174
|
+
async function crmReplay(ctx, options, deps) {
|
|
175
|
+
const { instance } = ctx;
|
|
176
|
+
if (!instance.integrations.crmStorage)
|
|
177
|
+
throw new Error(`crm-replay needs the instance's CRM table (integrations.crm) in ${ctx.paths.path}`);
|
|
178
|
+
if (instance.crm.provider.kind !== "dynamodb")
|
|
179
|
+
throw new Error("crm-replay writes into the DynamoDB provider's table: set crm.provider back to kind dynamodb, deploy, then replay");
|
|
180
|
+
const config = instanceConfig(ctx, deps);
|
|
181
|
+
const crm = crmCapability(config);
|
|
182
|
+
if (crm === undefined || crm.statuses.company.length === 0 || crm.statuses.contact.length === 0)
|
|
183
|
+
throw new Error(`${ctx.paths.configPath}: crm-replay needs capabilities.crm with its statuses`);
|
|
184
|
+
const { createDynamoCrmClient, deterministicRecordId } = await import("./chunk-mh1206ne.js");
|
|
185
|
+
const writes = parseCrmReplay(deps.readFile(options.from), crm, deterministicRecordId);
|
|
186
|
+
const counts = REPLAY_OPS.map((op) => `${writes.filter((write) => write.op === op).length} ${op}`).join(", ");
|
|
187
|
+
if (ctx.dryRun === true) {
|
|
188
|
+
deps.log(`dry run: ${writes.length} writes would be replayed (${counts})`);
|
|
189
|
+
return { applied: 0, refused: [] };
|
|
190
|
+
}
|
|
191
|
+
const outreach = crmOutreachConfigured(crm.outreach) ? { ...crm.outreach, timezone: config.timezone } : undefined;
|
|
192
|
+
const client = createDynamoCrmClient({
|
|
193
|
+
documentClient: await deps.documentClient(ctx),
|
|
194
|
+
tableName: await deps.crmTableName(ctx),
|
|
195
|
+
tenantId: instance.name,
|
|
196
|
+
defaultStatuses: {
|
|
197
|
+
company: crm.statuses.company[0],
|
|
198
|
+
contact: crm.statuses.contact[0]
|
|
199
|
+
},
|
|
200
|
+
maxBatchSize: crm.maxBatchSize,
|
|
201
|
+
actor: {
|
|
202
|
+
teamId: instance.slack.teamId,
|
|
203
|
+
userId: "foundation-deploy",
|
|
204
|
+
channelId: "crm-replay",
|
|
205
|
+
sessionKey: `crm-replay:${instance.name}`
|
|
206
|
+
},
|
|
207
|
+
statusPolicy: crm.statusPolicy,
|
|
208
|
+
...outreach === undefined ? {} : { outreach },
|
|
209
|
+
countsOutreach: false
|
|
210
|
+
});
|
|
211
|
+
deps.log(`replaying ${writes.length} writes (${counts})`);
|
|
212
|
+
const result = { applied: 0, refused: [] };
|
|
213
|
+
for (const write of writes) {
|
|
214
|
+
try {
|
|
215
|
+
if (write.op === "upsert_records")
|
|
216
|
+
await client.upsertRecords(write.args);
|
|
217
|
+
else if (write.op === "update_status")
|
|
218
|
+
await client.updateStatus(write.args);
|
|
219
|
+
else
|
|
220
|
+
await client.logActivity(write.args);
|
|
221
|
+
result.applied += 1;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const code = replayRefusal(error);
|
|
224
|
+
if (code === undefined)
|
|
225
|
+
throw new Error(`line ${write.line} (${write.op}) failed: ${error instanceof Error ? error.name : "error"}; lines before it were replayed, and rerunning the file skips them`);
|
|
226
|
+
result.refused.push({ line: write.line, op: write.op, code });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
deps.log(`${result.applied} applied, ${result.refused.length} refused`);
|
|
230
|
+
for (const refused of result.refused)
|
|
231
|
+
deps.log(` line ${refused.line} (${refused.op}): ${refused.code}`);
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
function replayRefusal(error) {
|
|
235
|
+
if (error instanceof CrmPolicyRefusedError)
|
|
236
|
+
return error.code;
|
|
237
|
+
if (error instanceof CrmRecordNotFoundError)
|
|
238
|
+
return "not_found";
|
|
239
|
+
if (error instanceof CrmIdempotencyConflictError)
|
|
240
|
+
return "conflict";
|
|
241
|
+
if (error instanceof CrmInputError)
|
|
242
|
+
return "invalid_input";
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
async function readPipedStdin() {
|
|
246
|
+
if (process.stdin.isTTY === true)
|
|
247
|
+
throw new Error("stdin is a terminal, where the key would be echoed: pipe it in instead, e.g. `pbpaste | foundation-deploy …`");
|
|
248
|
+
const chunks = [];
|
|
249
|
+
for await (const chunk of process.stdin)
|
|
250
|
+
chunks.push(Buffer.from(chunk));
|
|
251
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
252
|
+
}
|
|
253
|
+
async function crmTableNameFromStack(ctx) {
|
|
254
|
+
const names = (await aws(ctx, [
|
|
255
|
+
"cloudformation",
|
|
256
|
+
"describe-stack-resources",
|
|
257
|
+
"--stack-name",
|
|
258
|
+
ctx.names.data,
|
|
259
|
+
"--query",
|
|
260
|
+
"StackResources[?ResourceType=='AWS::DynamoDB::Table' && starts_with(LogicalResourceId, 'CrmTable')].PhysicalResourceId",
|
|
261
|
+
"--output",
|
|
262
|
+
"text"
|
|
263
|
+
])).split(/\s+/).filter((name) => name !== "" && name !== "None");
|
|
264
|
+
if (names.length !== 1)
|
|
265
|
+
throw new Error(`expected one CRM table in ${ctx.names.data}, found ${names.length}: is integrations.crm deployed?`);
|
|
266
|
+
return names[0];
|
|
267
|
+
}
|
|
268
|
+
async function operatorDocumentClient(ctx) {
|
|
269
|
+
const [{ DynamoDBClient }, { DynamoDBDocumentClient }] = await Promise.all([
|
|
270
|
+
import("@aws-sdk/client-dynamodb"),
|
|
271
|
+
import("@aws-sdk/lib-dynamodb")
|
|
272
|
+
]);
|
|
273
|
+
return DynamoDBDocumentClient.from(new DynamoDBClient({
|
|
274
|
+
region: ctx.region,
|
|
275
|
+
...ctx.profile === "" ? {} : { profile: ctx.profile }
|
|
276
|
+
}), { marshallOptions: { removeUndefinedValues: true } });
|
|
277
|
+
}
|
|
278
|
+
function defaultCrmCommandDeps() {
|
|
279
|
+
return {
|
|
280
|
+
readStdin: readPipedStdin,
|
|
281
|
+
secretExists,
|
|
282
|
+
putSecretString,
|
|
283
|
+
readSecretString,
|
|
284
|
+
runConformance: async (target) => {
|
|
285
|
+
const { runCrmConformance } = await import("./chunk-jf5rw6h4.js");
|
|
286
|
+
return runCrmConformance(target);
|
|
287
|
+
},
|
|
288
|
+
crmTableName: crmTableNameFromStack,
|
|
289
|
+
documentClient: operatorDocumentClient,
|
|
290
|
+
readFile: (path) => readFileSync(path, "utf8"),
|
|
291
|
+
log: (line) => console.log(line)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
export {
|
|
295
|
+
crmCheck,
|
|
296
|
+
crmCheckTarget,
|
|
297
|
+
crmKey,
|
|
298
|
+
crmReplay,
|
|
299
|
+
defaultCrmCommandDeps,
|
|
300
|
+
parseCrmReplay
|
|
301
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ../capability-crm/src/proxy/errors.ts
|
|
2
|
+
class CrmOutreachUnconfiguredError extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("CRM outreach is not configured");
|
|
5
|
+
this.name = "CrmOutreachUnconfiguredError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class CrmRecordNotFoundError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("CRM record does not exist");
|
|
12
|
+
this.name = "CrmRecordNotFoundError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class CrmIdempotencyConflictError extends Error {
|
|
17
|
+
constructor() {
|
|
18
|
+
super("CRM idempotency key conflicts with an earlier request");
|
|
19
|
+
this.name = "CrmIdempotencyConflictError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CrmStorageError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("CRM storage operation failed");
|
|
26
|
+
this.name = "CrmStorageError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class CrmStaleWriteError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
constructor(code) {
|
|
32
|
+
super(`CRM record changed since it was read: ${code}`);
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.name = "CrmStaleWriteError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { CrmOutreachUnconfiguredError, CrmRecordNotFoundError, CrmIdempotencyConflictError, CrmStorageError, CrmStaleWriteError };
|
package/dist/src/index.js
CHANGED
|
@@ -10,42 +10,46 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-59dpqyap.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
17
|
-
LIVE_ENDPOINT_NAME,
|
|
18
17
|
RELEASE_BUCKET_ENV,
|
|
19
18
|
RELEASE_DIR_ENV,
|
|
20
|
-
WEB_SEARCH_CONNECTOR_VERSION,
|
|
21
|
-
WEB_SEARCH_TARGET,
|
|
22
|
-
adminsFor,
|
|
23
19
|
agentImage,
|
|
24
20
|
agentImageTagRequired,
|
|
25
21
|
buildManifest,
|
|
26
|
-
capabilityEnabled,
|
|
27
|
-
configPathFor,
|
|
28
22
|
ecrRepositoryArn,
|
|
29
|
-
enabledIntegrations,
|
|
30
|
-
instanceNames,
|
|
31
23
|
lambdaBundleContext,
|
|
32
24
|
lambdaCode,
|
|
33
25
|
lambdaKey,
|
|
34
|
-
loadInstanceFile,
|
|
35
26
|
manifestKey,
|
|
36
27
|
manifestSigningPayload,
|
|
37
28
|
parseManifest,
|
|
38
|
-
provisionsIntegration,
|
|
39
29
|
releaseAccess,
|
|
40
30
|
releaseCacheDir,
|
|
41
31
|
releaseImageRepositoryArn,
|
|
42
32
|
releaseSource,
|
|
43
|
-
requiresAuthenticatedQueue,
|
|
44
33
|
skillsKey,
|
|
45
34
|
skipBundle,
|
|
46
35
|
verifyManifest
|
|
47
|
-
} from "../chunk-
|
|
36
|
+
} from "../chunk-pxhftba1.js";
|
|
48
37
|
import"../chunk-dz91avrb.js";
|
|
38
|
+
import {
|
|
39
|
+
LIVE_ENDPOINT_NAME,
|
|
40
|
+
WEB_SEARCH_CONNECTOR_VERSION,
|
|
41
|
+
WEB_SEARCH_TARGET,
|
|
42
|
+
adminsFor,
|
|
43
|
+
capabilityEnabled,
|
|
44
|
+
configPathFor,
|
|
45
|
+
enabledIntegrations,
|
|
46
|
+
instanceNames,
|
|
47
|
+
loadInstanceFile,
|
|
48
|
+
provisionsIntegration,
|
|
49
|
+
requiresAuthenticatedQueue
|
|
50
|
+
} from "../chunk-rxetj2p9.js";
|
|
51
|
+
import"../chunk-96fjaysn.js";
|
|
52
|
+
import"../chunk-5wgwscd0.js";
|
|
49
53
|
export {
|
|
50
54
|
BUILD_TIMEOUT,
|
|
51
55
|
BUN_VERSION,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deployfoundation/foundation-deploy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.52",
|
|
4
4
|
"description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"synth": "cdk synth --quiet"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@aws-sdk/client-dynamodb": "3.1111.0",
|
|
31
32
|
"@aws-sdk/client-kms": "3.1111.0",
|
|
33
|
+
"@aws-sdk/lib-dynamodb": "3.1111.0",
|
|
32
34
|
"aws-cdk": "2.1142.0",
|
|
33
35
|
"aws-cdk-lib": "2.270.0",
|
|
34
36
|
"constructs": "10.8.1",
|
package/src/deploy/cli.ts
CHANGED
|
@@ -35,6 +35,9 @@ export const COMMANDS = [
|
|
|
35
35
|
"slack-manifest",
|
|
36
36
|
"stage-customization",
|
|
37
37
|
"job-key",
|
|
38
|
+
"crm-key",
|
|
39
|
+
"crm-check",
|
|
40
|
+
"crm-replay",
|
|
38
41
|
] as const;
|
|
39
42
|
|
|
40
43
|
export type Command = (typeof COMMANDS)[number];
|
|
@@ -56,6 +59,12 @@ commands
|
|
|
56
59
|
slack-manifest print the instance's Slack app manifest as JSON
|
|
57
60
|
stage-customization validate and stage a tenant-owned runtime config
|
|
58
61
|
job-key <id> print a webhook job's URL and signing key as JSON
|
|
62
|
+
crm-key write the company CRM's crm/1 key, read from stdin, into
|
|
63
|
+
the instance's <secretPrefix>/crm/http secret
|
|
64
|
+
crm-check run the crm/1 conformance suite against the instance's
|
|
65
|
+
CRM (crm.provider kind http) with its stored key
|
|
66
|
+
crm-replay replay the agent's CRM writes (JSONL) into the
|
|
67
|
+
instance's own table after a rollback to dynamodb
|
|
59
68
|
|
|
60
69
|
options
|
|
61
70
|
--instance <path> REQUIRED: path to the deployment's instance YAML
|
|
@@ -91,6 +100,24 @@ job-key options
|
|
|
91
100
|
--put-secret <id> write {url, key} into this Secrets Manager secret
|
|
92
101
|
(created if absent) instead of printing the key
|
|
93
102
|
|
|
103
|
+
crm-key options
|
|
104
|
+
--stdin REQUIRED: read the key from a pipe, never argv, e.g.
|
|
105
|
+
pbpaste | foundation-deploy crm-key --instance <path> --stdin
|
|
106
|
+
|
|
107
|
+
crm-check options
|
|
108
|
+
--allow-skips pass despite cases only a person can set up (they need
|
|
109
|
+
the CRM's own harness); without it they count as failed
|
|
110
|
+
--base-url <url> check any crm/1 server instead of the instance's: needs
|
|
111
|
+
--tenant <id> the tenant its /meta names, and the key on stdin
|
|
112
|
+
--key-stdin (--key-stdin). No --instance in this form.
|
|
113
|
+
--write with --base-url only: also run the write cases, which
|
|
114
|
+
create records and spend the week's outreach allowance;
|
|
115
|
+
point it at a sandbox tenant
|
|
116
|
+
|
|
117
|
+
crm-replay options
|
|
118
|
+
--from <file> REQUIRED: the writes to replay, one JSON object per line
|
|
119
|
+
(docs/crm-provider-contract.md); --dry-run validates only
|
|
120
|
+
|
|
94
121
|
github-app-create options
|
|
95
122
|
--org <org> GitHub org (default: the instance's github.org)
|
|
96
123
|
--port <n> local callback port (default 8765)
|
|
@@ -199,6 +226,27 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
199
226
|
return 0;
|
|
200
227
|
}
|
|
201
228
|
|
|
229
|
+
// A CRM's own CI checks its server with the published tool and no instance
|
|
230
|
+
// file at all: everything the suite needs is a flag, and the key a pipe.
|
|
231
|
+
if (command === "crm-check" && args.includes("--base-url")) {
|
|
232
|
+
if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance=")))
|
|
233
|
+
throw new Error("crm-check takes --instance or --base-url, not both");
|
|
234
|
+
const baseUrl = flag(args, "--base-url");
|
|
235
|
+
const tenantId = flag(args, "--tenant");
|
|
236
|
+
if (baseUrl === undefined || tenantId === undefined || !args.includes("--key-stdin"))
|
|
237
|
+
throw new Error("crm-check --base-url needs --tenant <id> and --key-stdin");
|
|
238
|
+
const { crmCheckTarget, defaultCrmCommandDeps } = await import("./crm.ts");
|
|
239
|
+
return crmCheckTarget(
|
|
240
|
+
{
|
|
241
|
+
baseUrl,
|
|
242
|
+
tenantId,
|
|
243
|
+
write: args.includes("--write"),
|
|
244
|
+
allowSkips: args.includes("--allow-skips"),
|
|
245
|
+
},
|
|
246
|
+
defaultCrmCommandDeps(),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
202
250
|
if (command === "github-app-create") {
|
|
203
251
|
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
204
252
|
console.log(instanceBanner(paths));
|
|
@@ -270,6 +318,29 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
270
318
|
console.log(` ${result.status}: ${result.runtimeConfigPath}`);
|
|
271
319
|
return 0;
|
|
272
320
|
}
|
|
321
|
+
// Loaded on demand: the CRM commands bring the conformance suite and,
|
|
322
|
+
// for a replay, the AWS SDK's DynamoDB client.
|
|
323
|
+
case "crm-key": {
|
|
324
|
+
const { crmKey, defaultCrmCommandDeps } = await import("./crm.ts");
|
|
325
|
+
await crmKey(ctx, { stdin: args.includes("--stdin") }, defaultCrmCommandDeps());
|
|
326
|
+
return 0;
|
|
327
|
+
}
|
|
328
|
+
case "crm-check": {
|
|
329
|
+
if (args.includes("--write"))
|
|
330
|
+
throw new Error(
|
|
331
|
+
"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",
|
|
332
|
+
);
|
|
333
|
+
const { crmCheck, defaultCrmCommandDeps } = await import("./crm.ts");
|
|
334
|
+
return crmCheck(ctx, { allowSkips: args.includes("--allow-skips") }, defaultCrmCommandDeps());
|
|
335
|
+
}
|
|
336
|
+
case "crm-replay": {
|
|
337
|
+
const from = flag(args, "--from");
|
|
338
|
+
if (from === undefined || from.startsWith("-"))
|
|
339
|
+
throw new Error("crm-replay needs --from <file>: the writes to replay, as JSONL");
|
|
340
|
+
const { crmReplay, defaultCrmCommandDeps } = await import("./crm.ts");
|
|
341
|
+
const result = await crmReplay(ctx, { from }, defaultCrmCommandDeps());
|
|
342
|
+
return result.refused.length > 0 ? 1 : 0;
|
|
343
|
+
}
|
|
273
344
|
default:
|
|
274
345
|
console.error(`unhandled command: ${command}\n\n${USAGE}`);
|
|
275
346
|
return 1;
|