@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.
@@ -1,36 +1,104 @@
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-fjdgxw8h.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-gnz747z2.js";
31
+ } from "./chunk-96fjaysn.js";
28
32
 
29
33
  // src/stacks/agent-stack.ts
30
34
  import * as cdk from "aws-cdk-lib";
31
35
  import * as ecr from "aws-cdk-lib/aws-ecr";
32
36
  import * as iam from "aws-cdk-lib/aws-iam";
33
37
  import * as logs from "aws-cdk-lib/aws-logs";
38
+
39
+ // src/stacks/secret-bearing-reads.ts
40
+ var SECRET_BEARING_READ_ACTIONS = [
41
+ "wafv2:GetSampledRequests",
42
+ "waf:GetSampledRequests",
43
+ "waf-regional:GetSampledRequests",
44
+ "lambda:GetFunction",
45
+ "lambda:GetFunctionConfiguration",
46
+ "lambda:ListFunctions",
47
+ "lambda:ListVersionsByFunction",
48
+ "lambda:GetLayerVersion",
49
+ "lambda:GetLayerVersionByArn",
50
+ "ecs:DescribeTaskDefinition",
51
+ "apprunner:DescribeService",
52
+ "batch:DescribeJobDefinitions",
53
+ "batch:DescribeJobs",
54
+ "codebuild:BatchGetProjects",
55
+ "codebuild:BatchGetBuilds",
56
+ "codebuild:BatchGetBuildBatches",
57
+ "amplify:GetApp",
58
+ "amplify:ListApps",
59
+ "amplify:GetBranch",
60
+ "amplify:ListBranches",
61
+ "amplify:GetBackendEnvironment",
62
+ "elasticbeanstalk:DescribeConfigurationSettings",
63
+ "ec2:DescribeLaunchTemplateVersions",
64
+ "ec2:DescribeInstanceAttribute",
65
+ "ssm:GetParameter",
66
+ "ssm:GetParameters",
67
+ "ssm:GetParametersByPath",
68
+ "ssm:GetParameterHistory",
69
+ "cloudformation:GetTemplate",
70
+ "appconfig:GetConfiguration",
71
+ "appconfig:GetHostedConfigurationVersion",
72
+ "glue:GetJob",
73
+ "glue:GetJobs",
74
+ "glue:BatchGetJobs",
75
+ "glue:GetConnection",
76
+ "glue:GetConnections",
77
+ "glue:GetDevEndpoint",
78
+ "glue:GetDevEndpoints",
79
+ "glue:BatchGetDevEndpoints",
80
+ "sagemaker:DescribeModel",
81
+ "sagemaker:DescribeTrainingJob",
82
+ "sagemaker:DescribeProcessingJob",
83
+ "sagemaker:DescribeTransformJob",
84
+ "datapipeline:GetPipelineDefinition",
85
+ "appsync:ListApiKeys",
86
+ "appsync:GetDataSource",
87
+ "appsync:ListDataSources",
88
+ "kinesisanalytics:DescribeApplication",
89
+ "lightsail:GetBucketAccessKeys",
90
+ "lightsail:GetContainerServices",
91
+ "lightsail:GetContainerServiceDeployments",
92
+ "opsworks:DescribeApps",
93
+ "opsworks:DescribeStacks",
94
+ "cognito-identity:GetCredentialsForIdentity",
95
+ "cognito-identity:GetOpenIdToken",
96
+ "cognito-identity:GetOpenIdTokenForDeveloperIdentity",
97
+ "connect:GetFederationToken",
98
+ "codeartifact:GetAuthorizationToken"
99
+ ];
100
+
101
+ // src/stacks/agent-stack.ts
34
102
  class FoundationAgent extends cdk.Stack {
35
103
  repository;
36
104
  executionRole;
@@ -73,6 +141,7 @@ class FoundationAgent extends cdk.Stack {
73
141
  this.accessPoint = accessPoint;
74
142
  this.executionRole = this.buildExecutionRole(props);
75
143
  this.readOnlyRole = this.buildReadOnlyRole(props);
144
+ this.secretBearingReadsPolicy(props.instance, [this.executionRole, this.readOnlyRole]);
76
145
  this.buildSpansIngestionPolicy();
77
146
  const gateway = this.buildToolGateway(display);
78
147
  this.webSearchUrl = gateway.getAtt("GatewayUrl").toString();
@@ -221,20 +290,27 @@ class FoundationAgent extends cdk.Stack {
221
290
  role.addToPolicy(new iam.PolicyStatement({
222
291
  sid: "DenyDirectCrmTableAccess",
223
292
  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
- ],
293
+ actions: ["dynamodb:*"],
235
294
  resources
236
295
  }));
237
296
  }
297
+ denyDirectCrmHttpSecretAccess(role, instance) {
298
+ if (!provisionsIntegration(instance, "crm") || instance.crm.provider.kind !== "http")
299
+ return;
300
+ role.addToPolicy(new iam.PolicyStatement({
301
+ sid: "DenyCrmHttpSecret",
302
+ effect: iam.Effect.DENY,
303
+ actions: ["secretsmanager:*"],
304
+ resources: [
305
+ cdk.Stack.of(this).formatArn({
306
+ service: "secretsmanager",
307
+ resource: "secret",
308
+ resourceName: `${this.names.secretCrmHttp}-??????`,
309
+ arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
310
+ })
311
+ ]
312
+ }));
313
+ }
238
314
  denyDirectKnockSecretAccess(role, props) {
239
315
  if (!provisionsIntegration(props.instance, "knock"))
240
316
  return;
@@ -385,6 +461,7 @@ class FoundationAgent extends cdk.Stack {
385
461
  managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("ReadOnlyAccess")]
386
462
  });
387
463
  this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
464
+ this.denyDirectCrmHttpSecretAccess(role, props.instance);
388
465
  this.denyDirectKnockSecretAccess(role, props);
389
466
  this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
390
467
  this.denyDirectDocsSecretAccess(role, instanceNames(props.instance).secretGoogleDocs);
@@ -427,6 +504,20 @@ class FoundationAgent extends cdk.Stack {
427
504
  }));
428
505
  return role;
429
506
  }
507
+ secretBearingReadsPolicy(instance, roles) {
508
+ new iam.ManagedPolicy(this, "AgentSecretBearingReadsDeny", {
509
+ description: `${instance.displayName} agent: no AWS read whose response carries a secret`,
510
+ roles,
511
+ statements: [
512
+ new iam.PolicyStatement({
513
+ sid: "DenySecretBearingReads",
514
+ effect: iam.Effect.DENY,
515
+ actions: [...SECRET_BEARING_READ_ACTIONS],
516
+ resources: ["*"]
517
+ })
518
+ ]
519
+ });
520
+ }
430
521
  buildSpansIngestionPolicy() {
431
522
  return new logs.ResourcePolicy(this, "AgentSpansIngestion", {
432
523
  resourcePolicyName: `${this.stackName}-AgentSpansFromXray`,
@@ -454,6 +545,7 @@ class FoundationAgent extends cdk.Stack {
454
545
  description: `${props.instance.displayName} AgentCore execution role (least privilege)`
455
546
  });
456
547
  this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
548
+ this.denyDirectCrmHttpSecretAccess(role, props.instance);
457
549
  this.denyDirectKnockSecretAccess(role, props);
458
550
  this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
459
551
  this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
@@ -1550,12 +1642,14 @@ class FoundationApi extends cdk2.Stack {
1550
1642
  }
1551
1643
  if (provisionsIntegration(props.instance, "crm")) {
1552
1644
  const crm = crmConfigForProvisioning(props.configPath);
1645
+ const provider = props.instance.crm.provider;
1553
1646
  const crmTable = props.crmTable;
1554
1647
  if (crmTable === undefined)
1555
1648
  throw new Error(`${id}: integrations.crm is enabled but no CRM table was supplied by FoundationData`);
1649
+ const usesTable = provider.kind === "dynamodb" || crm.outreach !== undefined;
1556
1650
  const crmProxyRole = new iam2.Role(this, "CrmProxyRole", {
1557
1651
  assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
1558
- description: `${display} CRM proxy Lambda role (self-hosted CRM table access only)`,
1652
+ 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
1653
  managedPolicies: [
1560
1654
  iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
1561
1655
  ]
@@ -1565,30 +1659,83 @@ class FoundationApi extends cdk2.Stack {
1565
1659
  actions: ["secretsmanager:GetSecretValue"],
1566
1660
  resources: [secretArnPattern2(props.secrets.signing)]
1567
1661
  }));
1568
- crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1569
- sid: "CrmTableData",
1570
- actions: [
1571
- "dynamodb:GetItem",
1572
- "dynamodb:PutItem",
1573
- "dynamodb:UpdateItem",
1574
- "dynamodb:DeleteItem",
1575
- "dynamodb:BatchGetItem",
1576
- "dynamodb:BatchWriteItem",
1577
- "dynamodb:TransactWriteItems",
1578
- "dynamodb:ConditionCheckItem"
1579
- ],
1580
- resources: [crmTable.tableArn]
1581
- }));
1582
- crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1583
- sid: "CrmRecordsIndex",
1584
- actions: ["dynamodb:Query"],
1585
- resources: [crmTable.tableArn, `${crmTable.tableArn}/index/RecordsIndex`]
1586
- }));
1587
- crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1588
- sid: "CrmTableKey",
1589
- actions: ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
1590
- resources: [(crmTable.encryptionKey ?? props.dataKey).keyArn]
1591
- }));
1662
+ if (provider.kind === "dynamodb") {
1663
+ crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1664
+ sid: "CrmTableData",
1665
+ actions: [
1666
+ "dynamodb:GetItem",
1667
+ "dynamodb:PutItem",
1668
+ "dynamodb:UpdateItem",
1669
+ "dynamodb:DeleteItem",
1670
+ "dynamodb:BatchGetItem",
1671
+ "dynamodb:BatchWriteItem",
1672
+ "dynamodb:TransactWriteItems",
1673
+ "dynamodb:ConditionCheckItem"
1674
+ ],
1675
+ resources: [crmTable.tableArn]
1676
+ }));
1677
+ crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1678
+ sid: "CrmRecordsIndex",
1679
+ actions: ["dynamodb:Query"],
1680
+ resources: [crmTable.tableArn, `${crmTable.tableArn}/index/RecordsIndex`]
1681
+ }));
1682
+ } else {
1683
+ crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1684
+ sid: "CrmHttpSecret",
1685
+ actions: ["secretsmanager:GetSecretValue"],
1686
+ resources: [
1687
+ secretArnPattern2(secretsmanager.Secret.fromSecretNameV2(this, "CrmHttpSecret", names.secretCrmHttp))
1688
+ ]
1689
+ }));
1690
+ if (usesTable)
1691
+ crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1692
+ sid: "CrmOutreachLedger",
1693
+ actions: [
1694
+ "dynamodb:GetItem",
1695
+ "dynamodb:PutItem",
1696
+ "dynamodb:UpdateItem",
1697
+ "dynamodb:DeleteItem",
1698
+ "dynamodb:TransactWriteItems"
1699
+ ],
1700
+ resources: [crmTable.tableArn],
1701
+ conditions: {
1702
+ "ForAllValues:StringEquals": {
1703
+ "dynamodb:LeadingKeys": [crmOutreachLedgerPartition(props.instance.name)]
1704
+ }
1705
+ }
1706
+ }));
1707
+ }
1708
+ if (usesTable)
1709
+ crmProxyRole.addToPolicy(new iam2.PolicyStatement({
1710
+ sid: "CrmTableKey",
1711
+ actions: ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey", "kms:DescribeKey"],
1712
+ resources: [(crmTable.encryptionKey ?? props.dataKey).keyArn]
1713
+ }));
1714
+ const release = releaseVersionContext(this);
1715
+ const crmProxyEnvironment = {
1716
+ FOUNDATION_CRM_TABLE_NAME: crmTable.tableName,
1717
+ FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
1718
+ FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
1719
+ FOUNDATION_CRM_TENANT_ID: props.instance.name,
1720
+ FOUNDATION_CRM_ALLOWED_CHANNELS: JSON.stringify(crm.channels),
1721
+ FOUNDATION_CRM_ALLOWED_STATUSES: JSON.stringify(crm.statuses),
1722
+ FOUNDATION_CRM_ACTIVITY_TYPES: JSON.stringify(crm.activityTypes),
1723
+ FOUNDATION_CRM_MAX_BATCH_SIZE: String(crm.maxBatchSize),
1724
+ FOUNDATION_CRM_STATUS_POLICY: JSON.stringify(crm.statusPolicy),
1725
+ FOUNDATION_CRM_DELETES: String(crm.deletes),
1726
+ ...crm.outreach === undefined ? {} : {
1727
+ FOUNDATION_CRM_OUTREACH: JSON.stringify(crm.outreach),
1728
+ FOUNDATION_CRM_TIMEZONE: crm.timezone
1729
+ },
1730
+ ...provider.kind === "http" ? {
1731
+ FOUNDATION_CRM_PROVIDER: "http",
1732
+ FOUNDATION_CRM_HTTP_BASE_URL: provider.baseUrl,
1733
+ FOUNDATION_CRM_HTTP_SECRET_ID: names.secretCrmHttp,
1734
+ FOUNDATION_CRM_HTTP_EXPECTED_TENANT: provider.expectedTenant,
1735
+ ...release === undefined ? {} : { FOUNDATION_RELEASE: release }
1736
+ } : {}
1737
+ };
1738
+ assertLambdaEnvironmentBudget(crmProxyEnvironment);
1592
1739
  this.crmProxyFunction = new lambda.Function(this, "CrmProxyFunction", {
1593
1740
  functionName: names.crmProxyFunctionName,
1594
1741
  runtime: lambda.Runtime.NODEJS_22_X,
@@ -1597,20 +1744,7 @@ class FoundationApi extends cdk2.Stack {
1597
1744
  role: crmProxyRole,
1598
1745
  timeout: cdk2.Duration.seconds(30),
1599
1746
  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
- }
1747
+ environment: crmProxyEnvironment
1614
1748
  });
1615
1749
  new cdk2.CfnOutput(this, "CrmProxyFunctionArn", {
1616
1750
  value: this.crmProxyFunction.functionArn
@@ -1660,6 +1794,12 @@ class FoundationApi extends cdk2.Stack {
1660
1794
  agentRuntimeArn: props.agentRuntimeArn,
1661
1795
  alarmAction
1662
1796
  });
1797
+ if (this.crmProxyFunction !== undefined && props.instance.crm.provider.kind === "http")
1798
+ addCrmProviderAlarms(this, {
1799
+ instance: props.instance,
1800
+ functionName: names.crmProxyFunctionName,
1801
+ alarmAction
1802
+ });
1663
1803
  let dispatcherFunctionName;
1664
1804
  if (configuredScheduledJobs.length > 0) {
1665
1805
  const dispatchRole = new iam2.Role(this, "ScheduledJobDispatcherRole", {
@@ -1782,34 +1922,13 @@ function addConfigLoadAlarm(stack, args) {
1782
1922
  return;
1783
1923
  const runtimeId = cdk2.Fn.select(1, cdk2.Fn.split("/", args.agentRuntimeArn));
1784
1924
  const logGroupName = `/aws/bedrock-agentcore/runtimes/${runtimeId}-${LIVE_ENDPOINT_NAME}`;
1785
- const role = new iam2.Role(stack, "AgentLiveLogGroupRole", {
1786
- assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
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({
1925
+ const ensureLogGroup = ensureLogGroupExists(stack, {
1926
+ id: "AgentLiveLogGroup",
1793
1927
  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
1928
  resourceType: "Custom::FoundationRuntimeLogGroup",
1810
- properties: { LogGroupName: logGroupName }
1929
+ description: `${args.instance.displayName} creates the runtime's live log group when missing`,
1930
+ logGroupName
1811
1931
  });
1812
- ensureLogGroup.node.addDependency(role);
1813
1932
  const filter = new logs2.MetricFilter(stack, "ConfigLoadFailureMetricFilter", {
1814
1933
  logGroup: logs2.LogGroup.fromLogGroupName(stack, "AgentLiveLogs", logGroupName),
1815
1934
  filterPattern: logs2.FilterPattern.any(...CONFIG_LOAD_FAILURE_EVENTS.map((event) => logs2.FilterPattern.stringValue("$.event", "=", event))),
@@ -1835,22 +1954,136 @@ function addConfigLoadAlarm(stack, args) {
1835
1954
  alarm.addAlarmAction(args.alarmAction);
1836
1955
  return alarm;
1837
1956
  }
1957
+ function ensureLogGroupExists(stack, args) {
1958
+ const role = new iam2.Role(stack, `${args.id}Role`, {
1959
+ assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
1960
+ description: args.description,
1961
+ managedPolicies: [
1962
+ iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
1963
+ ]
1964
+ });
1965
+ role.addToPolicy(new iam2.PolicyStatement({
1966
+ sid: args.sid,
1967
+ actions: ["logs:CreateLogGroup"],
1968
+ resources: [
1969
+ `arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:${args.logGroupName}`
1970
+ ]
1971
+ }));
1972
+ const ensureFunction = new lambda.Function(stack, `${args.id}Function`, {
1973
+ runtime: lambda.Runtime.NODEJS_22_X,
1974
+ handler: "index.runtimeLogGroupHandler",
1975
+ code: lambdaCode(stack, "gateway"),
1976
+ role,
1977
+ timeout: cdk2.Duration.seconds(30),
1978
+ memorySize: 128
1979
+ });
1980
+ const ensureLogGroup = new cdk2.CustomResource(stack, args.id, {
1981
+ serviceToken: ensureFunction.functionArn,
1982
+ resourceType: args.resourceType,
1983
+ properties: { LogGroupName: args.logGroupName }
1984
+ });
1985
+ ensureLogGroup.node.addDependency(role);
1986
+ return ensureLogGroup;
1987
+ }
1988
+ var CRM_PROVIDER_ALARMS = [
1989
+ {
1990
+ event: "crm_proxy",
1991
+ code: "tenant_mismatch",
1992
+ name: "TenantMismatch",
1993
+ 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."
1994
+ },
1995
+ {
1996
+ event: "crm_proxy",
1997
+ code: "upstream_unauthorized",
1998
+ name: "UpstreamUnauthorized",
1999
+ 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."
2000
+ },
2001
+ {
2002
+ event: "crm_proxy",
2003
+ code: "upstream_forbidden",
2004
+ name: "UpstreamForbidden",
2005
+ 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."
2006
+ },
2007
+ {
2008
+ event: "crm_proxy",
2009
+ code: "unconfigured",
2010
+ name: "Unconfigured",
2011
+ 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."
2012
+ },
2013
+ {
2014
+ event: "crm_proxy_warning",
2015
+ code: "ledger_kept",
2016
+ name: "LedgerKept",
2017
+ 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."
2018
+ },
2019
+ {
2020
+ event: "crm_proxy_warning",
2021
+ code: "policy_mismatch",
2022
+ name: "PolicyMismatch",
2023
+ 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."
2024
+ }
2025
+ ];
2026
+ function addCrmProviderAlarms(stack, args) {
2027
+ const logGroupName = `/aws/lambda/${args.functionName}`;
2028
+ const ensureLogGroup = ensureLogGroupExists(stack, {
2029
+ id: "CrmProxyLogGroup",
2030
+ sid: "CreateCrmProxyLogGroup",
2031
+ resourceType: "Custom::FoundationLambdaLogGroup",
2032
+ description: `${args.instance.displayName} creates the CRM proxy's log group when missing`,
2033
+ logGroupName
2034
+ });
2035
+ const logGroup = logs2.LogGroup.fromLogGroupName(stack, "CrmProxyLogs", logGroupName);
2036
+ return CRM_PROVIDER_ALARMS.map((entry) => {
2037
+ const filter = new logs2.MetricFilter(stack, `CrmProxy${entry.name}MetricFilter`, {
2038
+ logGroup,
2039
+ filterPattern: logs2.FilterPattern.all(logs2.FilterPattern.stringValue("$.event", "=", entry.event), logs2.FilterPattern.stringValue("$.code", "=", entry.code)),
2040
+ metricNamespace: `Foundation/${args.instance.naming.prefix}`,
2041
+ metricName: `CrmProxy${entry.name}`,
2042
+ metricValue: "1"
2043
+ });
2044
+ filter.node.addDependency(ensureLogGroup);
2045
+ const alarm = new cloudwatch.Alarm(stack, `CrmProxy${entry.name}Alarm`, {
2046
+ alarmDescription: `${args.instance.displayName} CRM proxy logged ${entry.code}. ${entry.description}`,
2047
+ metric: filter.metric({
2048
+ statistic: "Sum",
2049
+ period: cdk2.Duration.minutes(entry.code === "policy_mismatch" ? 60 : 5)
2050
+ }),
2051
+ threshold: 1,
2052
+ evaluationPeriods: 1,
2053
+ comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
2054
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
2055
+ });
2056
+ alarm.addAlarmAction(args.alarmAction);
2057
+ return alarm;
2058
+ });
2059
+ }
2060
+ function crmOutreachLedgerPartition(tenantId) {
2061
+ return `TENANT#${tenantId}#OUTREACH`;
2062
+ }
2063
+ function releaseVersionContext(scope) {
2064
+ const explicit = String(scope.node.tryGetContext("foundationVersion") ?? "");
2065
+ const version = explicit !== "" ? explicit : releaseSource(scope)?.version;
2066
+ return version !== undefined && FOUNDATION_VERSION_RE.test(version) ? version : undefined;
2067
+ }
1838
2068
  function crmConfigForProvisioning(configPath) {
1839
2069
  const config = parseInstanceConfig(readFileSync(configPath, "utf8"));
1840
2070
  const crm = config.capabilities.crm;
1841
2071
  const missing = [
1842
2072
  ...crm.enabled ? [] : ["capabilities.crm.enabled"],
1843
2073
  ...crm.channels.length > 0 ? [] : ["capabilities.crm.channels"],
1844
- ...crm.statuses.length > 0 ? [] : ["capabilities.crm.statuses"],
2074
+ ...crm.statuses.company.length > 0 ? [] : ["capabilities.crm.statuses.company"],
2075
+ ...crm.statuses.contact.length > 0 ? [] : ["capabilities.crm.statuses.contact"],
1845
2076
  ...crm.activityTypes.length > 0 ? [] : ["capabilities.crm.activityTypes"]
1846
2077
  ];
1847
2078
  if (missing.length > 0)
1848
2079
  throw new Error(`${configPath}: integrations.crm requires a configured CRM capability (${missing.join(", ")})`);
1849
2080
  return {
1850
2081
  channels: [...crm.channels],
1851
- statuses: [...crm.statuses],
2082
+ statuses: { company: [...crm.statuses.company], contact: [...crm.statuses.contact] },
1852
2083
  activityTypes: [...crm.activityTypes],
1853
2084
  maxBatchSize: crm.maxBatchSize,
2085
+ statusPolicy: crm.statusPolicy,
2086
+ deletes: crm.deletes,
1854
2087
  policyFingerprint: crmPolicyFingerprint(crm),
1855
2088
  ...crmOutreachConfigured(crm.outreach) ? { outreach: crm.outreach } : {},
1856
2089
  timezone: config.timezone
@@ -1929,7 +2162,7 @@ function assertLambdaEnvironmentBudget(values) {
1929
2162
  bytes += cdk2.Token.isUnresolved(value) ? LAMBDA_ENVIRONMENT_TOKEN_BYTES : Buffer.byteLength(value);
1930
2163
  }
1931
2164
  if (bytes > LAMBDA_ENVIRONMENT_MAX_BYTES) {
1932
- throw new Error(`Gateway Lambda environment needs an estimated ${bytes} bytes; the AWS limit is ${LAMBDA_ENVIRONMENT_MAX_BYTES}. Shorten job prompts or reduce configured values.`);
2165
+ 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
2166
  }
1934
2167
  }
1935
2168
  function scheduledJobsFor(configPath) {
@@ -2120,6 +2353,7 @@ class FoundationData extends cdk4.Stack {
2120
2353
  crmTable;
2121
2354
  upworkApprovalTable;
2122
2355
  etradePendingTable;
2356
+ crmHttpSecret;
2123
2357
  secrets;
2124
2358
  constructor(scope, id, props) {
2125
2359
  super(scope, id, props);
@@ -2238,6 +2472,15 @@ class FoundationData extends cdk4.Stack {
2238
2472
  generateStringKey: "placeholder"
2239
2473
  }
2240
2474
  }) : undefined;
2475
+ this.crmHttpSecret = provisionsIntegration(props.instance, "crm") && props.instance.crm.provider.kind === "http" ? new secretsmanager2.Secret(this, "CrmHttpSecret", {
2476
+ secretName: names.secretCrmHttp,
2477
+ description: `crm/1 key for ${display}'s CRM ({ api_key }); written by foundation-deploy crm-key, read only by the CRM proxy`,
2478
+ removalPolicy: cdk4.RemovalPolicy.DESTROY,
2479
+ generateSecretString: {
2480
+ secretStringTemplate: "{}",
2481
+ generateStringKey: "placeholder"
2482
+ }
2483
+ }) : undefined;
2241
2484
  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
2485
  const etradeClient = provisionsIntegration(props.instance, "etrade") ? new secretsmanager2.Secret(this, "EtradeClientSecret", {
2243
2486
  secretName: names.secretEtradeClient,
@@ -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-efqdhd2e.js";
13
+ } from "../chunk-spq5kr1t.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-gnz747z2.js";
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-fjdgxw8h.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.51",
3
+ "version": "0.2.53",
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",