@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 +4 -1
- package/dist/bin/app.js +6 -3
- package/dist/bin/foundation-deploy.js +141 -305
- package/dist/bin/release-account.js +1 -0
- package/dist/chunk-1pww63wd.js +301 -0
- package/dist/chunk-3614v9rr.js +907 -0
- package/dist/chunk-5wgwscd0.js +4 -0
- package/dist/{chunk-gnz747z2.js → chunk-96fjaysn.js} +204 -884
- package/dist/chunk-fjdgxw8h.js +516 -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-nvsj7bbw.js +259 -0
- package/dist/chunk-p7zkm4pp.js +151 -0
- package/dist/chunk-pxhftba1.js +336 -0
- package/dist/{chunk-efqdhd2e.js → chunk-spq5kr1t.js} +333 -90
- 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/deploy/runtime-secret.ts +9 -0
- package/src/stacks/agent-stack.ts +68 -11
- package/src/stacks/api-stack.ts +340 -87
- package/src/stacks/data-stack.ts +27 -1
- package/src/stacks/secret-bearing-reads.ts +131 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
instanceNames,
|
|
3
|
+
loadInstanceFile
|
|
4
|
+
} from "./chunk-fjdgxw8h.js";
|
|
5
|
+
|
|
6
|
+
// src/deploy/aws.ts
|
|
7
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
// src/deploy/instance.ts
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
var INSTANCE_FILE_ENV = "FOUNDATION_INSTANCE_FILE";
|
|
14
|
+
function resolveInstanceFilePath(argv = [], env = {}) {
|
|
15
|
+
for (let i = 0;i < argv.length; i++) {
|
|
16
|
+
const arg = argv[i] ?? "";
|
|
17
|
+
if (arg === "--instance") {
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next === undefined || next.startsWith("-"))
|
|
20
|
+
throw new Error("--instance needs a path, e.g. --instance .foundation/instance.yaml");
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
if (arg.startsWith("--instance=")) {
|
|
24
|
+
const value = arg.slice("--instance=".length);
|
|
25
|
+
if (value === "")
|
|
26
|
+
throw new Error("--instance needs a path");
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const fromEnv = env[INSTANCE_FILE_ENV];
|
|
31
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
32
|
+
return fromEnv;
|
|
33
|
+
throw new Error(`--instance <path> is required (or set ${INSTANCE_FILE_ENV}); it is the path to the deployment's instance YAML`);
|
|
34
|
+
}
|
|
35
|
+
function loadInstanceContext(path, cwd = process.cwd()) {
|
|
36
|
+
const file = loadInstanceFile(path, cwd);
|
|
37
|
+
return { ...file, root: dirname(file.path) };
|
|
38
|
+
}
|
|
39
|
+
function loadInstanceFromArgs(argv = process.argv.slice(2), env = process.env) {
|
|
40
|
+
return loadInstanceContext(resolveInstanceFilePath(argv, env));
|
|
41
|
+
}
|
|
42
|
+
function instanceBanner(context) {
|
|
43
|
+
const { instance } = context;
|
|
44
|
+
return `instance: ${instance.name} (account ${instance.aws.account}, region ${instance.aws.region})
|
|
45
|
+
file: ${context.path}
|
|
46
|
+
config: ${context.configPath}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/deploy/sh.ts
|
|
50
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
51
|
+
function formatCommand(cmd, env) {
|
|
52
|
+
const prefix = Object.entries(env ?? {}).map(([k, v]) => `${k}=${quote(v)}`).join(" ");
|
|
53
|
+
const body = cmd.map(quote).join(" ");
|
|
54
|
+
return prefix === "" ? body : `${prefix} ${body}`;
|
|
55
|
+
}
|
|
56
|
+
function quote(value) {
|
|
57
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
58
|
+
}
|
|
59
|
+
function start(cmd, opts, stdout) {
|
|
60
|
+
const [bin, ...rest] = cmd;
|
|
61
|
+
if (bin === undefined)
|
|
62
|
+
throw new Error("run: empty command");
|
|
63
|
+
const child = nodeSpawn(bin, rest, {
|
|
64
|
+
cwd: opts.cwd,
|
|
65
|
+
env: { ...process.env, ...opts.env },
|
|
66
|
+
stdio: [
|
|
67
|
+
opts.stdin === undefined ? "ignore" : "pipe",
|
|
68
|
+
stdout,
|
|
69
|
+
stdout === "inherit" ? "inherit" : "pipe"
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
if (opts.stdin !== undefined && child.stdin !== null) {
|
|
73
|
+
child.stdin.end(opts.stdin);
|
|
74
|
+
}
|
|
75
|
+
return child;
|
|
76
|
+
}
|
|
77
|
+
function exited(child) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
child.once("error", reject);
|
|
80
|
+
child.once("close", (code, signal) => resolve(code ?? (signal === null ? 1 : 128)));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function collect(stream) {
|
|
84
|
+
if (stream === null)
|
|
85
|
+
return Promise.resolve("");
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
stream.on("data", (c) => chunks.push(c));
|
|
89
|
+
stream.once("error", reject);
|
|
90
|
+
stream.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function run(cmd, opts = {}) {
|
|
94
|
+
if (opts.dryRun === true) {
|
|
95
|
+
console.log(` $ ${formatCommand(cmd, opts.env)}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const code = await exited(start(cmd, opts, "inherit"));
|
|
99
|
+
if (code !== 0)
|
|
100
|
+
throw new Error(`${formatCommand(cmd)} exited ${code}`);
|
|
101
|
+
}
|
|
102
|
+
async function runCapture(cmd, opts = {}) {
|
|
103
|
+
const child = start(cmd, opts, "pipe");
|
|
104
|
+
const [out, err, code] = await Promise.all([
|
|
105
|
+
collect(child.stdout),
|
|
106
|
+
collect(child.stderr),
|
|
107
|
+
exited(child)
|
|
108
|
+
]);
|
|
109
|
+
if (code !== 0)
|
|
110
|
+
throw new Error(`${formatCommand(cmd)} exited ${code}: ${err.trim()}`);
|
|
111
|
+
return out.trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/deploy/aws.ts
|
|
115
|
+
function resolveProfile(fallback, explicit, env = process.env) {
|
|
116
|
+
if (env.FOUNDATION_NO_PROFILE === "1")
|
|
117
|
+
return "";
|
|
118
|
+
if (explicit !== undefined)
|
|
119
|
+
return explicit;
|
|
120
|
+
if (env.AWS_PROFILE !== undefined && env.AWS_PROFILE !== "")
|
|
121
|
+
return env.AWS_PROFILE;
|
|
122
|
+
if (env.GITHUB_ACTIONS === "true")
|
|
123
|
+
return "";
|
|
124
|
+
return fallback;
|
|
125
|
+
}
|
|
126
|
+
function awsContext(overrides = {}) {
|
|
127
|
+
const env = overrides.env ?? process.env;
|
|
128
|
+
const paths = overrides.paths ?? loadInstanceFromArgs();
|
|
129
|
+
const instance = paths.instance;
|
|
130
|
+
return {
|
|
131
|
+
profile: resolveProfile(instance.aws.profile, overrides.profile, env),
|
|
132
|
+
region: overrides.region ?? env.AWS_REGION ?? instance.aws.region,
|
|
133
|
+
dryRun: overrides.dryRun ?? false,
|
|
134
|
+
instance,
|
|
135
|
+
paths,
|
|
136
|
+
names: instanceNames(instance)
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function argv(ctx, args) {
|
|
140
|
+
const profile = ctx.profile === "" ? [] : ["--profile", ctx.profile];
|
|
141
|
+
return ["aws", ...args, ...profile, "--region", ctx.region];
|
|
142
|
+
}
|
|
143
|
+
function cdkEnv(ctx) {
|
|
144
|
+
return {
|
|
145
|
+
...ctx.profile === "" ? {} : { AWS_PROFILE: ctx.profile },
|
|
146
|
+
AWS_REGION: ctx.region,
|
|
147
|
+
CDK_DEFAULT_REGION: ctx.region
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function aws(ctx, args, stdin) {
|
|
151
|
+
return runCapture(argv(ctx, args), { stdin });
|
|
152
|
+
}
|
|
153
|
+
async function awsMutate(ctx, args, stdin) {
|
|
154
|
+
if (ctx.dryRun === true) {
|
|
155
|
+
console.log(` $ ${argv(ctx, args).join(" ")}${stdin === undefined ? "" : " < (stdin)"}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await run(argv(ctx, args), { stdin });
|
|
159
|
+
}
|
|
160
|
+
async function stackOutput(ctx, stack, key) {
|
|
161
|
+
const value = await aws(ctx, [
|
|
162
|
+
"cloudformation",
|
|
163
|
+
"describe-stacks",
|
|
164
|
+
"--stack-name",
|
|
165
|
+
stack,
|
|
166
|
+
"--query",
|
|
167
|
+
`Stacks[0].Outputs[?OutputKey=='${key}'].OutputValue`,
|
|
168
|
+
"--output",
|
|
169
|
+
"text"
|
|
170
|
+
]);
|
|
171
|
+
if (value === "" || value === "None")
|
|
172
|
+
throw new Error(`stack ${stack} has no output ${key} — has it been deployed?`);
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
async function stackExists(ctx, stack) {
|
|
176
|
+
try {
|
|
177
|
+
await aws(ctx, [
|
|
178
|
+
"cloudformation",
|
|
179
|
+
"describe-stacks",
|
|
180
|
+
"--stack-name",
|
|
181
|
+
stack,
|
|
182
|
+
"--output",
|
|
183
|
+
"text"
|
|
184
|
+
]);
|
|
185
|
+
return true;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function secretExists(ctx, secretId) {
|
|
191
|
+
try {
|
|
192
|
+
await aws(ctx, [
|
|
193
|
+
"secretsmanager",
|
|
194
|
+
"describe-secret",
|
|
195
|
+
"--secret-id",
|
|
196
|
+
secretId,
|
|
197
|
+
"--output",
|
|
198
|
+
"text"
|
|
199
|
+
]);
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function readSecretString(ctx, secretId) {
|
|
206
|
+
return aws(ctx, [
|
|
207
|
+
"secretsmanager",
|
|
208
|
+
"get-secret-value",
|
|
209
|
+
"--secret-id",
|
|
210
|
+
secretId,
|
|
211
|
+
"--query",
|
|
212
|
+
"SecretString",
|
|
213
|
+
"--output",
|
|
214
|
+
"text"
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
async function readSecretJson(ctx, secretId) {
|
|
218
|
+
return JSON.parse(await readSecretString(ctx, secretId));
|
|
219
|
+
}
|
|
220
|
+
async function putSecretString(ctx, secretId, value) {
|
|
221
|
+
await throughSecretFile(value, (file) => awsMutate(ctx, [
|
|
222
|
+
"secretsmanager",
|
|
223
|
+
"put-secret-value",
|
|
224
|
+
"--secret-id",
|
|
225
|
+
secretId,
|
|
226
|
+
"--secret-string",
|
|
227
|
+
`file://${file}`
|
|
228
|
+
]));
|
|
229
|
+
}
|
|
230
|
+
async function createSecretString(ctx, secretId, value, description) {
|
|
231
|
+
await throughSecretFile(value, (file) => awsMutate(ctx, [
|
|
232
|
+
"secretsmanager",
|
|
233
|
+
"create-secret",
|
|
234
|
+
"--name",
|
|
235
|
+
secretId,
|
|
236
|
+
"--description",
|
|
237
|
+
description,
|
|
238
|
+
"--secret-string",
|
|
239
|
+
`file://${file}`
|
|
240
|
+
]));
|
|
241
|
+
}
|
|
242
|
+
async function throughSecretFile(value, body) {
|
|
243
|
+
const dir = mkdtempSync(join(tmpdir(), "foundation-secret-"));
|
|
244
|
+
const file = join(dir, "value.json");
|
|
245
|
+
try {
|
|
246
|
+
writeFileSync(file, value, { mode: 384 });
|
|
247
|
+
await body(file);
|
|
248
|
+
} finally {
|
|
249
|
+
rmSync(dir, { recursive: true, force: true });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function putSecretJson(ctx, secretId, value) {
|
|
253
|
+
await putSecretString(ctx, secretId, JSON.stringify(value));
|
|
254
|
+
}
|
|
255
|
+
async function callerAccountId(ctx) {
|
|
256
|
+
return aws(ctx, ["sts", "get-caller-identity", "--query", "Account", "--output", "text"]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { resolveInstanceFilePath, loadInstanceContext, instanceBanner, run, runCapture, awsContext, argv, cdkEnv, aws, awsMutate, stackOutput, stackExists, secretExists, readSecretString, readSecretJson, putSecretString, createSecretString, putSecretJson, callerAccountId };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CRM_MAX_ATTRIBUTE_STRING_CHARS,
|
|
3
|
+
CRM_NOTE_ACTIVITY,
|
|
4
|
+
CRM_OUTREACH_DRAFTED_ACTIVITY,
|
|
5
|
+
CRM_OUTREACH_REPLY_ACTIVITY,
|
|
6
|
+
CRM_OUTREACH_SENT_ACTIVITY,
|
|
7
|
+
CrmInputError
|
|
8
|
+
} from "./chunk-3614v9rr.js";
|
|
9
|
+
|
|
10
|
+
// ../capability-crm/src/proxy/policy.ts
|
|
11
|
+
class CrmPolicyRefusedError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code) {
|
|
14
|
+
super(`CRM policy refused the request: ${code}`);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = "CrmPolicyRefusedError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function checkStatusChange(policy, recordType, current, next) {
|
|
20
|
+
const rule = policy[recordType];
|
|
21
|
+
if (rule === undefined)
|
|
22
|
+
return;
|
|
23
|
+
const personOwned = rule.respectHumanChanges && (current.setBy ?? "person") === "person";
|
|
24
|
+
if (personOwned) {
|
|
25
|
+
if (rule.personSet === "lock")
|
|
26
|
+
return "status_locked";
|
|
27
|
+
const from = rule.ladder.indexOf(current.status);
|
|
28
|
+
const to = rule.ladder.indexOf(next);
|
|
29
|
+
if (from < 0 || to <= from)
|
|
30
|
+
return "status_locked";
|
|
31
|
+
} else if (current.status === next) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const exits = Object.hasOwn(rule.agentTransitions, current.status) ? rule.agentTransitions[current.status] : undefined;
|
|
35
|
+
return exits?.includes(next) === true ? undefined : "transition_not_allowed";
|
|
36
|
+
}
|
|
37
|
+
function personOwnedStatuses(rule, statuses) {
|
|
38
|
+
if (rule === undefined)
|
|
39
|
+
return [];
|
|
40
|
+
const reachable = new Set;
|
|
41
|
+
for (const [from, targets] of Object.entries(rule.agentTransitions)) {
|
|
42
|
+
if (targets.length > 0)
|
|
43
|
+
reachable.add(from);
|
|
44
|
+
for (const target of targets)
|
|
45
|
+
reachable.add(target);
|
|
46
|
+
}
|
|
47
|
+
return statuses.filter((status) => !reachable.has(status));
|
|
48
|
+
}
|
|
49
|
+
function checkSuppressed(suppressed, contact, company) {
|
|
50
|
+
if (contact.doNotContact === true || suppressed.contact.includes(contact.status) || company?.doNotContact === true || company !== undefined && suppressed.company.includes(company.status)) {
|
|
51
|
+
return "suppressed";
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
function checkDraft(policy, touch, counters, kind) {
|
|
56
|
+
if (kind === "first_touch") {
|
|
57
|
+
if (touch !== null)
|
|
58
|
+
return "already_contacted";
|
|
59
|
+
if (counters.firstTouch >= policy.weeklyLimit)
|
|
60
|
+
return "weekly_limit_reached";
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (touch === null)
|
|
64
|
+
return "not_contacted";
|
|
65
|
+
if (touch.followUps >= policy.maxFollowUps)
|
|
66
|
+
return "max_follow_ups_reached";
|
|
67
|
+
if (counters.followUp >= policy.followUpLimit)
|
|
68
|
+
return "follow_up_limit_reached";
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
function checkDraftedStatus(policy, recordType, currentStatus, next, touched, counters) {
|
|
72
|
+
if (policy.draftedStatus === undefined || recordType !== "contact" || next !== policy.draftedStatus || currentStatus === policy.draftedStatus || touched) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
return counters.firstTouch >= policy.weeklyLimit ? "weekly_limit_reached" : undefined;
|
|
76
|
+
}
|
|
77
|
+
function movesToDraftedStatus(policy, recordType, currentStatus, next) {
|
|
78
|
+
return policy.draftedStatus !== undefined && recordType === "contact" && next === policy.draftedStatus && currentStatus !== policy.draftedStatus;
|
|
79
|
+
}
|
|
80
|
+
function classifyOutreach(args, outreachConfigured) {
|
|
81
|
+
if (!outreachConfigured)
|
|
82
|
+
return;
|
|
83
|
+
if (args.activityType === CRM_OUTREACH_DRAFTED_ACTIVITY) {
|
|
84
|
+
if (args.recordType !== "contact") {
|
|
85
|
+
throw new CrmInputError("email_drafted must be logged on the contact, not the company");
|
|
86
|
+
}
|
|
87
|
+
const kind = args.attributes?.kind;
|
|
88
|
+
if (kind !== "first_touch" && kind !== "follow_up") {
|
|
89
|
+
throw new CrmInputError("email_drafted attributes.kind must be first_touch or follow_up");
|
|
90
|
+
}
|
|
91
|
+
const threadId = boundedAttribute(args.attributes?.threadId);
|
|
92
|
+
const draftId = boundedAttribute(args.attributes?.draftId);
|
|
93
|
+
return {
|
|
94
|
+
type: "drafted",
|
|
95
|
+
kind,
|
|
96
|
+
...threadId === undefined ? {} : { threadId },
|
|
97
|
+
...draftId === undefined ? {} : { draftId }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (args.recordType !== "contact")
|
|
101
|
+
return;
|
|
102
|
+
if (args.activityType === CRM_OUTREACH_SENT_ACTIVITY) {
|
|
103
|
+
const threadId = boundedAttribute(args.attributes?.threadId);
|
|
104
|
+
return threadId === undefined ? { type: "sent" } : { type: "sent", threadId };
|
|
105
|
+
}
|
|
106
|
+
if (args.activityType === CRM_OUTREACH_REPLY_ACTIVITY)
|
|
107
|
+
return { type: "reply" };
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
function checkNoteEdit(activity, unattributed) {
|
|
111
|
+
if (activity.activityType !== CRM_NOTE_ACTIVITY)
|
|
112
|
+
return "not_editable";
|
|
113
|
+
const by = activity.attributes?.by ?? unattributed;
|
|
114
|
+
return by === "agent" ? undefined : "not_editable";
|
|
115
|
+
}
|
|
116
|
+
function boundedAttribute(value) {
|
|
117
|
+
return typeof value === "string" && value.length > 0 && value.length <= CRM_MAX_ATTRIBUTE_STRING_CHARS ? value : undefined;
|
|
118
|
+
}
|
|
119
|
+
var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
120
|
+
|
|
121
|
+
class CrmOutreachTimezoneError extends Error {
|
|
122
|
+
constructor() {
|
|
123
|
+
super("CRM outreach timezone is unusable");
|
|
124
|
+
this.name = "CrmOutreachTimezoneError";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function outreachWeekStart(now, timezone) {
|
|
128
|
+
let parts;
|
|
129
|
+
try {
|
|
130
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
131
|
+
timeZone: timezone,
|
|
132
|
+
year: "numeric",
|
|
133
|
+
month: "2-digit",
|
|
134
|
+
day: "2-digit",
|
|
135
|
+
weekday: "short"
|
|
136
|
+
}).formatToParts(now);
|
|
137
|
+
} catch {
|
|
138
|
+
throw new CrmOutreachTimezoneError;
|
|
139
|
+
}
|
|
140
|
+
const part = (type) => parts.find((entry) => entry.type === type)?.value;
|
|
141
|
+
const weekday = WEEKDAYS.indexOf(part("weekday"));
|
|
142
|
+
const year = Number(part("year"));
|
|
143
|
+
const month = Number(part("month"));
|
|
144
|
+
const day = Number(part("day"));
|
|
145
|
+
if (weekday < 0 || !Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
|
146
|
+
throw new CrmOutreachTimezoneError;
|
|
147
|
+
}
|
|
148
|
+
return new Date(Date.UTC(year, month - 1, day - weekday)).toISOString().slice(0, 10);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { CrmPolicyRefusedError, checkStatusChange, personOwnedStatuses, checkSuppressed, checkDraft, checkDraftedStatus, movesToDraftedStatus, classifyOutreach, checkNoteEdit, outreachWeekStart };
|