@deployfoundation/foundation-deploy 0.2.2 → 0.2.3
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/agent-image/Dockerfile +1 -0
- package/dist/bin/app.js +7 -3
- package/dist/bin/foundation-deploy.js +8 -2
- package/dist/bin/release-account.js +2 -2
- package/dist/{chunk-an8qhf3t.js → chunk-7h6zgsa5.js} +1 -1
- package/dist/{chunk-gcg67ck3.js → chunk-kf1yvx1w.js} +72 -12
- package/dist/{chunk-q9sxg76n.js → chunk-my0awr1g.js} +327 -28
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
- package/src/artifacts.ts +1 -0
- package/src/deploy/assets/slack-app-manifest.yml +6 -0
- package/src/deploy/deploy.ts +6 -0
- package/src/deploy/runtime-secret.ts +12 -3
- package/src/stacks/agent-stack.ts +68 -0
- package/src/stacks/api-stack.ts +336 -48
- package/src/stacks/data-stack.ts +44 -0
|
@@ -8,15 +8,18 @@ import {
|
|
|
8
8
|
crmOutreachConfigured,
|
|
9
9
|
crmPolicyFingerprint,
|
|
10
10
|
instanceNames,
|
|
11
|
+
isScheduledJob,
|
|
11
12
|
lambdaCode,
|
|
12
13
|
newsletterManifestFor,
|
|
13
14
|
parseInstanceConfig,
|
|
15
|
+
parseSchedule,
|
|
14
16
|
provisionsIntegration,
|
|
15
17
|
releaseImageRepositoryArn,
|
|
16
18
|
requiresAuthenticatedQueue,
|
|
19
|
+
scheduledJobBindings,
|
|
17
20
|
slackCommandPrefixes,
|
|
18
21
|
webhookJobBindings
|
|
19
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-kf1yvx1w.js";
|
|
20
23
|
|
|
21
24
|
// src/stacks/agent-stack.ts
|
|
22
25
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -243,6 +246,34 @@ class FoundationAgent extends cdk.Stack {
|
|
|
243
246
|
resources: [upworkApprovalTable.tableArn, `${upworkApprovalTable.tableArn}/index/*`]
|
|
244
247
|
}));
|
|
245
248
|
}
|
|
249
|
+
denyDirectEtradeAccess(role, props) {
|
|
250
|
+
const retainedSecretArn = (name) => cdk.Stack.of(this).formatArn({
|
|
251
|
+
service: "secretsmanager",
|
|
252
|
+
resource: "secret",
|
|
253
|
+
resourceName: `${name}-??????`,
|
|
254
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
255
|
+
});
|
|
256
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
257
|
+
sid: "DenyEtradeSecrets",
|
|
258
|
+
effect: iam.Effect.DENY,
|
|
259
|
+
actions: ["secretsmanager:*"],
|
|
260
|
+
resources: [
|
|
261
|
+
retainedSecretArn(this.names.secretEtradeClient),
|
|
262
|
+
retainedSecretArn(this.names.secretEtradeCredential)
|
|
263
|
+
]
|
|
264
|
+
}));
|
|
265
|
+
if (!provisionsIntegration(props.instance, "etrade"))
|
|
266
|
+
return;
|
|
267
|
+
const pending = props.etradePendingTable;
|
|
268
|
+
if (pending === undefined)
|
|
269
|
+
throw new Error(`${this.stackName}: integrations.etrade is enabled but FoundationData supplied no E*TRADE pending-state table`);
|
|
270
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
271
|
+
sid: "DenyEtradePendingState",
|
|
272
|
+
effect: iam.Effect.DENY,
|
|
273
|
+
actions: ["dynamodb:*"],
|
|
274
|
+
resources: [pending.tableArn, `${pending.tableArn}/index/*`]
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
246
277
|
buildReadOnlyRole(props) {
|
|
247
278
|
const role = new iam.Role(this, "AgentReadOnlyRole", {
|
|
248
279
|
roleName: `${props.instance.naming.prefix}ReadOnlyRole`,
|
|
@@ -254,6 +285,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
254
285
|
this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
|
|
255
286
|
this.denyDirectKnockSecretAccess(role, props);
|
|
256
287
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
288
|
+
this.denyDirectEtradeAccess(role, props);
|
|
257
289
|
role.addToPolicy(new iam.PolicyStatement({
|
|
258
290
|
sid: "DenyAgentCoreBrowserMetadata",
|
|
259
291
|
effect: iam.Effect.DENY,
|
|
@@ -292,6 +324,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
292
324
|
this.denyDirectCrmTableAccess(role, props.instance, props.crmTable);
|
|
293
325
|
this.denyDirectKnockSecretAccess(role, props);
|
|
294
326
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
327
|
+
this.denyDirectEtradeAccess(role, props);
|
|
295
328
|
const releaseRepositoryArn = releaseImageRepositoryArn(this);
|
|
296
329
|
role.addToPolicy(new iam.PolicyStatement({
|
|
297
330
|
sid: "EcrPull",
|
|
@@ -456,6 +489,20 @@ class FoundationAgent extends cdk.Stack {
|
|
|
456
489
|
]
|
|
457
490
|
}));
|
|
458
491
|
}
|
|
492
|
+
if (provisionsIntegration(props.instance, "etrade")) {
|
|
493
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
494
|
+
sid: "EtradeProxyInvoke",
|
|
495
|
+
actions: ["lambda:InvokeFunction"],
|
|
496
|
+
resources: [
|
|
497
|
+
cdk.Stack.of(this).formatArn({
|
|
498
|
+
service: "lambda",
|
|
499
|
+
resource: "function",
|
|
500
|
+
resourceName: this.names.etradeProxyFunctionName,
|
|
501
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
502
|
+
})
|
|
503
|
+
]
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
459
506
|
if (provisionsIntegration(props.instance, "crm")) {
|
|
460
507
|
role.addToPolicy(new iam.PolicyStatement({
|
|
461
508
|
sid: "CrmProxyInvoke",
|
|
@@ -550,6 +597,11 @@ class FoundationAgent extends cdk.Stack {
|
|
|
550
597
|
`arn:${this.partition}:scheduler:${this.region}:${this.account}:schedule/${this.names.routineGroup}/*`
|
|
551
598
|
]
|
|
552
599
|
}));
|
|
600
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
601
|
+
sid: "RoutineScheduleList",
|
|
602
|
+
actions: ["scheduler:ListSchedules"],
|
|
603
|
+
resources: ["*"]
|
|
604
|
+
}));
|
|
553
605
|
role.addToPolicy(new iam.PolicyStatement({
|
|
554
606
|
sid: "PassRoutineSchedulerRole",
|
|
555
607
|
actions: ["iam:PassRole"],
|
|
@@ -612,6 +664,13 @@ function secretArnPattern(secret) {
|
|
|
612
664
|
|
|
613
665
|
// src/stacks/api-stack.ts
|
|
614
666
|
import { readFileSync } from "node:fs";
|
|
667
|
+
|
|
668
|
+
// ../core/src/actor-capability.ts
|
|
669
|
+
var ACTOR_CAPABILITY_TTL_SECONDS = 15 * 60;
|
|
670
|
+
var JOB_ACTOR = "config";
|
|
671
|
+
var JOB_ACTOR_CAPABILITY_TTL_SECONDS = 60 * 60;
|
|
672
|
+
|
|
673
|
+
// src/stacks/api-stack.ts
|
|
615
674
|
import * as cdk2 from "aws-cdk-lib";
|
|
616
675
|
import * as apigateway from "aws-cdk-lib/aws-apigateway";
|
|
617
676
|
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
|
|
@@ -635,6 +694,7 @@ class FoundationApi extends cdk2.Stack {
|
|
|
635
694
|
crmProxyFunction;
|
|
636
695
|
knockProxyFunction;
|
|
637
696
|
upworkProxyFunction;
|
|
697
|
+
etradeProxyFunction;
|
|
638
698
|
constructor(scope, id, props) {
|
|
639
699
|
super(scope, id, props);
|
|
640
700
|
const names = instanceNames(props.instance);
|
|
@@ -644,7 +704,18 @@ class FoundationApi extends cdk2.Stack {
|
|
|
644
704
|
const upworkApprovalTable = upwork === undefined ? undefined : props.upworkApprovalTable;
|
|
645
705
|
if (upwork !== undefined && (upworkSecret === undefined || upworkApprovalTable === undefined))
|
|
646
706
|
throw new Error(`${id}: integrations.upwork is enabled but its secret or approval table was not supplied by FoundationData`);
|
|
707
|
+
const etrade = provisionsIntegration(props.instance, "etrade") ? etradeConfigForProvisioning(props.configPath) : undefined;
|
|
708
|
+
const etradeClientSecret = etrade === undefined ? undefined : props.secrets.etradeClient;
|
|
709
|
+
const etradeCredentialSecret = etrade === undefined ? undefined : props.secrets.etradeCredential;
|
|
710
|
+
const etradePendingTable = etrade === undefined ? undefined : props.etradePendingTable;
|
|
711
|
+
if (etrade !== undefined && (etradeClientSecret === undefined || etradeCredentialSecret === undefined || etradePendingTable === undefined))
|
|
712
|
+
throw new Error(`${id}: integrations.etrade is enabled but its client secret, credential secret, or pending OAuth table was not supplied by FoundationData`);
|
|
647
713
|
const authenticatedQueue = requiresAuthenticatedQueue(props.instance);
|
|
714
|
+
const configuredScheduledJobs = scheduledJobsFor(props.configPath);
|
|
715
|
+
const scheduledJobs = scheduledJobBindings(configuredScheduledJobs);
|
|
716
|
+
const scheduledJobTimezone = configTimezoneFor(props.configPath);
|
|
717
|
+
const etradeAllowedChannels = etrade === undefined ? "" : JSON.stringify(etrade.channels);
|
|
718
|
+
const etradeAllowedUsers = etrade === undefined ? "" : JSON.stringify(etrade.users);
|
|
648
719
|
this.gatewayDlq = new sqs.Queue(this, "GatewayDlq", {
|
|
649
720
|
encryption: sqs.QueueEncryption.KMS,
|
|
650
721
|
encryptionMasterKey: props.dataKey,
|
|
@@ -711,6 +782,38 @@ class FoundationApi extends cdk2.Stack {
|
|
|
711
782
|
resources: [secretArnPattern2(upworkSecret)]
|
|
712
783
|
}));
|
|
713
784
|
}
|
|
785
|
+
if (etradeClientSecret !== undefined && etradeCredentialSecret !== undefined && etradePendingTable !== undefined) {
|
|
786
|
+
gatewayRole.addToPolicy(new iam2.PolicyStatement({
|
|
787
|
+
sid: "EtradeOAuthClientRead",
|
|
788
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
789
|
+
resources: [secretArnPattern2(etradeClientSecret)]
|
|
790
|
+
}));
|
|
791
|
+
gatewayRole.addToPolicy(new iam2.PolicyStatement({
|
|
792
|
+
sid: "EtradeOAuthCredentialWrite",
|
|
793
|
+
actions: [
|
|
794
|
+
"secretsmanager:DescribeSecret",
|
|
795
|
+
"secretsmanager:PutSecretValue",
|
|
796
|
+
"secretsmanager:UpdateSecretVersionStage"
|
|
797
|
+
],
|
|
798
|
+
resources: [secretArnPattern2(etradeCredentialSecret)]
|
|
799
|
+
}));
|
|
800
|
+
gatewayRole.addToPolicy(new iam2.PolicyStatement({
|
|
801
|
+
sid: "DenyEtradeOAuthCredentialRead",
|
|
802
|
+
effect: iam2.Effect.DENY,
|
|
803
|
+
actions: ["secretsmanager:GetSecretValue", "secretsmanager:BatchGetSecretValue"],
|
|
804
|
+
resources: [secretArnPattern2(etradeCredentialSecret)]
|
|
805
|
+
}));
|
|
806
|
+
gatewayRole.addToPolicy(new iam2.PolicyStatement({
|
|
807
|
+
sid: "EtradeOAuthPendingState",
|
|
808
|
+
actions: [
|
|
809
|
+
"dynamodb:GetItem",
|
|
810
|
+
"dynamodb:PutItem",
|
|
811
|
+
"dynamodb:UpdateItem",
|
|
812
|
+
"dynamodb:DeleteItem"
|
|
813
|
+
],
|
|
814
|
+
resources: [etradePendingTable.tableArn]
|
|
815
|
+
}));
|
|
816
|
+
}
|
|
714
817
|
gatewayRole.addToPolicy(new iam2.PolicyStatement({
|
|
715
818
|
sid: "DedupeAndThreadIndex",
|
|
716
819
|
actions: ["dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem"],
|
|
@@ -745,6 +848,41 @@ class FoundationApi extends cdk2.Stack {
|
|
|
745
848
|
});
|
|
746
849
|
if (jobsWebhookSecret !== undefined)
|
|
747
850
|
jobsWebhookSecret.grantRead(gatewayRole);
|
|
851
|
+
const gatewayEnvironment = {
|
|
852
|
+
FOUNDATION_TABLE_NAME: props.table.tableName,
|
|
853
|
+
FOUNDATION_INVOKE_QUEUE_URL: this.invokeQueue.queueUrl,
|
|
854
|
+
FOUNDATION_GATEWAY_DLQ_URL: this.gatewayDlq.queueUrl,
|
|
855
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
856
|
+
FOUNDATION_SLACK_APP_SECRET_ID: props.secrets.slackApp.secretName,
|
|
857
|
+
FOUNDATION_ADMINS: props.admins,
|
|
858
|
+
FOUNDATION_AGENT_RUNTIME_ARN: props.agentRuntimeArn,
|
|
859
|
+
FOUNDATION_DISPLAY_NAME: props.instance.displayName,
|
|
860
|
+
FOUNDATION_ITEMS_TABLE_NAME: props.itemsTable.tableName,
|
|
861
|
+
FOUNDATION_GOOGLE_CALENDAR_SECRET_ID: props.secrets.googleCalendar.secretName,
|
|
862
|
+
FOUNDATION_GOOGLE_OAUTH_SECRET_ID: props.secrets.googleOauth.secretName,
|
|
863
|
+
FOUNDATION_GOOGLE_DRIVE_SECRET_ID: props.secrets.googleDrive.secretName,
|
|
864
|
+
FOUNDATION_GOOGLE_EMAIL_SECRET_ID: props.secrets.googleEmail.secretName,
|
|
865
|
+
...props.secrets.knockOauthClient === undefined ? {} : { FOUNDATION_KNOCK_OAUTH_CLIENT_SECRET_ID: props.secrets.knockOauthClient.secretName },
|
|
866
|
+
...props.secrets.knockCredential === undefined ? {} : { FOUNDATION_KNOCK_CREDENTIAL_SECRET_ID: props.secrets.knockCredential.secretName },
|
|
867
|
+
...upworkSecret === undefined ? {} : { FOUNDATION_UPWORK_SECRET_ID: upworkSecret.secretName },
|
|
868
|
+
...etradeClientSecret === undefined || etradeCredentialSecret === undefined || etradePendingTable === undefined || etrade === undefined ? {} : {
|
|
869
|
+
FOUNDATION_ETRADE_CLIENT_SECRET_ID: etradeClientSecret.secretName,
|
|
870
|
+
FOUNDATION_ETRADE_CREDENTIAL_SECRET_ID: etradeCredentialSecret.secretName,
|
|
871
|
+
FOUNDATION_ETRADE_PENDING_TABLE_NAME: etradePendingTable.tableName,
|
|
872
|
+
FOUNDATION_ETRADE_ALLOWED_CHANNELS: etradeAllowedChannels,
|
|
873
|
+
FOUNDATION_ETRADE_ALLOWED_USERS: etradeAllowedUsers
|
|
874
|
+
},
|
|
875
|
+
FOUNDATION_INSTANCE: props.instance.name,
|
|
876
|
+
FOUNDATION_COMMAND_PREFIXES: slackCommandPrefixes(props.instance).join(","),
|
|
877
|
+
FOUNDATION_PROACTIVE_CHANNELS: props.instance.proactive.channels.join(","),
|
|
878
|
+
FOUNDATION_PROACTIVE_MAX_PER_HOUR: String(props.instance.proactive.maxPerHour),
|
|
879
|
+
...jobsWebhookSecret === undefined ? {} : {
|
|
880
|
+
FOUNDATION_WEBHOOK_JOBS: JSON.stringify(webhookJobs),
|
|
881
|
+
FOUNDATION_JOBS_WEBHOOK_SECRET_ID: jobsWebhookSecret.secretName
|
|
882
|
+
},
|
|
883
|
+
FOUNDATION_SCHEDULED_JOB_BINDINGS: JSON.stringify(scheduledJobs)
|
|
884
|
+
};
|
|
885
|
+
assertLambdaEnvironmentBudget(gatewayEnvironment);
|
|
748
886
|
const gatewayFunction = new lambda.Function(this, "GatewayFunction", {
|
|
749
887
|
...authenticatedQueue ? { functionName: names.slackGatewayFunctionName } : {},
|
|
750
888
|
runtime: lambda.Runtime.NODEJS_22_X,
|
|
@@ -754,32 +892,7 @@ class FoundationApi extends cdk2.Stack {
|
|
|
754
892
|
timeout: cdk2.Duration.seconds(10),
|
|
755
893
|
memorySize: 256,
|
|
756
894
|
deadLetterQueue: this.gatewayDlq,
|
|
757
|
-
environment:
|
|
758
|
-
FOUNDATION_TABLE_NAME: props.table.tableName,
|
|
759
|
-
FOUNDATION_INVOKE_QUEUE_URL: this.invokeQueue.queueUrl,
|
|
760
|
-
FOUNDATION_GATEWAY_DLQ_URL: this.gatewayDlq.queueUrl,
|
|
761
|
-
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
762
|
-
FOUNDATION_SLACK_APP_SECRET_ID: props.secrets.slackApp.secretName,
|
|
763
|
-
FOUNDATION_ADMINS: props.admins,
|
|
764
|
-
FOUNDATION_AGENT_RUNTIME_ARN: props.agentRuntimeArn,
|
|
765
|
-
FOUNDATION_DISPLAY_NAME: props.instance.displayName,
|
|
766
|
-
FOUNDATION_ITEMS_TABLE_NAME: props.itemsTable.tableName,
|
|
767
|
-
FOUNDATION_GOOGLE_CALENDAR_SECRET_ID: props.secrets.googleCalendar.secretName,
|
|
768
|
-
FOUNDATION_GOOGLE_OAUTH_SECRET_ID: props.secrets.googleOauth.secretName,
|
|
769
|
-
FOUNDATION_GOOGLE_DRIVE_SECRET_ID: props.secrets.googleDrive.secretName,
|
|
770
|
-
FOUNDATION_GOOGLE_EMAIL_SECRET_ID: props.secrets.googleEmail.secretName,
|
|
771
|
-
...props.secrets.knockOauthClient === undefined ? {} : { FOUNDATION_KNOCK_OAUTH_CLIENT_SECRET_ID: props.secrets.knockOauthClient.secretName },
|
|
772
|
-
...props.secrets.knockCredential === undefined ? {} : { FOUNDATION_KNOCK_CREDENTIAL_SECRET_ID: props.secrets.knockCredential.secretName },
|
|
773
|
-
...upworkSecret === undefined ? {} : { FOUNDATION_UPWORK_SECRET_ID: upworkSecret.secretName },
|
|
774
|
-
FOUNDATION_INSTANCE: props.instance.name,
|
|
775
|
-
FOUNDATION_COMMAND_PREFIXES: slackCommandPrefixes(props.instance).join(","),
|
|
776
|
-
FOUNDATION_PROACTIVE_CHANNELS: props.instance.proactive.channels.join(","),
|
|
777
|
-
FOUNDATION_PROACTIVE_MAX_PER_HOUR: String(props.instance.proactive.maxPerHour),
|
|
778
|
-
...jobsWebhookSecret === undefined ? {} : {
|
|
779
|
-
FOUNDATION_WEBHOOK_JOBS: JSON.stringify(webhookJobs),
|
|
780
|
-
FOUNDATION_JOBS_WEBHOOK_SECRET_ID: jobsWebhookSecret.secretName
|
|
781
|
-
}
|
|
782
|
-
}
|
|
895
|
+
environment: gatewayEnvironment
|
|
783
896
|
});
|
|
784
897
|
gatewayFunction.configureAsyncInvoke({
|
|
785
898
|
retryAttempts: 0,
|
|
@@ -1103,6 +1216,51 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1103
1216
|
value: this.upworkProxyFunction.functionArn
|
|
1104
1217
|
});
|
|
1105
1218
|
}
|
|
1219
|
+
if (etradeClientSecret !== undefined && etradeCredentialSecret !== undefined && etrade !== undefined) {
|
|
1220
|
+
const etradeProxyRole = new iam2.Role(this, "EtradeProxyRole", {
|
|
1221
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1222
|
+
description: `${display} E*TRADE fixed read-only proxy Lambda`,
|
|
1223
|
+
managedPolicies: [
|
|
1224
|
+
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1225
|
+
]
|
|
1226
|
+
});
|
|
1227
|
+
etradeProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1228
|
+
sid: "EtradeProxyOAuthSecrets",
|
|
1229
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
1230
|
+
resources: [
|
|
1231
|
+
secretArnPattern2(etradeClientSecret),
|
|
1232
|
+
secretArnPattern2(etradeCredentialSecret)
|
|
1233
|
+
]
|
|
1234
|
+
}));
|
|
1235
|
+
etradeProxyRole.addToPolicy(new iam2.PolicyStatement({
|
|
1236
|
+
sid: "EtradeProxySigningSecret",
|
|
1237
|
+
actions: ["secretsmanager:GetSecretValue"],
|
|
1238
|
+
resources: [secretArnPattern2(props.secrets.signing)]
|
|
1239
|
+
}));
|
|
1240
|
+
const etradeProxyEnvironment = {
|
|
1241
|
+
FOUNDATION_ETRADE_CLIENT_SECRET_ID: etradeClientSecret.secretName,
|
|
1242
|
+
FOUNDATION_ETRADE_CREDENTIAL_SECRET_ID: etradeCredentialSecret.secretName,
|
|
1243
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
1244
|
+
FOUNDATION_SLACK_TEAM_ID: props.instance.slack.teamId,
|
|
1245
|
+
FOUNDATION_ETRADE_ALLOWED_CHANNELS: JSON.stringify(etrade.channels),
|
|
1246
|
+
FOUNDATION_ETRADE_ALLOWED_USERS: JSON.stringify(etrade.users),
|
|
1247
|
+
FOUNDATION_ADMINS: props.admins
|
|
1248
|
+
};
|
|
1249
|
+
assertLambdaEnvironmentBudget(etradeProxyEnvironment);
|
|
1250
|
+
this.etradeProxyFunction = new lambda.Function(this, "EtradeProxyFunction", {
|
|
1251
|
+
functionName: names.etradeProxyFunctionName,
|
|
1252
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
1253
|
+
handler: "index.handler",
|
|
1254
|
+
code: lambdaCode(this, "etrade-proxy"),
|
|
1255
|
+
role: etradeProxyRole,
|
|
1256
|
+
timeout: cdk2.Duration.seconds(30),
|
|
1257
|
+
memorySize: 256,
|
|
1258
|
+
environment: etradeProxyEnvironment
|
|
1259
|
+
});
|
|
1260
|
+
new cdk2.CfnOutput(this, "EtradeProxyFunctionArn", {
|
|
1261
|
+
value: this.etradeProxyFunction.functionArn
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1106
1264
|
if (provisionsIntegration(props.instance, "crm")) {
|
|
1107
1265
|
const crm = crmConfigForProvisioning(props.configPath);
|
|
1108
1266
|
const crmTable = props.crmTable;
|
|
@@ -1129,7 +1287,8 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1129
1287
|
"dynamodb:DeleteItem",
|
|
1130
1288
|
"dynamodb:BatchGetItem",
|
|
1131
1289
|
"dynamodb:BatchWriteItem",
|
|
1132
|
-
"dynamodb:TransactWriteItems"
|
|
1290
|
+
"dynamodb:TransactWriteItems",
|
|
1291
|
+
"dynamodb:ConditionCheckItem"
|
|
1133
1292
|
],
|
|
1134
1293
|
resources: [crmTable.tableArn]
|
|
1135
1294
|
}));
|
|
@@ -1201,6 +1360,69 @@ class FoundationApi extends cdk2.Stack {
|
|
|
1201
1360
|
});
|
|
1202
1361
|
alarm.addAlarmAction(alarmAction);
|
|
1203
1362
|
}
|
|
1363
|
+
if (configuredScheduledJobs.length > 0) {
|
|
1364
|
+
const dispatchRole = new iam2.Role(this, "ScheduledJobDispatcherRole", {
|
|
1365
|
+
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
1366
|
+
description: `${display} deployment-owned scheduled job dispatcher`,
|
|
1367
|
+
managedPolicies: [
|
|
1368
|
+
iam2.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
|
|
1369
|
+
]
|
|
1370
|
+
});
|
|
1371
|
+
props.secrets.signing.grantRead(dispatchRole);
|
|
1372
|
+
gatewayFunction.grantInvoke(dispatchRole);
|
|
1373
|
+
const dispatchFunction = new lambda.Function(this, "ScheduledJobDispatcherFunction", {
|
|
1374
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
1375
|
+
handler: "index.scheduledJobDispatcherHandler",
|
|
1376
|
+
code: lambdaCode(this, "gateway"),
|
|
1377
|
+
role: dispatchRole,
|
|
1378
|
+
timeout: cdk2.Duration.seconds(30),
|
|
1379
|
+
memorySize: 128,
|
|
1380
|
+
deadLetterQueue: this.gatewayDlq,
|
|
1381
|
+
environment: {
|
|
1382
|
+
FOUNDATION_SLACK_SIGNING_SECRET_ID: props.secrets.signing.secretName,
|
|
1383
|
+
FOUNDATION_GATEWAY_FUNCTION_NAME: gatewayFunction.functionName
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
dispatchFunction.configureAsyncInvoke({
|
|
1387
|
+
retryAttempts: 0,
|
|
1388
|
+
maxEventAge: cdk2.Duration.minutes(15)
|
|
1389
|
+
});
|
|
1390
|
+
const jobGroup = new scheduler.CfnScheduleGroup(this, "ScheduledJobGroup");
|
|
1391
|
+
const jobSchedulerRole = new iam2.Role(this, "ScheduledJobSchedulerRole", {
|
|
1392
|
+
assumedBy: new iam2.ServicePrincipal("scheduler.amazonaws.com", {
|
|
1393
|
+
conditions: { StringEquals: { "aws:SourceAccount": this.account } }
|
|
1394
|
+
}),
|
|
1395
|
+
description: `${display} deployment-owned scheduled job invoker`
|
|
1396
|
+
});
|
|
1397
|
+
dispatchFunction.grantInvoke(jobSchedulerRole);
|
|
1398
|
+
for (const job of configuredScheduledJobs) {
|
|
1399
|
+
const parsed = parseSchedule(job.on.schedule);
|
|
1400
|
+
if (!parsed.ok)
|
|
1401
|
+
throw new Error(`Scheduled job ${job.id} has an invalid schedule`);
|
|
1402
|
+
const schedule = new scheduler.CfnSchedule(this, `ScheduledJob-${job.id}`, {
|
|
1403
|
+
groupName: jobGroup.ref,
|
|
1404
|
+
scheduleExpression: job.on.schedule,
|
|
1405
|
+
scheduleExpressionTimezone: scheduledJobTimezone,
|
|
1406
|
+
flexibleTimeWindow: { mode: "OFF" },
|
|
1407
|
+
...parsed.kind === "at" ? { actionAfterCompletion: "DELETE" } : {},
|
|
1408
|
+
target: {
|
|
1409
|
+
arn: dispatchFunction.functionArn,
|
|
1410
|
+
roleArn: jobSchedulerRole.roleArn,
|
|
1411
|
+
input: JSON.stringify({
|
|
1412
|
+
internal: "scheduled_job",
|
|
1413
|
+
routineId: `job-${job.id}`,
|
|
1414
|
+
name: job.name,
|
|
1415
|
+
channel: job.channel,
|
|
1416
|
+
prompt: job.prompt,
|
|
1417
|
+
createdBy: JOB_ACTOR,
|
|
1418
|
+
scheduledTime: "<aws.scheduler.scheduled-time>"
|
|
1419
|
+
}),
|
|
1420
|
+
retryPolicy: { maximumEventAgeInSeconds: 900, maximumRetryAttempts: 0 }
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
schedule.addResourceDependency(jobGroup);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1204
1426
|
new scheduler.CfnScheduleGroup(this, "RoutineGroup", { name: names.routineGroup });
|
|
1205
1427
|
const schedulerRole = new iam2.Role(this, "RoutineSchedulerRole", {
|
|
1206
1428
|
roleName: names.routineSchedulerRole,
|
|
@@ -1283,6 +1505,33 @@ function upworkConfigForProvisioning(configPath) {
|
|
|
1283
1505
|
throw new Error(`${configPath}: integrations.upwork requires a configured Upwork capability (${missing.join(", ")})`);
|
|
1284
1506
|
return { channels: [...upwork.channels] };
|
|
1285
1507
|
}
|
|
1508
|
+
function etradeConfigForProvisioning(configPath) {
|
|
1509
|
+
const config = parseInstanceConfig(readFileSync(configPath, "utf8"));
|
|
1510
|
+
const etrade = config.capabilities.etrade;
|
|
1511
|
+
const missing = [
|
|
1512
|
+
...etrade.enabled ? [] : ["capabilities.etrade.enabled"],
|
|
1513
|
+
...etrade.channels.length > 0 ? [] : ["capabilities.etrade.channels"],
|
|
1514
|
+
...etrade.users.length > 0 ? [] : ["capabilities.etrade.users"]
|
|
1515
|
+
];
|
|
1516
|
+
if (missing.length > 0)
|
|
1517
|
+
throw new Error(`${configPath}: integrations.etrade requires a configured E*TRADE capability (${missing.join(", ")})`);
|
|
1518
|
+
const seenAdmins = new Set;
|
|
1519
|
+
const invalidAdmins = config.admins.filter((admin) => {
|
|
1520
|
+
const invalid = !/^[UW][A-Z0-9]{8,}$/.test(admin) || admin.length > 128 || seenAdmins.has(admin);
|
|
1521
|
+
seenAdmins.add(admin);
|
|
1522
|
+
return invalid;
|
|
1523
|
+
});
|
|
1524
|
+
if (invalidAdmins.length > 0) {
|
|
1525
|
+
throw new Error(`${configPath}: an E*TRADE deployment requires unique valid Slack user ids in admins`);
|
|
1526
|
+
}
|
|
1527
|
+
const nonAdmins = etrade.users.filter((user) => !config.admins.includes(user));
|
|
1528
|
+
if (nonAdmins.length > 0)
|
|
1529
|
+
throw new Error(`${configPath}: capabilities.etrade.users must be instance admins (${nonAdmins.join(", ")})`);
|
|
1530
|
+
return { channels: [...etrade.channels], users: [...etrade.users] };
|
|
1531
|
+
}
|
|
1532
|
+
function configTimezoneFor(configPath) {
|
|
1533
|
+
return parseInstanceConfig(readFileSync(configPath, "utf8")).timezone;
|
|
1534
|
+
}
|
|
1286
1535
|
function emailIdentitiesFor(configPath) {
|
|
1287
1536
|
const doc = parseYaml(readFileSync(configPath, "utf8"));
|
|
1288
1537
|
const email = doc.capabilities?.email;
|
|
@@ -1299,6 +1548,27 @@ function webhookJobsFor(configPath) {
|
|
|
1299
1548
|
}
|
|
1300
1549
|
return jobs;
|
|
1301
1550
|
}
|
|
1551
|
+
var LAMBDA_ENVIRONMENT_MAX_BYTES = 4096;
|
|
1552
|
+
var LAMBDA_ENVIRONMENT_TOKEN_BYTES = 128;
|
|
1553
|
+
function assertLambdaEnvironmentBudget(values) {
|
|
1554
|
+
let bytes = 2;
|
|
1555
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1556
|
+
bytes += Buffer.byteLength(key) + 3;
|
|
1557
|
+
bytes += cdk2.Token.isUnresolved(value) ? LAMBDA_ENVIRONMENT_TOKEN_BYTES : Buffer.byteLength(value);
|
|
1558
|
+
}
|
|
1559
|
+
if (bytes > LAMBDA_ENVIRONMENT_MAX_BYTES) {
|
|
1560
|
+
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.`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
function scheduledJobsFor(configPath) {
|
|
1564
|
+
const jobs = parseInstanceConfig(readFileSync(configPath, "utf8")).jobs.filter(isScheduledJob);
|
|
1565
|
+
for (const job of jobs) {
|
|
1566
|
+
const parsed = parseSchedule(job.on.schedule);
|
|
1567
|
+
if (!parsed.ok)
|
|
1568
|
+
throw new Error(`Scheduled job ${job.id} has an invalid schedule: ${parsed.reason}`);
|
|
1569
|
+
}
|
|
1570
|
+
return jobs;
|
|
1571
|
+
}
|
|
1302
1572
|
function browserDomainsFor(configPath) {
|
|
1303
1573
|
const browser = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.browser;
|
|
1304
1574
|
return browser.enabled && browser.mode === "read" ? [...browser.allowedDomains] : [];
|
|
@@ -1462,6 +1732,7 @@ class FoundationData extends cdk4.Stack {
|
|
|
1462
1732
|
itemsTable;
|
|
1463
1733
|
crmTable;
|
|
1464
1734
|
upworkApprovalTable;
|
|
1735
|
+
etradePendingTable;
|
|
1465
1736
|
secrets;
|
|
1466
1737
|
constructor(scope, id, props) {
|
|
1467
1738
|
super(scope, id, props);
|
|
@@ -1536,6 +1807,14 @@ class FoundationData extends cdk4.Stack {
|
|
|
1536
1807
|
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
|
|
1537
1808
|
removalPolicy: cdk4.RemovalPolicy.DESTROY
|
|
1538
1809
|
}) : undefined;
|
|
1810
|
+
this.etradePendingTable = provisionsIntegration(props.instance, "etrade") ? new dynamodb.Table(this, "EtradeOAuthPendingTable", {
|
|
1811
|
+
partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
|
|
1812
|
+
timeToLiveAttribute: "ttl",
|
|
1813
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
1814
|
+
encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED,
|
|
1815
|
+
encryptionKey: this.dataKey,
|
|
1816
|
+
removalPolicy: cdk4.RemovalPolicy.DESTROY
|
|
1817
|
+
}) : undefined;
|
|
1539
1818
|
const secretShell = (constructId, name, description) => new secretsmanager2.Secret(this, constructId, {
|
|
1540
1819
|
secretName: name,
|
|
1541
1820
|
description,
|
|
@@ -1573,6 +1852,24 @@ class FoundationData extends cdk4.Stack {
|
|
|
1573
1852
|
}
|
|
1574
1853
|
}) : undefined;
|
|
1575
1854
|
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;
|
|
1855
|
+
const etradeClient = provisionsIntegration(props.instance, "etrade") ? new secretsmanager2.Secret(this, "EtradeClientSecret", {
|
|
1856
|
+
secretName: names.secretEtradeClient,
|
|
1857
|
+
description: `E*TRADE OAuth consumer for ${display} ({ consumer_key, consumer_secret, environment })`,
|
|
1858
|
+
removalPolicy: cdk4.RemovalPolicy.DESTROY,
|
|
1859
|
+
generateSecretString: {
|
|
1860
|
+
secretStringTemplate: "{}",
|
|
1861
|
+
generateStringKey: "placeholder"
|
|
1862
|
+
}
|
|
1863
|
+
}) : undefined;
|
|
1864
|
+
const etradeCredential = provisionsIntegration(props.instance, "etrade") ? new secretsmanager2.Secret(this, "EtradeCredentialSecret", {
|
|
1865
|
+
secretName: names.secretEtradeCredential,
|
|
1866
|
+
description: `E*TRADE access token for ${display} ({ oauth_token, oauth_token_secret }); gateway writes and fixed read proxy alone reads`,
|
|
1867
|
+
removalPolicy: cdk4.RemovalPolicy.DESTROY,
|
|
1868
|
+
generateSecretString: {
|
|
1869
|
+
secretStringTemplate: "{}",
|
|
1870
|
+
generateStringKey: "placeholder"
|
|
1871
|
+
}
|
|
1872
|
+
}) : undefined;
|
|
1576
1873
|
this.secrets = {
|
|
1577
1874
|
signing: secretsmanager2.Secret.fromSecretNameV2(this, "SlackSigningSecret", names.secretSlackSigning),
|
|
1578
1875
|
slackApp: secretsmanager2.Secret.fromSecretNameV2(this, "SlackAppSecret", names.secretSlackApp),
|
|
@@ -1614,6 +1911,8 @@ class FoundationData extends cdk4.Stack {
|
|
|
1614
1911
|
...knockOauthClient === undefined ? {} : { knockOauthClient },
|
|
1615
1912
|
...knockCredential === undefined ? {} : { knockCredential },
|
|
1616
1913
|
...upwork === undefined ? {} : { upwork },
|
|
1914
|
+
...etradeClient === undefined ? {} : { etradeClient },
|
|
1915
|
+
...etradeCredential === undefined ? {} : { etradeCredential },
|
|
1617
1916
|
googleOauth: secretShell("GoogleOauthSecret", names.secretGoogleOauth, `Google OAuth client shared by ${display}'s Google capabilities ({ client_id, client_secret, redirect_uri })`),
|
|
1618
1917
|
runtime: secretShell("RuntimeSecret", names.secretRuntime, "Agent runtime env map, written by scripts/setup.ts after deploy")
|
|
1619
1918
|
};
|
package/dist/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
FoundationPipeline,
|
|
10
10
|
NewsletterStack,
|
|
11
11
|
deployStatements
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-my0awr1g.js";
|
|
13
13
|
import {
|
|
14
14
|
BUN_VERSION,
|
|
15
15
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
skillsKey,
|
|
43
43
|
skipBundle,
|
|
44
44
|
verifyManifest
|
|
45
|
-
} from "../chunk-
|
|
45
|
+
} from "../chunk-kf1yvx1w.js";
|
|
46
46
|
export {
|
|
47
47
|
BUILD_TIMEOUT,
|
|
48
48
|
BUN_VERSION,
|
package/package.json
CHANGED
package/src/artifacts.ts
CHANGED
|
@@ -65,6 +65,7 @@ export const LAMBDA_ENTRY_POINTS = {
|
|
|
65
65
|
"knock-proxy": { entry: "packages/capability-knock/src/proxy/handler.ts" },
|
|
66
66
|
"crm-proxy": { entry: "packages/capability-crm/src/proxy/handler.ts" },
|
|
67
67
|
"upwork-proxy": { entry: "packages/capability-upwork/src/proxy/handler.ts" },
|
|
68
|
+
"etrade-proxy": { entry: "packages/capability-etrade/src/proxy/handler.ts" },
|
|
68
69
|
"browser-proxy": { entry: "packages/gateway/src/browser-proxy.ts", playwright: true },
|
|
69
70
|
"newsletter-public": { entry: "packages/newsletter/src/lambda-public.ts" },
|
|
70
71
|
"newsletter-campaign": { entry: "packages/newsletter/src/lambda-campaign.ts" },
|
|
@@ -78,6 +78,12 @@ features:
|
|
|
78
78
|
url: ${COMMANDS_REQUEST_URL}
|
|
79
79
|
description: Connect the Upwork account ${DISPLAY_NAME} uses (admins)
|
|
80
80
|
should_escape: false
|
|
81
|
+
# Admins only: E*TRADE OAuth completes with an out-of-band verifier in
|
|
82
|
+
# the configured private channel; financial credentials never enter Slack.
|
|
83
|
+
- command: /${COMMAND_PREFIX}-etrade-connect
|
|
84
|
+
url: ${COMMANDS_REQUEST_URL}
|
|
85
|
+
description: Connect ${DISPLAY_NAME} to E*TRADE read-only tools (admins)
|
|
86
|
+
should_escape: false
|
|
81
87
|
|
|
82
88
|
# oauth_config.scopes.bot and settings.event_subscriptions.bot_events are
|
|
83
89
|
# GENERATED from the capability registry by `slack-manifest.ts`
|
package/src/deploy/deploy.ts
CHANGED
|
@@ -369,6 +369,11 @@ export async function syncRuntimeSecret(ctx: AwsContext): Promise<string[]> {
|
|
|
369
369
|
? `<${api}.UpworkProxyFunctionArn>`
|
|
370
370
|
: await optionalOutput(ctx, api, "UpworkProxyFunctionArn")
|
|
371
371
|
: undefined;
|
|
372
|
+
const etradeProxyFunctionArn = ctx.instance.integrations.etrade
|
|
373
|
+
? dry
|
|
374
|
+
? `<${api}.EtradeProxyFunctionArn>`
|
|
375
|
+
: await optionalOutput(ctx, api, "EtradeProxyFunctionArn")
|
|
376
|
+
: undefined;
|
|
372
377
|
const routineIngressFunctionArn = dry
|
|
373
378
|
? `<${api}.RoutineIngressFunctionArn>`
|
|
374
379
|
: await optionalOutput(ctx, api, "RoutineIngressFunctionArn");
|
|
@@ -398,6 +403,7 @@ export async function syncRuntimeSecret(ctx: AwsContext): Promise<string[]> {
|
|
|
398
403
|
...(otterProxyFunctionArn === undefined ? {} : { otterProxyFunctionArn }),
|
|
399
404
|
...(knockProxyFunctionArn === undefined ? {} : { knockProxyFunctionArn }),
|
|
400
405
|
...(upworkProxyFunctionArn === undefined ? {} : { upworkProxyFunctionArn }),
|
|
406
|
+
...(etradeProxyFunctionArn === undefined ? {} : { etradeProxyFunctionArn }),
|
|
401
407
|
...(routineIngressFunctionArn === undefined ? {} : { routineIngressFunctionArn }),
|
|
402
408
|
});
|
|
403
409
|
await putSecretJson(ctx, secretRuntime, value);
|
|
@@ -92,6 +92,12 @@ export interface RuntimeSecretInputs {
|
|
|
92
92
|
* shell-capable container. OAuth client and token state stay in the proxy.
|
|
93
93
|
*/
|
|
94
94
|
upworkProxyFunctionArn?: string;
|
|
95
|
+
/**
|
|
96
|
+
* `FoundationApi.EtradeProxyFunctionArn` — the agent's sole E*TRADE binding.
|
|
97
|
+
* Consumer/access credentials and OOB pending-state identifiers never enter
|
|
98
|
+
* this runtime secret.
|
|
99
|
+
*/
|
|
100
|
+
etradeProxyFunctionArn?: string;
|
|
95
101
|
/** Trusted gateway ingress used instead of granting the agent normal-queue writes. */
|
|
96
102
|
routineIngressFunctionArn?: string;
|
|
97
103
|
defaultRepo?: string;
|
|
@@ -220,6 +226,9 @@ export function composeRuntimeSecret(inputs: RuntimeSecretInputs): Record<string
|
|
|
220
226
|
...(inputs.upworkProxyFunctionArn !== undefined && inputs.upworkProxyFunctionArn !== ""
|
|
221
227
|
? { FOUNDATION_UPWORK_PROXY_ARN: inputs.upworkProxyFunctionArn }
|
|
222
228
|
: {}),
|
|
229
|
+
...(inputs.etradeProxyFunctionArn !== undefined && inputs.etradeProxyFunctionArn !== ""
|
|
230
|
+
? { FOUNDATION_ETRADE_PROXY_ARN: inputs.etradeProxyFunctionArn }
|
|
231
|
+
: {}),
|
|
223
232
|
...(inputs.routineIngressFunctionArn !== undefined && inputs.routineIngressFunctionArn !== ""
|
|
224
233
|
? { FOUNDATION_ROUTINE_INGRESS_FUNCTION_ARN: inputs.routineIngressFunctionArn }
|
|
225
234
|
: {}),
|
|
@@ -227,9 +236,9 @@ export function composeRuntimeSecret(inputs: RuntimeSecretInputs): Record<string
|
|
|
227
236
|
// LAZILY, so a secret still holding the CDK placeholder costs nothing
|
|
228
237
|
// until someone asks a MongoDB question.
|
|
229
238
|
FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
|
|
230
|
-
// CRM storage, the Otter credential, the Knock OAuth secrets, and the
|
|
231
|
-
// OAuth
|
|
232
|
-
// only their fixed proxy ARNs.
|
|
239
|
+
// CRM storage, the Otter credential, the Knock OAuth secrets, and the
|
|
240
|
+
// Upwork/E*TRADE OAuth secrets are intentionally absent here: the
|
|
241
|
+
// shell-capable agent receives only their fixed proxy ARNs.
|
|
233
242
|
// A routine fire builds its own session key, so the container needs the
|
|
234
243
|
// team id the gateway takes from the event. The app secret carries it
|
|
235
244
|
// when setup wrote one; the instance file is the fallback.
|