@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.
- package/README.md +4 -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
|
@@ -1,30 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentImage,
|
|
3
|
+
agentImageTagRequired,
|
|
4
|
+
lambdaCode,
|
|
5
|
+
releaseAccess,
|
|
6
|
+
releaseImageRepositoryArn,
|
|
7
|
+
releaseSource
|
|
8
|
+
} from "./chunk-pxhftba1.js";
|
|
1
9
|
import {
|
|
2
10
|
CUSTOMIZATION_ARTIFACT_NAME,
|
|
3
11
|
FOUNDATION_VERSION_RE,
|
|
4
12
|
LIVE_ENDPOINT_NAME,
|
|
5
13
|
WEB_SEARCH_TARGET,
|
|
6
|
-
agentImage,
|
|
7
|
-
agentImageTagRequired,
|
|
8
14
|
capabilityEnabled,
|
|
15
|
+
instanceNames,
|
|
16
|
+
newsletterManifestFor,
|
|
17
|
+
provisionsIntegration,
|
|
18
|
+
requiresAuthenticatedQueue,
|
|
19
|
+
slackCommandPrefixes
|
|
20
|
+
} from "./chunk-rxetj2p9.js";
|
|
21
|
+
import {
|
|
9
22
|
capabilityScheduledJobs,
|
|
10
23
|
crmOutreachConfigured,
|
|
11
24
|
crmPolicyFingerprint,
|
|
12
25
|
encodeProactiveChannels,
|
|
13
|
-
instanceNames,
|
|
14
26
|
isScheduledJob,
|
|
15
|
-
lambdaCode,
|
|
16
|
-
newsletterManifestFor,
|
|
17
27
|
parseInstanceConfig,
|
|
18
28
|
parseSchedule,
|
|
19
|
-
provisionsIntegration,
|
|
20
|
-
releaseAccess,
|
|
21
|
-
releaseImageRepositoryArn,
|
|
22
|
-
releaseSource,
|
|
23
|
-
requiresAuthenticatedQueue,
|
|
24
29
|
scheduledJobBindings,
|
|
25
|
-
slackCommandPrefixes,
|
|
26
30
|
webhookJobBindings
|
|
27
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-96fjaysn.js";
|
|
28
32
|
|
|
29
33
|
// src/stacks/agent-stack.ts
|
|
30
34
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -221,20 +225,27 @@ class FoundationAgent extends cdk.Stack {
|
|
|
221
225
|
role.addToPolicy(new iam.PolicyStatement({
|
|
222
226
|
sid: "DenyDirectCrmTableAccess",
|
|
223
227
|
effect: iam.Effect.DENY,
|
|
224
|
-
actions: [
|
|
225
|
-
"dynamodb:GetItem",
|
|
226
|
-
"dynamodb:BatchGetItem",
|
|
227
|
-
"dynamodb:TransactGetItems",
|
|
228
|
-
"dynamodb:Query",
|
|
229
|
-
"dynamodb:Scan",
|
|
230
|
-
"dynamodb:PartiQLSelect",
|
|
231
|
-
"dynamodb:ExecuteStatement",
|
|
232
|
-
"dynamodb:BatchExecuteStatement",
|
|
233
|
-
"dynamodb:ExportTableToPointInTime"
|
|
234
|
-
],
|
|
228
|
+
actions: ["dynamodb:*"],
|
|
235
229
|
resources
|
|
236
230
|
}));
|
|
237
231
|
}
|
|
232
|
+
denyDirectCrmHttpSecretAccess(role, instance) {
|
|
233
|
+
if (!provisionsIntegration(instance, "crm") || instance.crm.provider.kind !== "http")
|
|
234
|
+
return;
|
|
235
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
236
|
+
sid: "DenyCrmHttpSecret",
|
|
237
|
+
effect: iam.Effect.DENY,
|
|
238
|
+
actions: ["secretsmanager:*"],
|
|
239
|
+
resources: [
|
|
240
|
+
cdk.Stack.of(this).formatArn({
|
|
241
|
+
service: "secretsmanager",
|
|
242
|
+
resource: "secret",
|
|
243
|
+
resourceName: `${this.names.secretCrmHttp}-??????`,
|
|
244
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
245
|
+
})
|
|
246
|
+
]
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
238
249
|
denyDirectKnockSecretAccess(role, props) {
|
|
239
250
|
if (!provisionsIntegration(props.instance, "knock"))
|
|
240
251
|
return;
|
|
@@ -385,6 +396,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
385
396
|
managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")]
|
|
386
397
|
});
|
|
387
398
|
this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
|
|
399
|
+
this.denyDirectCrmHttpSecretAccess(role, props.instance);
|
|
388
400
|
this.denyDirectKnockSecretAccess(role, props);
|
|
389
401
|
this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
|
|
390
402
|
this.denyDirectDocsSecretAccess(role, instanceNames(props.instance).secretGoogleDocs);
|
|
@@ -454,6 +466,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
454
466
|
description: `${props.instance.displayName} AgentCore execution role (least privilege)`
|
|
455
467
|
});
|
|
456
468
|
this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
|
|
469
|
+
this.denyDirectCrmHttpSecretAccess(role, props.instance);
|
|
457
470
|
this.denyDirectKnockSecretAccess(role, props);
|
|
458
471
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
459
472
|
this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
|
|
@@ -1550,12 +1563,14 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1550
1563
|
}
|
|
1551
1564
|
if (provisionsIntegration(props.instance, "crm")) {
|
|
1552
1565
|
const crm = crmConfigForProvisioning(props.configPath);
|
|
1566
|
+
const provider = props.instance.crm.provider;
|
|
1553
1567
|
const crmTable = props.crmTable;
|
|
1554
1568
|
if (crmTable === undefined)
|
|
1555
1569
|
throw new Error(`${id}: integrations.crm is enabled but no CRM table was supplied by FoundationData`);
|
|
1570
|
+
const usesTable = provider.kind === "dynamodb" || crm.outreach !== undefined;
|
|
1556
1571
|
const crmProxyRole = new iam2.Role(this, "CrmProxyRole", {
|
|
1557
1572
|
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1558
|
-
description: `${display} CRM proxy Lambda role (self-hosted CRM table access only)`,
|
|
1573
|
+
description: provider.kind === "dynamodb" ? `${display} CRM proxy Lambda role (self-hosted CRM table access only)` : `${display} CRM proxy Lambda role (crm/1 key and outreach ledger only)`,
|
|
1559
1574
|
managedPolicies: [
|
|
1560
1575
|
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1561
1576
|
]
|
|
@@ -1565,30 +1580,83 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1565
1580
|
actions: ["secretsmanager:GetSecretValue"],
|
|
1566
1581
|
resources: [secretArnPattern2(props.secrets.signing)]
|
|
1567
1582
|
}));
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1583
|
+
if (provider.kind === "dynamodb") {
|
|
1584
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1585
|
+
sid: "CrmTableData",
|
|
1586
|
+
actions: [
|
|
1587
|
+
"dynamodb:GetItem",
|
|
1588
|
+
"dynamodb:PutItem",
|
|
1589
|
+
"dynamodb:UpdateItem",
|
|
1590
|
+
"dynamodb:DeleteItem",
|
|
1591
|
+
"dynamodb:BatchGetItem",
|
|
1592
|
+
"dynamodb:BatchWriteItem",
|
|
1593
|
+
"dynamodb:TransactWriteItems",
|
|
1594
|
+
"dynamodb:ConditionCheckItem"
|
|
1595
|
+
],
|
|
1596
|
+
resources: [crmTable.tableArn]
|
|
1597
|
+
}));
|
|
1598
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1599
|
+
sid: "CrmRecordsIndex",
|
|
1600
|
+
actions: ["dynamodb:Query"],
|
|
1601
|
+
resources: [crmTable.tableArn, `${crmTable.tableArn}/index/RecordsIndex`]
|
|
1602
|
+
}));
|
|
1603
|
+
} else {
|
|
1604
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1605
|
+
sid: "CrmHttpSecret",
|
|
1606
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
1607
|
+
resources: [
|
|
1608
|
+
secretArnPattern2(secretsmanager.Secret.fromSecretNameV2(this, "CrmHttpSecret", names.secretCrmHttp))
|
|
1609
|
+
]
|
|
1610
|
+
}));
|
|
1611
|
+
if (usesTable)
|
|
1612
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1613
|
+
sid: "CrmOutreachLedger",
|
|
1614
|
+
actions: [
|
|
1615
|
+
"dynamodb:GetItem",
|
|
1616
|
+
"dynamodb:PutItem",
|
|
1617
|
+
"dynamodb:UpdateItem",
|
|
1618
|
+
"dynamodb:DeleteItem",
|
|
1619
|
+
"dynamodb:TransactWriteItems"
|
|
1620
|
+
],
|
|
1621
|
+
resources: [crmTable.tableArn],
|
|
1622
|
+
conditions: {
|
|
1623
|
+
"ForAllValues:StringEquals": {
|
|
1624
|
+
"dynamodb:LeadingKeys": [crmOutreachLedgerPartition(props.instance.name)]
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}));
|
|
1628
|
+
}
|
|
1629
|
+
if (usesTable)
|
|
1630
|
+
crmProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1631
|
+
sid: "CrmTableKey",
|
|
1632
|
+
actions: ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
|
|
1633
|
+
resources: [(crmTable.encryptionKey ?? props.dataKey).keyArn]
|
|
1634
|
+
}));
|
|
1635
|
+
const release = releaseVersionContext(this);
|
|
1636
|
+
const crmProxyEnvironment = {
|
|
1637
|
+
FOUNDATION_CRM_TABLE_NAME: crmTable.tableName,
|
|
1638
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
1639
|
+
FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
|
|
1640
|
+
FOUNDATION_CRM_TENANT_ID: props.instance.name,
|
|
1641
|
+
FOUNDATION_CRM_ALLOWED_CHANNELS: JSON.stringify(crm.channels),
|
|
1642
|
+
FOUNDATION_CRM_ALLOWED_STATUSES: JSON.stringify(crm.statuses),
|
|
1643
|
+
FOUNDATION_CRM_ACTIVITY_TYPES: JSON.stringify(crm.activityTypes),
|
|
1644
|
+
FOUNDATION_CRM_MAX_BATCH_SIZE: String(crm.maxBatchSize),
|
|
1645
|
+
FOUNDATION_CRM_STATUS_POLICY: JSON.stringify(crm.statusPolicy),
|
|
1646
|
+
FOUNDATION_CRM_DELETES: String(crm.deletes),
|
|
1647
|
+
...crm.outreach === undefined ? {} : {
|
|
1648
|
+
FOUNDATION_CRM_OUTREACH: JSON.stringify(crm.outreach),
|
|
1649
|
+
FOUNDATION_CRM_TIMEZONE: crm.timezone
|
|
1650
|
+
},
|
|
1651
|
+
...provider.kind === "http" ? {
|
|
1652
|
+
FOUNDATION_CRM_PROVIDER: "http",
|
|
1653
|
+
FOUNDATION_CRM_HTTP_BASE_URL: provider.baseUrl,
|
|
1654
|
+
FOUNDATION_CRM_HTTP_SECRET_ID: names.secretCrmHttp,
|
|
1655
|
+
FOUNDATION_CRM_HTTP_EXPECTED_TENANT: provider.expectedTenant,
|
|
1656
|
+
...release === undefined ? {} : { FOUNDATION_RELEASE: release }
|
|
1657
|
+
} : {}
|
|
1658
|
+
};
|
|
1659
|
+
assertLambdaEnvironmentBudget(crmProxyEnvironment);
|
|
1592
1660
|
this.crmProxyFunction = new lambda.Function(this, "CrmProxyFunction", {
|
|
1593
1661
|
functionName: names.crmProxyFunctionName,
|
|
1594
1662
|
runtime: lambda.Runtime.NODEJS_22_X,
|
|
@@ -1597,20 +1665,7 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1597
1665
|
role: crmProxyRole,
|
|
1598
1666
|
timeout: cdk2.Duration.seconds(30),
|
|
1599
1667
|
memorySize: 256,
|
|
1600
|
-
environment:
|
|
1601
|
-
FOUNDATION_CRM_TABLE_NAME: crmTable.tableName,
|
|
1602
|
-
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
1603
|
-
FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
|
|
1604
|
-
FOUNDATION_CRM_TENANT_ID: props.instance.name,
|
|
1605
|
-
FOUNDATION_CRM_ALLOWED_CHANNELS: JSON.stringify(crm.channels),
|
|
1606
|
-
FOUNDATION_CRM_ALLOWED_STATUSES: JSON.stringify(crm.statuses),
|
|
1607
|
-
FOUNDATION_CRM_ACTIVITY_TYPES: JSON.stringify(crm.activityTypes),
|
|
1608
|
-
FOUNDATION_CRM_MAX_BATCH_SIZE: String(crm.maxBatchSize),
|
|
1609
|
-
...crm.outreach === undefined ? {} : {
|
|
1610
|
-
FOUNDATION_CRM_OUTREACH: JSON.stringify(crm.outreach),
|
|
1611
|
-
FOUNDATION_CRM_TIMEZONE: crm.timezone
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1668
|
+
environment: crmProxyEnvironment
|
|
1614
1669
|
});
|
|
1615
1670
|
new cdk2.CfnOutput(this, "CrmProxyFunctionArn", {
|
|
1616
1671
|
value: this.crmProxyFunction.functionArn
|
|
@@ -1660,6 +1715,12 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1660
1715
|
agentRuntimeArn: props.agentRuntimeArn,
|
|
1661
1716
|
alarmAction
|
|
1662
1717
|
});
|
|
1718
|
+
if (this.crmProxyFunction !== undefined && props.instance.crm.provider.kind === "http")
|
|
1719
|
+
addCrmProviderAlarms(this, {
|
|
1720
|
+
instance: props.instance,
|
|
1721
|
+
functionName: names.crmProxyFunctionName,
|
|
1722
|
+
alarmAction
|
|
1723
|
+
});
|
|
1663
1724
|
let dispatcherFunctionName;
|
|
1664
1725
|
if (configuredScheduledJobs.length > 0) {
|
|
1665
1726
|
const dispatchRole = new iam2.Role(this, "ScheduledJobDispatcherRole", {
|
|
@@ -1782,34 +1843,13 @@ function addConfigLoadAlarm(stack, args) {
|
|
|
1782
1843
|
return;
|
|
1783
1844
|
const runtimeId = cdk2.Fn.select(1, cdk2.Fn.split("/", args.agentRuntimeArn));
|
|
1784
1845
|
const logGroupName = `/aws/bedrock-agentcore/runtimes/${runtimeId}-${LIVE_ENDPOINT_NAME}`;
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
description: `${args.instance.displayName} creates the runtime's live log group when missing`,
|
|
1788
|
-
managedPolicies: [
|
|
1789
|
-
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1790
|
-
]
|
|
1791
|
-
});
|
|
1792
|
-
role.addToPolicy(new iam2.PolicyStatement({
|
|
1846
|
+
const ensureLogGroup = ensureLogGroupExists(stack, {
|
|
1847
|
+
id: "AgentLiveLogGroup",
|
|
1793
1848
|
sid: "CreateRuntimeLiveLogGroup",
|
|
1794
|
-
actions: ["logs:CreateLogGroup"],
|
|
1795
|
-
resources: [
|
|
1796
|
-
`arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:${logGroupName}`
|
|
1797
|
-
]
|
|
1798
|
-
}));
|
|
1799
|
-
const ensureFunction = new lambda.Function(stack, "AgentLiveLogGroupFunction", {
|
|
1800
|
-
runtime: lambda.Runtime.NODEJS_22_X,
|
|
1801
|
-
handler: "index.runtimeLogGroupHandler",
|
|
1802
|
-
code: lambdaCode(stack, "gateway"),
|
|
1803
|
-
role,
|
|
1804
|
-
timeout: cdk2.Duration.seconds(30),
|
|
1805
|
-
memorySize: 128
|
|
1806
|
-
});
|
|
1807
|
-
const ensureLogGroup = new cdk2.CustomResource(stack, "AgentLiveLogGroup", {
|
|
1808
|
-
serviceToken: ensureFunction.functionArn,
|
|
1809
1849
|
resourceType: "Custom::FoundationRuntimeLogGroup",
|
|
1810
|
-
|
|
1850
|
+
description: `${args.instance.displayName} creates the runtime's live log group when missing`,
|
|
1851
|
+
logGroupName
|
|
1811
1852
|
});
|
|
1812
|
-
ensureLogGroup.node.addDependency(role);
|
|
1813
1853
|
const filter = new logs2.MetricFilter(stack, "ConfigLoadFailureMetricFilter", {
|
|
1814
1854
|
logGroup: logs2.LogGroup.fromLogGroupName(stack, "AgentLiveLogs", logGroupName),
|
|
1815
1855
|
filterPattern: logs2.FilterPattern.any(...CONFIG_LOAD_FAILURE_EVENTS.map((event) => logs2.FilterPattern.stringValue("$.event", "=", event))),
|
|
@@ -1835,22 +1875,136 @@ function addConfigLoadAlarm(stack, args) {
|
|
|
1835
1875
|
alarm.addAlarmAction(args.alarmAction);
|
|
1836
1876
|
return alarm;
|
|
1837
1877
|
}
|
|
1878
|
+
function ensureLogGroupExists(stack, args) {
|
|
1879
|
+
const role = new iam2.Role(stack, `${args.id}Role`, {
|
|
1880
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1881
|
+
description: args.description,
|
|
1882
|
+
managedPolicies: [
|
|
1883
|
+
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1884
|
+
]
|
|
1885
|
+
});
|
|
1886
|
+
role.addToPolicy(new iam2.PolicyStatement({
|
|
1887
|
+
sid: args.sid,
|
|
1888
|
+
actions: ["logs:CreateLogGroup"],
|
|
1889
|
+
resources: [
|
|
1890
|
+
`arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:${args.logGroupName}`
|
|
1891
|
+
]
|
|
1892
|
+
}));
|
|
1893
|
+
const ensureFunction = new lambda.Function(stack, `${args.id}Function`, {
|
|
1894
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
1895
|
+
handler: "index.runtimeLogGroupHandler",
|
|
1896
|
+
code: lambdaCode(stack, "gateway"),
|
|
1897
|
+
role,
|
|
1898
|
+
timeout: cdk2.Duration.seconds(30),
|
|
1899
|
+
memorySize: 128
|
|
1900
|
+
});
|
|
1901
|
+
const ensureLogGroup = new cdk2.CustomResource(stack, args.id, {
|
|
1902
|
+
serviceToken: ensureFunction.functionArn,
|
|
1903
|
+
resourceType: args.resourceType,
|
|
1904
|
+
properties: { LogGroupName: args.logGroupName }
|
|
1905
|
+
});
|
|
1906
|
+
ensureLogGroup.node.addDependency(role);
|
|
1907
|
+
return ensureLogGroup;
|
|
1908
|
+
}
|
|
1909
|
+
var CRM_PROVIDER_ALARMS = [
|
|
1910
|
+
{
|
|
1911
|
+
event: "crm_proxy",
|
|
1912
|
+
code: "tenant_mismatch",
|
|
1913
|
+
name: "TenantMismatch",
|
|
1914
|
+
description: "The CRM answered as a different tenant from crm.provider.expectedTenant, so the CRM proxy refused the reply and the CRM tools fail closed. Check that the key written with crm-key was issued for that tenant, and that expectedTenant is right."
|
|
1915
|
+
},
|
|
1916
|
+
{
|
|
1917
|
+
event: "crm_proxy",
|
|
1918
|
+
code: "upstream_unauthorized",
|
|
1919
|
+
name: "UpstreamUnauthorized",
|
|
1920
|
+
description: "The CRM refused the CRM proxy's key (401), even after the proxy re-read it: revoked, expired or wrong. Issue a new key in the CRM and run foundation-deploy crm-key; the proxy picks it up on its next call."
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
event: "crm_proxy",
|
|
1924
|
+
code: "upstream_forbidden",
|
|
1925
|
+
name: "UpstreamForbidden",
|
|
1926
|
+
description: "The CRM answered 403: a scope the key lacks, the CRM's kill switch, or a WAF in front of it blocking the proxy. Check the key's scopes, the switch and the WAF's logs."
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
event: "crm_proxy",
|
|
1930
|
+
code: "unconfigured",
|
|
1931
|
+
name: "Unconfigured",
|
|
1932
|
+
description: "The CRM proxy is unconfigured, so every CRM tool fails closed: no key written yet (run foundation-deploy crm-key), or the CRM's /meta does not match the deployment. The log line's check field names which: contract, statuses, activity_types, batch_size or operations."
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
event: "crm_proxy_warning",
|
|
1936
|
+
code: "ledger_kept",
|
|
1937
|
+
name: "LedgerKept",
|
|
1938
|
+
description: "A draft's outreach reservation was kept because the CRM's answer was unknown (a timeout, a 5xx, a 429 or a reply without the contract header). The weekly cap counts it, failing closed. Check in the CRM whether that draft was logged."
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
event: "crm_proxy_warning",
|
|
1942
|
+
code: "policy_mismatch",
|
|
1943
|
+
name: "PolicyMismatch",
|
|
1944
|
+
description: "The CRM's own policy in /meta differs from the deployment's capabilities.crm (the log line's fields name which). Each side enforces its own, so the stricter wins; align the two."
|
|
1945
|
+
}
|
|
1946
|
+
];
|
|
1947
|
+
function addCrmProviderAlarms(stack, args) {
|
|
1948
|
+
const logGroupName = `/aws/lambda/${args.functionName}`;
|
|
1949
|
+
const ensureLogGroup = ensureLogGroupExists(stack, {
|
|
1950
|
+
id: "CrmProxyLogGroup",
|
|
1951
|
+
sid: "CreateCrmProxyLogGroup",
|
|
1952
|
+
resourceType: "Custom::FoundationLambdaLogGroup",
|
|
1953
|
+
description: `${args.instance.displayName} creates the CRM proxy's log group when missing`,
|
|
1954
|
+
logGroupName
|
|
1955
|
+
});
|
|
1956
|
+
const logGroup = logs2.LogGroup.fromLogGroupName(stack, "CrmProxyLogs", logGroupName);
|
|
1957
|
+
return CRM_PROVIDER_ALARMS.map((entry) => {
|
|
1958
|
+
const filter = new logs2.MetricFilter(stack, `CrmProxy${entry.name}MetricFilter`, {
|
|
1959
|
+
logGroup,
|
|
1960
|
+
filterPattern: logs2.FilterPattern.all(logs2.FilterPattern.stringValue("$.event", "=", entry.event), logs2.FilterPattern.stringValue("$.code", "=", entry.code)),
|
|
1961
|
+
metricNamespace: `Foundation/${args.instance.naming.prefix}`,
|
|
1962
|
+
metricName: `CrmProxy${entry.name}`,
|
|
1963
|
+
metricValue: "1"
|
|
1964
|
+
});
|
|
1965
|
+
filter.node.addDependency(ensureLogGroup);
|
|
1966
|
+
const alarm = new cloudwatch.Alarm(stack, `CrmProxy${entry.name}Alarm`, {
|
|
1967
|
+
alarmDescription: `${args.instance.displayName} CRM proxy logged ${entry.code}. ${entry.description}`,
|
|
1968
|
+
metric: filter.metric({
|
|
1969
|
+
statistic: "Sum",
|
|
1970
|
+
period: cdk2.Duration.minutes(entry.code === "policy_mismatch" ? 60 : 5)
|
|
1971
|
+
}),
|
|
1972
|
+
threshold: 1,
|
|
1973
|
+
evaluationPeriods: 1,
|
|
1974
|
+
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
|
|
1975
|
+
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
|
|
1976
|
+
});
|
|
1977
|
+
alarm.addAlarmAction(args.alarmAction);
|
|
1978
|
+
return alarm;
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
function crmOutreachLedgerPartition(tenantId) {
|
|
1982
|
+
return `TENANT#${tenantId}#OUTREACH`;
|
|
1983
|
+
}
|
|
1984
|
+
function releaseVersionContext(scope) {
|
|
1985
|
+
const explicit = String(scope.node.tryGetContext("foundationVersion") ?? "");
|
|
1986
|
+
const version = explicit !== "" ? explicit : releaseSource(scope)?.version;
|
|
1987
|
+
return version !== undefined && FOUNDATION_VERSION_RE.test(version) ? version : undefined;
|
|
1988
|
+
}
|
|
1838
1989
|
function crmConfigForProvisioning(configPath) {
|
|
1839
1990
|
const config = parseInstanceConfig(readFileSync(configPath, "utf8"));
|
|
1840
1991
|
const crm = config.capabilities.crm;
|
|
1841
1992
|
const missing = [
|
|
1842
1993
|
...crm.enabled ? [] : ["capabilities.crm.enabled"],
|
|
1843
1994
|
...crm.channels.length > 0 ? [] : ["capabilities.crm.channels"],
|
|
1844
|
-
...crm.statuses.length > 0 ? [] : ["capabilities.crm.statuses"],
|
|
1995
|
+
...crm.statuses.company.length > 0 ? [] : ["capabilities.crm.statuses.company"],
|
|
1996
|
+
...crm.statuses.contact.length > 0 ? [] : ["capabilities.crm.statuses.contact"],
|
|
1845
1997
|
...crm.activityTypes.length > 0 ? [] : ["capabilities.crm.activityTypes"]
|
|
1846
1998
|
];
|
|
1847
1999
|
if (missing.length > 0)
|
|
1848
2000
|
throw new Error(`${configPath}: integrations.crm requires a configured CRM capability (${missing.join(", ")})`);
|
|
1849
2001
|
return {
|
|
1850
2002
|
channels: [...crm.channels],
|
|
1851
|
-
statuses: [...crm.statuses],
|
|
2003
|
+
statuses: { company: [...crm.statuses.company], contact: [...crm.statuses.contact] },
|
|
1852
2004
|
activityTypes: [...crm.activityTypes],
|
|
1853
2005
|
maxBatchSize: crm.maxBatchSize,
|
|
2006
|
+
statusPolicy: crm.statusPolicy,
|
|
2007
|
+
deletes: crm.deletes,
|
|
1854
2008
|
policyFingerprint: crmPolicyFingerprint(crm),
|
|
1855
2009
|
...crmOutreachConfigured(crm.outreach) ? { outreach: crm.outreach } : {},
|
|
1856
2010
|
timezone: config.timezone
|
|
@@ -1929,7 +2083,7 @@ function assertLambdaEnvironmentBudget(values) {
|
|
|
1929
2083
|
bytes += cdk2.Token.isUnresolved(value) ? LAMBDA_ENVIRONMENT_TOKEN_BYTES : Buffer.byteLength(value);
|
|
1930
2084
|
}
|
|
1931
2085
|
if (bytes > LAMBDA_ENVIRONMENT_MAX_BYTES) {
|
|
1932
|
-
throw new Error(`
|
|
2086
|
+
throw new Error(`A Lambda environment needs an estimated ${bytes} bytes; the AWS limit is ${LAMBDA_ENVIRONMENT_MAX_BYTES}. Shorten job prompts or reduce configured values.`);
|
|
1933
2087
|
}
|
|
1934
2088
|
}
|
|
1935
2089
|
function scheduledJobsFor(configPath) {
|
|
@@ -2120,6 +2274,7 @@ class FoundationData extends cdk4.Stack {
|
|
|
2120
2274
|
crmTable;
|
|
2121
2275
|
upworkApprovalTable;
|
|
2122
2276
|
etradePendingTable;
|
|
2277
|
+
crmHttpSecret;
|
|
2123
2278
|
secrets;
|
|
2124
2279
|
constructor(scope, id, props) {
|
|
2125
2280
|
super(scope, id, props);
|
|
@@ -2238,6 +2393,15 @@ class FoundationData extends cdk4.Stack {
|
|
|
2238
2393
|
generateStringKey: "placeholder"
|
|
2239
2394
|
}
|
|
2240
2395
|
}) : undefined;
|
|
2396
|
+
this.crmHttpSecret = provisionsIntegration(props.instance, "crm") && props.instance.crm.provider.kind === "http" ? new secretsmanager2.Secret(this, "CrmHttpSecret", {
|
|
2397
|
+
secretName: names.secretCrmHttp,
|
|
2398
|
+
description: `crm/1 key for ${display}'s CRM ({ api_key }); written by foundation-deploy crm-key, read only by the CRM proxy`,
|
|
2399
|
+
removalPolicy: cdk4.RemovalPolicy.DESTROY,
|
|
2400
|
+
generateSecretString: {
|
|
2401
|
+
secretStringTemplate: "{}",
|
|
2402
|
+
generateStringKey: "placeholder"
|
|
2403
|
+
}
|
|
2404
|
+
}) : undefined;
|
|
2241
2405
|
const upwork = provisionsIntegration(props.instance, "upwork") ? secretShell("UpworkSecret", names.secretUpwork, `Upwork OAuth client and token store for ${display} ({ client_id, client_secret, access_token?, refresh_token?, expires_at?, tenant_id? })`) : undefined;
|
|
2242
2406
|
const etradeClient = provisionsIntegration(props.instance, "etrade") ? new secretsmanager2.Secret(this, "EtradeClientSecret", {
|
|
2243
2407
|
secretName: names.secretEtradeClient,
|