@deployfoundation/foundation-deploy 0.2.51 → 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.
@@ -0,0 +1,466 @@
1
+ import {
2
+ INTEGRATION_CAPABILITY_IDS,
3
+ PROACTIVE_CHANNEL_MODES,
4
+ SLACK_COMMAND_PREFIX_RE
5
+ } from "./chunk-96fjaysn.js";
6
+
7
+ // ../core/src/instance.ts
8
+ import { parse as parseYaml } from "yaml";
9
+ import { z } from "zod";
10
+ var PENDING = "TBD";
11
+ var CUSTOMIZATION_ARTIFACT_NAME = "Customization";
12
+ var RawAws = z.object({
13
+ account: z.string().regex(/^\d{12}$/),
14
+ region: z.string().min(1),
15
+ profile: z.string().min(1),
16
+ alarmEmail: z.string().email().optional()
17
+ }).strict();
18
+ var RawSlack = z.object({
19
+ teamId: z.string().min(1).max(128),
20
+ appId: z.string().min(1).max(128),
21
+ commandPrefix: z.string().regex(SLACK_COMMAND_PREFIX_RE).optional(),
22
+ legacyCommandPrefixes: z.array(z.string().regex(SLACK_COMMAND_PREFIX_RE)).max(3).default([])
23
+ }).strict();
24
+ var REPO_RE = /^[^/\s]+\/[^/\s]+$/;
25
+ var RawGithub = z.object({
26
+ org: z.string().min(1),
27
+ repo: z.string().regex(REPO_RE),
28
+ defaultRepo: z.string().regex(REPO_RE).optional(),
29
+ repoIds: z.object({ owner: z.number().int().positive(), repo: z.number().int().positive() }).strict(),
30
+ appId: z.union([z.number().int().positive(), z.literal(PENDING)]),
31
+ appSlug: z.union([z.string().regex(/^[a-z0-9][a-z0-9-]*$/), z.literal(PENDING)]),
32
+ workflowWriteRepos: z.array(z.string().regex(REPO_RE)).optional()
33
+ }).strict();
34
+ var RawDeploy = z.object({
35
+ via: z.enum(["github-actions", "codepipeline"]).default("github-actions"),
36
+ branch: z.string().min(1).default("main"),
37
+ connectionArn: z.string().regex(/^arn:aws:code(star-)?connections:[a-z0-9-]+:\d{12}:connection\/[0-9a-f-]+$/).optional()
38
+ }).strict().superRefine((deploy, ctx) => {
39
+ if (deploy.via === "codepipeline" && deploy.connectionArn === undefined)
40
+ ctx.addIssue({
41
+ code: z.ZodIssueCode.custom,
42
+ path: ["connectionArn"],
43
+ message: "deploy.connectionArn is required when deploy.via is codepipeline"
44
+ });
45
+ });
46
+ var UNSAFE_GIT_BRANCH_CHAR_RE = /[\x00-\x20\x7f~^:?*\[\\]/;
47
+ var UNSAFE_CUSTOMIZATION_PATH_CHAR_RE = /[\\\x00-\x1f\x7f]/;
48
+ function safeGitBranch(value) {
49
+ return value !== "" && value !== "@" && !value.startsWith("-") && !value.startsWith("/") && !value.endsWith("/") && !value.endsWith(".") && !value.includes("..") && !value.includes("//") && !value.includes("@{") && !UNSAFE_GIT_BRANCH_CHAR_RE.test(value) && !value.split("/").some((component) => component.endsWith(".lock"));
50
+ }
51
+ function normalizedRelativePosixPath(value) {
52
+ return value !== "" && !value.startsWith("/") && !value.endsWith("/") && !value.includes("..") && !UNSAFE_CUSTOMIZATION_PATH_CHAR_RE.test(value) && value.split("/").every((component) => component !== "" && component !== ".");
53
+ }
54
+ var RawCustomization = z.object({
55
+ repository: z.string().regex(REPO_RE),
56
+ branch: z.string().refine(safeGitBranch, "customization.branch must be a safe Git branch name").default("main"),
57
+ path: z.string().refine(normalizedRelativePosixPath, "customization.path must be a normalized relative POSIX path"),
58
+ required: z.boolean().default(true)
59
+ }).strict();
60
+ var ProactiveChannelId = z.string().regex(/^[CG][A-Z0-9]{8,}$/);
61
+ var RawProactiveChannel = z.union([
62
+ ProactiveChannelId.transform((id) => ({ id, mode: "triage" })),
63
+ z.object({ id: ProactiveChannelId, mode: z.enum(PROACTIVE_CHANNEL_MODES).default("triage") }).strict()
64
+ ]);
65
+ var RawProactive = z.object({
66
+ channels: z.array(RawProactiveChannel).default([]).refine((channels) => new Set(channels.map((c) => c.id)).size === channels.length, {
67
+ message: "a channel may be listed only once"
68
+ }),
69
+ maxPerHour: z.number().int().min(1).max(500).default(20)
70
+ }).strict().default({ channels: [] });
71
+ var RawRuntime = z.object({
72
+ idleTimeoutMinutes: z.number().int().min(5).max(480).default(15),
73
+ maxLifetimeHours: z.number().int().min(1).max(8).default(8)
74
+ }).strict().default({});
75
+ var RawNetwork = z.object({
76
+ natGateways: z.union([z.literal(1), z.literal(2)]).default(1),
77
+ endpointAzs: z.union([z.literal(1), z.literal(2)]).default(1)
78
+ }).strict().default({});
79
+ var RawNaming = z.object({
80
+ prefix: z.string().min(1).max(32).regex(/^[A-Z][A-Za-z0-9]*$/),
81
+ secretPrefix: z.string().min(1).max(40).regex(/^[a-z][a-z0-9-]*$/),
82
+ runtimeName: z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]{0,47}$/)
83
+ }).strict();
84
+ var INSTANCE_ID_RE = /^[a-z][a-z0-9-]*$/;
85
+ var DOMAIN_RE = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
86
+ var HOSTED_ZONE_ID_RE = /^Z[A-Z0-9]{1,32}$/;
87
+ var MAILBOX_RE = /^[^<>\r\n]+ <[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}>$/i;
88
+ function httpsOrigin(value) {
89
+ try {
90
+ const url = new URL(value);
91
+ return url.protocol === "https:" && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "" && value === url.origin;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ function safeHttpsUrl(value) {
97
+ try {
98
+ const url = new URL(value);
99
+ return url.protocol === "https:" && url.username === "" && url.password === "";
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ var RawNewsletterScope = z.object({
105
+ businessId: z.string().regex(INSTANCE_ID_RE),
106
+ newsletterId: z.string().regex(INSTANCE_ID_RE),
107
+ displayName: z.string().min(1).max(120),
108
+ consentVersion: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/),
109
+ confirmationTtlHours: z.number().int().min(1).max(168).default(72)
110
+ }).strict();
111
+ var RawNewsletterDisabled = z.object({ enabled: z.literal(false) }).strict();
112
+ var RawNewsletterEnabled = z.object({
113
+ enabled: z.literal(true),
114
+ hostedZoneId: z.string().regex(HOSTED_ZONE_ID_RE),
115
+ hostedZoneName: z.string().regex(DOMAIN_RE),
116
+ apiDomain: z.string().regex(DOMAIN_RE),
117
+ domainIdentity: z.string().regex(DOMAIN_RE),
118
+ allowedOrigins: z.array(z.string().refine(httpsOrigin, "must be an HTTPS origin")).min(1),
119
+ resultUrl: z.string().refine(safeHttpsUrl, "must be an HTTPS URL"),
120
+ sender: z.string().regex(MAILBOX_RE),
121
+ replyTo: z.string().email(),
122
+ scopes: z.array(RawNewsletterScope).min(1)
123
+ }).strict().superRefine((newsletter, ctx) => {
124
+ const hostedZone = newsletter.hostedZoneName.toLowerCase();
125
+ const identity = newsletter.domainIdentity.toLowerCase();
126
+ if (hostedZone !== identity)
127
+ ctx.addIssue({
128
+ code: z.ZodIssueCode.custom,
129
+ path: ["domainIdentity"],
130
+ message: "newsletter.domainIdentity must equal newsletter.hostedZoneName"
131
+ });
132
+ if (newsletter.apiDomain.toLowerCase() !== hostedZone && !newsletter.apiDomain.toLowerCase().endsWith(`.${hostedZone}`))
133
+ ctx.addIssue({
134
+ code: z.ZodIssueCode.custom,
135
+ path: ["apiDomain"],
136
+ message: "newsletter.apiDomain must be inside newsletter.hostedZoneName"
137
+ });
138
+ let result;
139
+ try {
140
+ result = new URL(newsletter.resultUrl);
141
+ } catch {
142
+ return;
143
+ }
144
+ const tenantHost = result.hostname.toLowerCase();
145
+ if (tenantHost !== hostedZone && !tenantHost.endsWith(`.${hostedZone}`))
146
+ ctx.addIssue({
147
+ code: z.ZodIssueCode.custom,
148
+ path: ["resultUrl"],
149
+ message: "newsletter.resultUrl must stay inside newsletter.hostedZoneName"
150
+ });
151
+ if (!newsletter.allowedOrigins.includes(result.origin))
152
+ ctx.addIssue({
153
+ code: z.ZodIssueCode.custom,
154
+ path: ["resultUrl"],
155
+ message: "newsletter.resultUrl must belong to an allowed origin"
156
+ });
157
+ for (const [index, origin] of newsletter.allowedOrigins.entries()) {
158
+ let originUrl;
159
+ try {
160
+ originUrl = new URL(origin);
161
+ } catch {
162
+ continue;
163
+ }
164
+ const originHost = originUrl.hostname.toLowerCase();
165
+ if (originHost !== hostedZone && !originHost.endsWith(`.${hostedZone}`))
166
+ ctx.addIssue({
167
+ code: z.ZodIssueCode.custom,
168
+ path: ["allowedOrigins", index],
169
+ message: "newsletter.allowedOrigins must stay inside newsletter.hostedZoneName"
170
+ });
171
+ }
172
+ const senderEmail = /<([^>]+)>$/.exec(newsletter.sender)?.[1]?.toLowerCase();
173
+ if (senderEmail === undefined || !senderEmail.endsWith(`@${identity}`))
174
+ ctx.addIssue({
175
+ code: z.ZodIssueCode.custom,
176
+ path: ["sender"],
177
+ message: "newsletter.sender must use newsletter.domainIdentity"
178
+ });
179
+ const seen = new Set;
180
+ for (const [index, scope] of newsletter.scopes.entries()) {
181
+ const key = `${scope.businessId}/${scope.newsletterId}`;
182
+ if (seen.has(key))
183
+ ctx.addIssue({
184
+ code: z.ZodIssueCode.custom,
185
+ path: ["scopes", index],
186
+ message: `duplicate newsletter scope ${key}`
187
+ });
188
+ seen.add(key);
189
+ }
190
+ });
191
+ var RawNewsletter = z.union([RawNewsletterDisabled, RawNewsletterEnabled]).default({ enabled: false });
192
+ var RawIntegrations = z.object({
193
+ otter: z.boolean().default(false),
194
+ browser: z.boolean().default(false),
195
+ knock: z.boolean().default(false),
196
+ upwork: z.boolean().default(false),
197
+ etrade: z.boolean().default(false),
198
+ pages: z.boolean().default(false),
199
+ crm: z.boolean().default(false),
200
+ crmStorage: z.boolean().optional()
201
+ }).strict().default({}).transform((integrations) => ({
202
+ ...integrations,
203
+ crmStorage: integrations.crmStorage ?? integrations.crm
204
+ }));
205
+ var RawLicense = z.object({ key: z.string().min(1) }).strict();
206
+ var PAGES_DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
207
+ var RawPages = z.object({
208
+ domain: z.string().regex(PAGES_DOMAIN_RE, "pages.domain must be a lowercase host name without a scheme or path").optional(),
209
+ hostedZoneId: z.string().regex(HOSTED_ZONE_ID_RE, "pages.hostedZoneId must be a Route 53 hosted zone id (Z…)").optional()
210
+ }).strict().superRefine((pages, ctx) => {
211
+ if (pages.hostedZoneId !== undefined && pages.domain === undefined)
212
+ ctx.addIssue({
213
+ code: z.ZodIssueCode.custom,
214
+ path: ["hostedZoneId"],
215
+ message: "pages.hostedZoneId needs pages.domain"
216
+ });
217
+ }).default({});
218
+ function crmHttpBaseUrl(value) {
219
+ if (value.includes("?") || value.includes("#"))
220
+ return false;
221
+ try {
222
+ const url = new URL(value);
223
+ return url.protocol === "https:" && url.username === "" && url.password === "" && url.search === "" && url.hash === "";
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+ var RawCrmProvider = z.discriminatedUnion("kind", [
229
+ z.object({ kind: z.literal("dynamodb") }).strict(),
230
+ z.object({
231
+ kind: z.literal("http"),
232
+ baseUrl: z.string().max(2048).refine(crmHttpBaseUrl, "crm.provider.baseUrl must be an HTTPS URL with no userinfo, query or fragment"),
233
+ expectedTenant: z.string().min(1).max(256).regex(/^[\x21-\x7e]+$/, "crm.provider.expectedTenant must be visible ASCII")
234
+ }).strict()
235
+ ]);
236
+ var RawCrm = z.object({ provider: RawCrmProvider.default({ kind: "dynamodb" }) }).strict().default({});
237
+ var RawInstance = z.object({
238
+ name: z.string().min(1).max(63).regex(/^[a-z][a-z0-9-]*$/),
239
+ displayName: z.string().min(1).max(120),
240
+ pending: z.boolean().default(false),
241
+ aws: RawAws,
242
+ slack: RawSlack,
243
+ github: RawGithub,
244
+ naming: RawNaming,
245
+ deploy: RawDeploy.default({}),
246
+ customization: RawCustomization.optional(),
247
+ license: RawLicense.optional(),
248
+ config: z.string().min(1),
249
+ proactive: RawProactive,
250
+ runtime: RawRuntime,
251
+ network: RawNetwork,
252
+ integrations: RawIntegrations,
253
+ newsletter: RawNewsletter,
254
+ pages: RawPages,
255
+ crm: RawCrm
256
+ }).strict().superRefine((doc, ctx) => {
257
+ if (doc.crm.provider.kind === "http" && !doc.integrations.crm)
258
+ ctx.addIssue({
259
+ code: z.ZodIssueCode.custom,
260
+ path: ["crm", "provider"],
261
+ message: "crm.provider kind http needs integrations.crm: true"
262
+ });
263
+ if (doc.integrations.pages && doc.pages.domain === undefined)
264
+ ctx.addIssue({
265
+ code: z.ZodIssueCode.custom,
266
+ path: ["pages", "domain"],
267
+ message: "integrations.pages needs pages.domain: each page is served from its own subdomain of it (docs/pages.md)"
268
+ });
269
+ if (doc.pages.domain !== undefined && !doc.integrations.pages)
270
+ ctx.addIssue({
271
+ code: z.ZodIssueCode.custom,
272
+ path: ["pages", "domain"],
273
+ message: "pages.domain needs integrations.pages: true"
274
+ });
275
+ if (doc.customization !== undefined && doc.deploy.via !== "codepipeline")
276
+ ctx.addIssue({
277
+ code: z.ZodIssueCode.custom,
278
+ path: ["customization"],
279
+ message: "customization is only supported when deploy.via is codepipeline"
280
+ });
281
+ if (doc.pending)
282
+ return;
283
+ for (const [path, value] of [
284
+ [["slack", "teamId"], doc.slack.teamId],
285
+ [["slack", "appId"], doc.slack.appId],
286
+ [["github", "appId"], String(doc.github.appId)],
287
+ [["github", "appSlug"], doc.github.appSlug]
288
+ ]) {
289
+ if (value === PENDING)
290
+ ctx.addIssue({
291
+ code: z.ZodIssueCode.custom,
292
+ path: [...path],
293
+ message: `${path.join(".")} is still ${PENDING}; set it or mark the instance pending: true`
294
+ });
295
+ }
296
+ });
297
+ function parseInstance(yamlText) {
298
+ return RawInstance.parse(parseYaml(yamlText));
299
+ }
300
+ function newsletterManifestFor(instance) {
301
+ const newsletter = instance.newsletter;
302
+ if (!newsletter.enabled)
303
+ throw new Error(`newsletter is not enabled for instance ${instance.name}`);
304
+ return {
305
+ apiBaseUrl: `https://${newsletter.apiDomain}`,
306
+ allowedOrigins: [...newsletter.allowedOrigins],
307
+ scopes: newsletter.scopes.map((scope) => ({
308
+ businessId: scope.businessId,
309
+ newsletterId: scope.newsletterId,
310
+ displayName: scope.displayName,
311
+ consentVersion: scope.consentVersion,
312
+ confirmationTtlSeconds: scope.confirmationTtlHours * 60 * 60,
313
+ resultUrl: newsletter.resultUrl,
314
+ sender: newsletter.sender,
315
+ replyTo: newsletter.replyTo
316
+ }))
317
+ };
318
+ }
319
+ var FOUNDATION_VERSION_RE = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
320
+ function versionedConfigKey(configKey, version) {
321
+ if (!FOUNDATION_VERSION_RE.test(version))
322
+ throw new Error(`not a Foundation version: "${version}" (expected one like v0.2.34)`);
323
+ return `config/${version}/${configKey.slice(configKey.lastIndexOf("/") + 1)}`;
324
+ }
325
+ function instanceNames(instance) {
326
+ const { prefix, secretPrefix, runtimeName } = instance.naming;
327
+ const stack = (suffix) => `${prefix}${suffix}`;
328
+ const secret = (suffix) => `${secretPrefix}/${suffix}`;
329
+ const [network, data, api, agent, newsletter, pages] = [
330
+ stack("Network"),
331
+ stack("Data"),
332
+ stack("Api"),
333
+ stack("Agent"),
334
+ stack("Newsletter"),
335
+ stack("Pages")
336
+ ];
337
+ return {
338
+ network,
339
+ data,
340
+ agent,
341
+ api,
342
+ ci: stack("Ci"),
343
+ newsletter,
344
+ pages,
345
+ pipeline: stack("Pipeline"),
346
+ allStacks: [
347
+ network,
348
+ data,
349
+ ...instance.integrations.pages ? [pages] : [],
350
+ api,
351
+ agent,
352
+ ...instance.newsletter.enabled ? [newsletter] : []
353
+ ],
354
+ secretSlackSigning: secret("slack/signing"),
355
+ secretSlackApp: secret("slack/app"),
356
+ secretGithubApp: secret("github/app"),
357
+ secretCodex: secret("openai/codex"),
358
+ secretGoogleAiStudio: secret("google/ai-studio"),
359
+ secretGoogleDrive: secret("google/drive"),
360
+ secretGoogleCalendar: secret("google/calendar"),
361
+ secretGoogleOauth: secret("google/oauth"),
362
+ secretGoogleEmail: secret("google/email"),
363
+ secretGoogleDocs: secret("google/docs"),
364
+ secretMongodbReadonly: secret("mongodb/readonly"),
365
+ secretOtterApi: secret("otter/api"),
366
+ secretKnockOauthClient: secret("knock/oauth-client"),
367
+ secretKnockCredential: secret("knock/credential"),
368
+ secretUpwork: secret("upwork/oauth"),
369
+ secretEtradeClient: secret("etrade/oauth-client"),
370
+ secretEtradeCredential: secret("etrade/credential"),
371
+ secretCrmHttp: secret("crm/http"),
372
+ secretRuntime: secret("agent/runtime"),
373
+ secretLicenseLastVerified: secret("license/last-verified"),
374
+ newsletterTokenSigningSecret: secret("newsletter/token-signing"),
375
+ newsletterKeyAlias: secret("newsletter"),
376
+ newsletterConfigurationSet: `${secretPrefix}-newsletter`,
377
+ newsletterCampaignOperatorRole: stack("NewsletterCampaignOperator"),
378
+ runtimeName,
379
+ ecrRepo: runtimeName.replaceAll("_", "-"),
380
+ keyAlias: secret("data"),
381
+ gatewayName: `${secretPrefix}-tools`,
382
+ slackGatewayFunctionName: stack("SlackGateway"),
383
+ routineGroup: `${secretPrefix}-routines`,
384
+ routineSchedulerRole: stack("RoutineScheduler"),
385
+ deployRole: stack("DeployRole"),
386
+ configKey: instance.config,
387
+ emailProxyFunctionName: stack("EmailProxy"),
388
+ googleDocsProxyFunctionName: stack("GoogleDocsProxy"),
389
+ otterProxyFunctionName: stack("OtterProxy"),
390
+ crmProxyFunctionName: stack("CrmProxy"),
391
+ pagesApiFunctionName: stack("PagesApi"),
392
+ secretPagesCookieKey: secret("pages/cookie-key"),
393
+ browserProxyFunctionName: stack("BrowserProxy"),
394
+ knockProxyFunctionName: stack("KnockProxy"),
395
+ upworkProxyFunctionName: stack("UpworkProxy"),
396
+ etradeProxyFunctionName: stack("EtradeProxy"),
397
+ defaultRepo: instance.github.defaultRepo ?? instance.github.repo
398
+ };
399
+ }
400
+ function slackCommandPrefix(instance) {
401
+ if (instance.slack.commandPrefix !== undefined)
402
+ return instance.slack.commandPrefix;
403
+ const derived = instance.displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 21).replace(/-+$/, "");
404
+ if (!SLACK_COMMAND_PREFIX_RE.test(derived))
405
+ throw new Error(`cannot derive a Slack command prefix from displayName "${instance.displayName}"; set slack.commandPrefix`);
406
+ return derived;
407
+ }
408
+ function slackCommandPrefixes(instance) {
409
+ return [slackCommandPrefix(instance), ...instance.slack.legacyCommandPrefixes];
410
+ }
411
+
412
+ // src/names.ts
413
+ import { readFileSync } from "node:fs";
414
+ import { dirname, isAbsolute, resolve } from "node:path";
415
+ import { parse as parseYaml2 } from "yaml";
416
+ function loadInstanceFile(path, cwd = process.cwd()) {
417
+ const absolute = isAbsolute(path) ? path : resolve(cwd, path);
418
+ let text;
419
+ try {
420
+ text = readFileSync(absolute, "utf8");
421
+ } catch {
422
+ throw new Error(`no instance file at ${absolute}`);
423
+ }
424
+ const instance = parseInstance(text);
425
+ return { instance, path: absolute, configPath: configPathFor(instance, absolute) };
426
+ }
427
+ function configPathFor(instance, instanceFilePath) {
428
+ return isAbsolute(instance.config) ? instance.config : resolve(dirname(instanceFilePath), instance.config);
429
+ }
430
+ function configDocument(configPath) {
431
+ const doc = parseYaml2(readFileSync(configPath, "utf8"));
432
+ return doc ?? {};
433
+ }
434
+ function capabilityEnabled(configPath, capability) {
435
+ const doc = configDocument(configPath);
436
+ const enabled = doc.capabilities?.[capability]?.enabled;
437
+ if (enabled !== undefined && typeof enabled !== "boolean")
438
+ throw new Error(`${configPath}: capabilities.${capability}.enabled must be a boolean`);
439
+ return enabled ?? false;
440
+ }
441
+ function requiresAuthenticatedQueue(instance) {
442
+ return enabledIntegrations(instance).length > 0;
443
+ }
444
+ function enabledIntegrations(instance) {
445
+ const integrations = instance.integrations;
446
+ return INTEGRATION_CAPABILITY_IDS.filter((id) => integrations[id] === true);
447
+ }
448
+ function provisionsIntegration(instance, id) {
449
+ if (!INTEGRATION_CAPABILITY_IDS.includes(id))
450
+ throw new Error(`${id} is not an integration capability in the registry`);
451
+ return instance.integrations[id] === true;
452
+ }
453
+ function adminsFor(configPath) {
454
+ const doc = configDocument(configPath);
455
+ const admins = doc.admins;
456
+ if (admins === undefined || admins === null)
457
+ return "";
458
+ if (!Array.isArray(admins) || admins.some((id) => typeof id !== "string"))
459
+ throw new Error(`${configPath}: admins must be a list of Slack user ids`);
460
+ return admins.join(",");
461
+ }
462
+ var WEB_SEARCH_TARGET = "websearch";
463
+ var WEB_SEARCH_CONNECTOR_VERSION = "1.2.0";
464
+ var LIVE_ENDPOINT_NAME = "live";
465
+
466
+ export { CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME };