@reventlessdev/reventless-aws 3.0.0-alpha.292 → 3.0.0-alpha.294

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,163 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Aws from "@pulumi/aws";
4
+ import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
5
+ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Pulumi from "@pulumi/pulumi";
8
+ import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
9
+ import * as AWS$ReventlessAws from "../AWS.res.mjs";
10
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
11
+ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
+ import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
+ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
+ import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
15
+ import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
16
+ import * as AppSync_Resolver_Native$ReventlessAws from "../Api/AppSync_Resolver_Native.res.mjs";
17
+
18
+ let invokeCode = `import { util } from '@aws-appsync/utils';
19
+ export function request(ctx) {
20
+ const id = ctx.identity;
21
+ return {
22
+ operation: 'Invoke',
23
+ payload: {
24
+ arguments: ctx.args,
25
+ identity: id != null && id.sub != null ? { sub: id.sub } : null
26
+ }
27
+ };
28
+ }
29
+ export function response(ctx) {
30
+ if (ctx.error) util.error(ctx.error.message, ctx.error.type);
31
+ return ctx.result;
32
+ }
33
+ `;
34
+
35
+ function makeTable(nameOpt, opts) {
36
+ let name = nameOpt !== undefined ? nameOpt : "ActiveRoleStore";
37
+ let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
38
+ let table = Util_DynamoDb$ReventlessAws.makeTable([{
39
+ name: "id",
40
+ type: "S"
41
+ }], undefined, undefined, undefined, AWS_Tags$ReventlessAws.make(name + "Table", "Platform", "Auth", "Platform", undefined, undefined, undefined, undefined), opts$1, name);
42
+ return {
43
+ name: table.name,
44
+ arn: table.arn
45
+ };
46
+ }
47
+
48
+ function makeWriteDoor(api, table, cognitoUserPoolId, cognitoUserPoolArn, nameOpt, opts) {
49
+ let name = nameOpt !== undefined ? nameOpt : "ActiveRoleStore";
50
+ let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
51
+ let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "Lambda", Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name + "Lambda", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
52
+ Pulumi.all([
53
+ table.arn,
54
+ cognitoUserPoolArn
55
+ ]).apply(param => {
56
+ new (Aws.iam.RolePolicy)(name + "LambdaPolicy", {
57
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "LambdaPolicy", [
58
+ {
59
+ Sid: "AllowLambdaLogging",
60
+ Effect: "Allow",
61
+ Action: [
62
+ "logs:CreateLogGroup",
63
+ "logs:CreateLogStream",
64
+ "logs:PutLogEvents"
65
+ ],
66
+ Resource: "arn:aws:logs:*:*:*"
67
+ },
68
+ {
69
+ Sid: "AllowActiveRoleRowAccess",
70
+ Effect: "Allow",
71
+ Action: [
72
+ "dynamodb:PutItem",
73
+ "dynamodb:DeleteItem",
74
+ "dynamodb:GetItem"
75
+ ],
76
+ Resource: param[0]
77
+ },
78
+ {
79
+ Sid: "AllowReadingOwnPoolMembership",
80
+ Effect: "Allow",
81
+ Action: "cognito-idp:AdminListGroupsForUser",
82
+ Resource: param[1]
83
+ }
84
+ ])),
85
+ role: lambdaRole.id
86
+ }, opts$1);
87
+ });
88
+ let packageDirs = Object.fromEntries([[
89
+ "@reventlessdev/reventless-aws",
90
+ Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
91
+ ]]);
92
+ let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs", packageDirs, undefined, false);
93
+ let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
94
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", undefined, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
95
+ let lambda = new (Aws.lambda.Function)(name + "Lambda", {
96
+ handler: "index.handler",
97
+ runtime: "nodejs22.x",
98
+ code: match.code,
99
+ role: lambdaRole.arn,
100
+ memorySize: 256,
101
+ timeout: 30,
102
+ layers: layers,
103
+ tags: AWS_Tags$ReventlessAws.make(name + "Lambda", "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined),
104
+ environment: {
105
+ variables: Object.fromEntries([
106
+ [
107
+ "Environment",
108
+ Pulumi.getStack()
109
+ ],
110
+ [
111
+ "ACTIVE_ROLE_TABLE",
112
+ table.name
113
+ ],
114
+ [
115
+ "COGNITO_USER_POOL_ID",
116
+ cognitoUserPoolId
117
+ ],
118
+ [
119
+ "NODE_OPTIONS",
120
+ Util_Bundle$ReventlessAws.esmLoaderNodeOptions
121
+ ],
122
+ [
123
+ "ESM_FALLBACK_DIRS",
124
+ Util_Bundle$ReventlessAws.esmFallbackDirs
125
+ ],
126
+ Util_LambdaLogging$ReventlessAws.logLevelEntry()
127
+ ])
128
+ },
129
+ sourceCodeHash: match.sourceCodeHash,
130
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
131
+ }, opts$1);
132
+ let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
133
+ Pulumi.all([
134
+ lambda.arn,
135
+ dataSourceRole.id
136
+ ]).apply(param => {
137
+ new (Aws.iam.RolePolicy)(name + "DataSource", {
138
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "DataSourcePolicy", [{
139
+ Sid: "AllowDataSourceInvokeLambda",
140
+ Effect: "Allow",
141
+ Action: "lambda:InvokeFunction",
142
+ Resource: param[0]
143
+ }])),
144
+ role: param[1]
145
+ }, opts$1);
146
+ });
147
+ let dataSource = new (Aws.appsync.DataSource)(name + "DataSource", {
148
+ type: "AWS_LAMBDA",
149
+ apiId: Output$Pulumi.flatMap(api, api => api.id),
150
+ lambdaConfig: {
151
+ functionArn: lambda.arn
152
+ },
153
+ serviceRoleArn: dataSourceRole.arn
154
+ }, opts$1);
155
+ AppSync_Resolver_Native$ReventlessAws.makeUnitJsResolver(name + "SetActiveRole", api, dataSource.name, "Mutation", "Platform_SetActiveRole", invokeCode, opts$1);
156
+ }
157
+
158
+ export {
159
+ invokeCode,
160
+ makeTable,
161
+ makeWriteDoor,
162
+ }
163
+ /* @pulumi/aws Not a pure module */
@@ -0,0 +1,148 @@
1
+ // Runtime handler for `Mutation.Platform_SetActiveRole` — compiled, type-checked
2
+ // and Pulumi-free so it ships as an EntryPoint module (`Auth_ActiveRoleStore`
3
+ // bundles it and attaches it as the domain API's Lambda data source). See
4
+ // [Upload_Presign_S3_Ops.res] for why an EntryPoint rather than a serialized
5
+ // closure, and [docs/plans/active-role-narrows-the-token.md] §6 for the design.
6
+ //
7
+ // What this writes is a *preference*, not a token. Cognito mints the token, and
8
+ // the pre-token-generation trigger ([Auth_ActiveRoleTrigger_Ops.res]) reads this
9
+ // row on the next refresh and narrows the group claim to it. Nothing here can
10
+ // grant anything: the trigger re-checks the stored role against real membership
11
+ // before applying it, so a row is at most a request that the trigger may honour.
12
+ //
13
+ // 🚨 **The subject is taken from the verified identity, never from the arguments.**
14
+ // The row key is `ctx.identity.sub` as the AppSync Cognito authorizer resolved it.
15
+ // There is deliberately no `sub` argument on the mutation — with one, a caller
16
+ // could set another caller's role, and this becomes a privilege-granting surface
17
+ // instead of a preference.
18
+
19
+ open AwsSdk
20
+
21
+ // ── Environment ─────────────────────────────────────────────────────────────
22
+
23
+ let getEnv = (k: string): option<string> =>
24
+ switch NodeProcess.env->Dict.get(k) {
25
+ | Some("") | None => None
26
+ | Some(v) => Some(v)
27
+ }
28
+
29
+ let tableName = (): string =>
30
+ switch getEnv("ACTIVE_ROLE_TABLE") {
31
+ | Some(t) => t
32
+ | None => JsError.throwWithMessage("ACTIVE_ROLE_TABLE is not configured")
33
+ }
34
+
35
+ let userPoolId = (): string =>
36
+ switch getEnv("COGNITO_USER_POOL_ID") {
37
+ | Some(p) => p
38
+ | None => JsError.throwWithMessage("COGNITO_USER_POOL_ID is not configured")
39
+ }
40
+
41
+ // ── Membership ──────────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ The caller's actual group membership, read from Cognito.
45
+
46
+ 🚨 **Not read from the presented token.** A narrowed token carries exactly one
47
+ group, so judging membership by `ctx.identity.groups` would make the switch
48
+ one-way: a caller who narrowed to `Shopper` could never widen back, because the
49
+ token they hold no longer mentions the role they are asking for. This is the same
50
+ trap the local path documents on `LocalAuth.Login.reissue`, where membership is
51
+ re-read from the user store rather than from the token's own record of it.
52
+
53
+ Paginated deliberately: `AdminListGroupsForUser` caps a page at 60 groups, and a
54
+ truncated read would silently refuse a role the caller genuinely holds.
55
+ */
56
+ let membershipOf = async (~sub: string, ~poolId: string): array<string> => {
57
+ let collected = []
58
+ let nextToken = ref(None)
59
+ let more = ref(true)
60
+ while more.contents {
61
+ let page = await CognitoIdentityServiceProvider.AdminListGroupsForUserCommand.make({
62
+ username: sub,
63
+ userPoolId: poolId,
64
+ nextToken: ?nextToken.contents,
65
+ })->CognitoIdentityServiceProvider.AdminListGroupsForUserCommand.send
66
+ page.groups
67
+ ->Option.getOr([])
68
+ ->Array.forEach(g => g.groupName->Option.forEach(n => collected->Array.push(n)))
69
+ switch page.nextToken {
70
+ | Some(_) as t => nextToken := t
71
+ | None => more := false
72
+ }
73
+ }
74
+ collected
75
+ }
76
+
77
+ // ── The subset rule ─────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ Whether a requested role may be stored.
81
+
82
+ Narrowing only, never widening — a client that tampers with the request can only
83
+ ever reduce its own privilege. Pure and exported so the conformance table in
84
+ `ReventlessCore.Auth_ActiveRole` can be driven against it without a Cognito pool
85
+ or a DynamoDB table in existence.
86
+ */
87
+ let mayActAs = (~membership: array<string>, ~requested: string): bool =>
88
+ membership->Array.includes(requested)
89
+
90
+ // ── AppSync resolver event / result shapes ──────────────────────────────────
91
+
92
+ type identity = {sub?: string}
93
+ type activeRoleArgs = {activeRole?: Nullable.t<string>}
94
+ type appSyncEvent = {
95
+ arguments?: activeRoleArgs,
96
+ identity?: identity,
97
+ }
98
+
99
+ let result = (~activeRole: option<string>, ~availableRoles: array<string>): JSON.t =>
100
+ Dict.fromArray([
101
+ ("activeRole", activeRole->Option.mapOr(JSON.Null, JSON.Encode.string)),
102
+ ("availableRoles", availableRoles->Array.map(JSON.Encode.string)->JSON.Encode.array),
103
+ ])->JSON.Encode.object
104
+
105
+ // ── Handler ─────────────────────────────────────────────────────────────────
106
+
107
+ let handler = async (event: appSyncEvent): JSON.t => {
108
+ let sub = event.identity->Option.flatMap(i => i.sub)->Option.getOr("")
109
+ if sub == "" {
110
+ // The authorizer should have refused first; this is the guard that keeps an
111
+ // unauthenticated path from ever writing a row keyed on the empty subject.
112
+ JsError.throwWithMessage("unauthenticated")
113
+ }
114
+ let table = tableName()
115
+ let membership = await membershipOf(~sub, ~poolId=userPoolId())
116
+
117
+ // An absent argument and an explicit `null` mean the same thing — clear the
118
+ // preference and go back to full membership on the next refresh. That is not an
119
+ // escalation: the set being widened to is the one Cognito says the caller holds.
120
+ let requested = switch event.arguments->Option.flatMap(a => a.activeRole) {
121
+ | Some(Value(role)) => Some(role)
122
+ | Some(Null) | Some(Undefined) | None => None
123
+ }
124
+
125
+ switch requested {
126
+ | None =>
127
+ let _ = await DynamoDb_DocumentClient.deleteById(~tableName=table, ~id=sub)
128
+ result(~activeRole=None, ~availableRoles=membership)
129
+ | Some(role) =>
130
+ // Refused, specifically — not ignored and stored anyway. A client asking for
131
+ // something it cannot have is either confused or hostile, and both are better
132
+ // served by an error than by a stored role that quietly never takes effect.
133
+ if !mayActAs(~membership, ~requested=role) {
134
+ JsError.throwWithMessage(`Cannot act as "${role}": not a group this user holds`)
135
+ }
136
+ let item =
137
+ Dict.fromArray([
138
+ ("id", JSON.Encode.string(sub)),
139
+ ("activeRole", JSON.Encode.string(role)),
140
+ ("updatedAt", JSON.Encode.string(Date.make()->Date.toISOString)),
141
+ ])->JSON.Encode.object
142
+ let _ = await DynamoDb_DocumentClient.PutCommand.make({
143
+ tableName: table,
144
+ item,
145
+ })->DynamoDb_DocumentClient.PutCommand.send
146
+ result(~activeRole=Some(role), ~availableRoles=membership)
147
+ }
148
+ }
@@ -0,0 +1,128 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
+ import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
7
+ import * as DynamoDb_DocumentClient$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DocumentClient.res.mjs";
8
+ import * as CognitoIdentityServiceProvider$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/CognitoIdentityServiceProvider.res.mjs";
9
+ import * as ClientCognitoIdentityProvider from "@aws-sdk/client-cognito-identity-provider";
10
+
11
+ function getEnv(k) {
12
+ let v = process.env[k];
13
+ if (v !== undefined && v !== "") {
14
+ return v;
15
+ }
16
+ }
17
+
18
+ function tableName() {
19
+ let t = getEnv("ACTIVE_ROLE_TABLE");
20
+ if (t !== undefined) {
21
+ return t;
22
+ } else {
23
+ return Stdlib_JsError.throwWithMessage("ACTIVE_ROLE_TABLE is not configured");
24
+ }
25
+ }
26
+
27
+ function userPoolId() {
28
+ let p = getEnv("COGNITO_USER_POOL_ID");
29
+ if (p !== undefined) {
30
+ return p;
31
+ } else {
32
+ return Stdlib_JsError.throwWithMessage("COGNITO_USER_POOL_ID is not configured");
33
+ }
34
+ }
35
+
36
+ async function membershipOf(sub, poolId) {
37
+ let collected = [];
38
+ let nextToken;
39
+ let more = true;
40
+ while (more) {
41
+ let page = await CognitoIdentityServiceProvider$AwsSdk.AdminListGroupsForUserCommand.send(new ClientCognitoIdentityProvider.AdminListGroupsForUserCommand({
42
+ Username: sub,
43
+ UserPoolId: poolId,
44
+ NextToken: nextToken
45
+ }));
46
+ Stdlib_Option.getOr(page.Groups, []).forEach(g => Stdlib_Option.forEach(g.GroupName, n => {
47
+ collected.push(n);
48
+ }));
49
+ let t = page.NextToken;
50
+ if (t !== undefined) {
51
+ nextToken = t;
52
+ } else {
53
+ more = false;
54
+ }
55
+ };
56
+ return collected;
57
+ }
58
+
59
+ function mayActAs(membership, requested) {
60
+ return membership.includes(requested);
61
+ }
62
+
63
+ function result(activeRole, availableRoles) {
64
+ return Object.fromEntries([
65
+ [
66
+ "activeRole",
67
+ Stdlib_Option.mapOr(activeRole, null, prim => prim)
68
+ ],
69
+ [
70
+ "availableRoles",
71
+ availableRoles.map(prim => prim)
72
+ ]
73
+ ]);
74
+ }
75
+
76
+ async function handler(event) {
77
+ let sub = Stdlib_Option.getOr(Stdlib_Option.flatMap(event.identity, i => i.sub), "");
78
+ if (sub === "") {
79
+ Stdlib_JsError.throwWithMessage("unauthenticated");
80
+ }
81
+ let table = tableName();
82
+ let membership = await membershipOf(sub, userPoolId());
83
+ let match = Stdlib_Option.flatMap(event.arguments, a => a.activeRole);
84
+ let requested;
85
+ if (match !== undefined) {
86
+ let role = Primitive_option.valFromOption(match);
87
+ requested = (role == null) ? undefined : role;
88
+ } else {
89
+ requested = undefined;
90
+ }
91
+ if (requested !== undefined) {
92
+ if (!membership.includes(requested)) {
93
+ Stdlib_JsError.throwWithMessage(`Cannot act as "` + requested + `": not a group this user holds`);
94
+ }
95
+ let item = Object.fromEntries([
96
+ [
97
+ "id",
98
+ sub
99
+ ],
100
+ [
101
+ "activeRole",
102
+ requested
103
+ ],
104
+ [
105
+ "updatedAt",
106
+ new Date().toISOString()
107
+ ]
108
+ ]);
109
+ await DynamoDb_DocumentClient$AwsSdk.PutCommand.send(new LibDynamodb.PutCommand({
110
+ Item: item,
111
+ TableName: table
112
+ }));
113
+ return result(requested, membership);
114
+ }
115
+ await DynamoDb_DocumentClient$AwsSdk.deleteById(table, sub);
116
+ return result(undefined, membership);
117
+ }
118
+
119
+ export {
120
+ getEnv,
121
+ tableName,
122
+ userPoolId,
123
+ membershipOf,
124
+ mayActAs,
125
+ result,
126
+ handler,
127
+ }
128
+ /* @aws-sdk/lib-dynamodb Not a pure module */
@@ -0,0 +1,185 @@
1
+ // Deploy-time half of the Cognito pre-token-generation trigger — the Lambda that
2
+ // narrows `cognito:groups` to the role a caller chose. Runtime logic lives in
3
+ // [Auth_ActiveRoleTrigger_Ops.res]; see
4
+ // [docs/plans/active-role-narrows-the-token.md] §6.
5
+ //
6
+ // `make` provisions the function and its execution role (Logs + a read of the
7
+ // one role-state table). It does **not** attach itself to a user pool, and does
8
+ // not grant Cognito permission to invoke it: both need the pool, and in auto
9
+ // mode the pool needs this function's ARN first — the pool cannot be declared
10
+ // with a trigger that does not exist yet. So `make` runs before the pool, and
11
+ // [grantInvoke] runs after it.
12
+ //
13
+ // Attaching is a separate step again ([Auth_ActiveRolePoolAttachment.res] in BYO
14
+ // mode, `lambdaConfig` on the declared pool in auto mode), kept apart because it
15
+ // is the only part of this feature that can damage something that already
16
+ // exists.
17
+
18
+ open PulumiAws
19
+
20
+ type triggerOutputs = {
21
+ functionArn: Pulumi.Output.t<string>,
22
+ functionName: Pulumi.Output.t<string>,
23
+ /** The archive's hash, exposed so whoever attaches this trigger to a pool can
24
+ tell that the code changed. The function ARN does not move when the code
25
+ does, so without this an attachment sees identical inputs across deploys
26
+ and never re-checks a function it proved once. */
27
+ sourceCodeHash: string,
28
+ }
29
+
30
+ let make = (
31
+ ~activeRoleTableName: Pulumi.Input.t<string>,
32
+ ~activeRoleTableArn: Pulumi.Input.t<string>,
33
+ ~name: string="ActiveRoleTrigger",
34
+ ~opts: Pulumi.ComponentResource.options,
35
+ ): triggerOutputs => {
36
+ let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
37
+
38
+ let lambdaRole = IAM.Role.makeWithDefaultPolicy(
39
+ ~name,
40
+ ~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
41
+ ~tags=AWS.Tags.make(
42
+ ~name,
43
+ ~kind=ReventlessCore.ComponentType.Platform,
44
+ ~role=Identity,
45
+ ~scope=Platform,
46
+ ),
47
+ ~opts,
48
+ )
49
+
50
+ // Logs and a single-item read. Nothing else: the trigger needs no Cognito
51
+ // permission at all, because the membership it checks against arrives in the
52
+ // event as `request.groupConfiguration.groupsToOverride`. `GetItem` alone —
53
+ // this function must never be able to write the preference it reads.
54
+ let _policy =
55
+ activeRoleTableArn
56
+ ->Pulumi.Output.fromInput
57
+ ->Pulumi.Output.apply(tableArn => {
58
+ let _ = IAM.RolePolicy.make(
59
+ ~name=name ++ "Policy",
60
+ ~args={
61
+ policy: PolicyDocument.make(
62
+ ~id=name ++ "Policy",
63
+ ~statements=[
64
+ {
65
+ sid: "AllowLambdaLogging",
66
+ effect: Allow,
67
+ actions: Actions([
68
+ "logs:CreateLogGroup",
69
+ "logs:CreateLogStream",
70
+ "logs:PutLogEvents",
71
+ ]),
72
+ resources: Resource("arn:aws:logs:*:*:*"),
73
+ },
74
+ {
75
+ sid: "AllowReadingStoredRole",
76
+ effect: Allow,
77
+ actions: Action("dynamodb:GetItem"),
78
+ resources: Resource(tableArn),
79
+ },
80
+ ],
81
+ )
82
+ ->PolicyDocument.toJsonString
83
+ ->Pulumi.Input.make,
84
+ role: lambdaRole.id->Pulumi.Output.asInput,
85
+ },
86
+ ~opts,
87
+ )
88
+ })
89
+
90
+ let packageDirs = Dict.fromArray([
91
+ (
92
+ "@reventlessdev/reventless-aws",
93
+ Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws"),
94
+ ),
95
+ ])
96
+ let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
97
+ ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs",
98
+ ~packageDirs,
99
+ ~bundleRuntimeExtensions=false,
100
+ )
101
+
102
+ let layers =
103
+ Lambda.reventlessLayerArn
104
+ ->Option.map(arn => [arn->Pulumi.Input.make])
105
+ ->Option.getOr([])
106
+ ->Pulumi.Input.make
107
+
108
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
109
+ ~name,
110
+ ~tags=AWS.Tags.make(
111
+ ~name=name ++ "LogGroup",
112
+ ~kind=ReventlessCore.ComponentType.Platform,
113
+ ~role=Logs,
114
+ ~scope=Platform,
115
+ ),
116
+ ~opts,
117
+ (),
118
+ )
119
+
120
+ // This function sits in the critical path of every token the pool mints, so a
121
+ // cold start is a login's latency and a timeout is a failed sign-in. One
122
+ // `GetItem` needs neither much memory nor much time; 5s is well inside
123
+ // Cognito's own 5s trigger budget, so the function fails before Cognito gives
124
+ // up on it rather than after.
125
+ let lambda = Lambda.Function.make(
126
+ ~name,
127
+ ~args={
128
+ handler: "index.handler"->Pulumi.Input.make,
129
+ runtime: "nodejs22.x"->Pulumi.Input.make,
130
+ code: code->Pulumi.Input.make,
131
+ sourceCodeHash: sourceCodeHash->Pulumi.Input.make,
132
+ role: lambdaRole.arn->Pulumi.Output.asInput,
133
+ memorySize: 256->Pulumi.Input.make,
134
+ timeout: 5->Pulumi.Input.make,
135
+ layers,
136
+ tags: AWS.Tags.make(
137
+ ~name,
138
+ ~kind=ReventlessCore.ComponentType.Platform,
139
+ ~role=Runtime,
140
+ ~scope=Platform,
141
+ ),
142
+ environment: (
143
+ {
144
+ Lambda.Function.variables: Dict.fromArray([
145
+ ("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
146
+ ("ACTIVE_ROLE_TABLE", activeRoleTableName),
147
+ ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
148
+ ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
149
+ Util_LambdaLogging.logLevelEntry(),
150
+ ]),
151
+ }: Lambda.Function.functionEnvironment
152
+ )->Pulumi.Input.make,
153
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
154
+ },
155
+ ~opts,
156
+ )
157
+
158
+ {functionArn: lambda.arn, functionName: lambda.name, sourceCodeHash}
159
+ }
160
+
161
+ /** Let Cognito invoke the trigger — scoped to one pool, so a function attached
162
+ to a pool nobody declared still cannot be invoked by it.
163
+
164
+ Separate from [make] because the pool's ARN is not known until the pool exists,
165
+ and in auto mode the pool is declared *with* the function's ARN. */
166
+ let grantInvoke = (
167
+ ~trigger: triggerOutputs,
168
+ ~cognitoUserPoolArn: Pulumi.Input.t<string>,
169
+ ~name: string="ActiveRoleTrigger",
170
+ ~opts: Pulumi.ComponentResource.options,
171
+ ): unit => {
172
+ let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
173
+ let _permission = trigger.functionName->Pulumi.Output.apply(functionName => {
174
+ let _ = Lambda.Permission.make(
175
+ ~name=name ++ "Invoke",
176
+ ~args={
177
+ action: "lambda:InvokeFunction",
178
+ function: functionName->Pulumi.Input.make,
179
+ principal: "cognito-idp.amazonaws.com",
180
+ sourceArn: cognitoUserPoolArn,
181
+ },
182
+ ~opts,
183
+ )
184
+ })
185
+ }