@reventlessdev/reventless-aws 3.0.0-alpha.395 → 3.0.0-alpha.397

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +7 -7
  3. package/scripts/DeployApp.res +7 -1
  4. package/scripts/DeployApp.res.mjs +1 -1
  5. package/scripts/ProvisionAccounts.res +89 -4
  6. package/scripts/ProvisionAccounts.res.mjs +101 -5
  7. package/scripts/ProvisionIdentity.res +42 -18
  8. package/scripts/ProvisionIdentity.res.mjs +40 -26
  9. package/src/Platform.res +47 -10
  10. package/src/Platform.res.mjs +44 -10
  11. package/src/Platform_Stack.res +17 -0
  12. package/src/Platform_Stack.res.mjs +7 -1
  13. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +25 -9
  14. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +17 -4
  15. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +59 -9
  16. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +53 -6
  17. package/src/adapter/Auth/Auth_Cognito.res +13 -2
  18. package/src/adapter/Auth/Auth_Cognito.res.mjs +9 -2
  19. package/src/adapter/Auth/Auth_IdentityStore.res +91 -0
  20. package/src/adapter/Auth/Auth_IdentityStore.res.mjs +85 -0
  21. package/src/adapter/Auth/Auth_IdentityStore_Schema.res +39 -0
  22. package/src/adapter/Auth/Auth_IdentityStore_Schema.res.mjs +35 -0
  23. package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res +251 -0
  24. package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res.mjs +300 -0
  25. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +7 -4
  26. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +2 -2
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +80 -0
  28. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +53 -2
  29. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  30. package/src/components/OutboundTranslationSlice_Builder.res +2 -0
  31. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  32. package/src/plugin/runtime/PluginRuntime_Builder.res +23 -0
  33. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +29 -0
  34. package/tests/Auth_ActiveRoleTrigger_OpsTest.res +40 -0
  35. package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +59 -8
  36. package/tests/Auth_CognitoTest.res +62 -0
  37. package/tests/Auth_CognitoTest.res.mjs +98 -0
  38. package/tests/Auth_IdentityStore_SchemaTest.res +35 -0
  39. package/tests/Auth_IdentityStore_SchemaTest.res.mjs +43 -0
  40. package/tests/IdentityProvider_Cognito_BackendTest.res +314 -0
  41. package/tests/IdentityProvider_Cognito_BackendTest.res.mjs +442 -0
  42. package/tests/ProvisionAccountsTest.res.mjs +1 -1
@@ -0,0 +1,91 @@
1
+ // The rows mapping a Cognito `sub` to the domain's own user id, and where they
2
+ // live. Read by the pre-token-generation trigger on every mint, written only
3
+ // when an account is created (`provision-accounts`, and the Cognito identity
4
+ // provider when it exists).
5
+ //
6
+ // 🚨 **The rows follow the identity provider, not the stack**, exactly as the
7
+ // active-role store's do and for the same reason: a pool holds one pre-token
8
+ // trigger, so two stacks sharing a pool with a table each would have the winning
9
+ // trigger reading rows the other stack wrote nowhere near. A stack that owns its
10
+ // pool owns this table; a stack handed an `identityProviderId` looks it up at
11
+ // [Auth_IdentityStore_Schema.derivedStoreName] and never creates it.
12
+
13
+ type storeTable = Auth_ActiveRoleStore.storeTable
14
+
15
+ /** Which store a deployment's trigger reads. Total and pure, like the active-role
16
+ store's: whether the provider is ours decides it, and nothing else. */
17
+ let chooseStore = (~identityProviderId: option<string>): Auth_ActiveRoleStore.storeChoice =>
18
+ switch identityProviderId {
19
+ | None => StackScoped
20
+ | Some(id) => ProviderScoped(Auth_IdentityStore_Schema.derivedStoreName(~identityProviderId=id))
21
+ }
22
+
23
+ /** One row per subject — the key schema is [Auth_IdentityStore_Schema]'s — plus
24
+ `userId`, the domain id minted for it. */
25
+ let makeTable = (
26
+ ~name: string="IdentityStore",
27
+ ~opts: Pulumi.ComponentResource.options,
28
+ ): storeTable => {
29
+ let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
30
+ let table = Util_DynamoDb.makeTable(
31
+ ~attributes=[{name: Auth_IdentityStore_Schema.partitionKey, type_: "S"}],
32
+ ~tags=AWS.Tags.make(
33
+ ~name=name ++ "Table",
34
+ ~kind=ReventlessCore.ComponentType.Platform,
35
+ ~role=Auth,
36
+ ~scope=Platform,
37
+ ),
38
+ ~opts,
39
+ name,
40
+ )
41
+ {name: table.name, arn: table.arn}
42
+ }
43
+
44
+ let log = ReventlessCore.Logger.fromEnv()
45
+
46
+ /**
47
+ The [ProviderScoped] table if `provision-identity` has created it, else an empty
48
+ name and ARN.
49
+
50
+ 🚨 **Absent is a supported state, not a failed lookup** — unlike the active-role
51
+ store, which a supplied pool has always had. A pool provisioned before this store
52
+ existed has no table, and its deployments must keep deploying: the trigger then
53
+ adds no claim and every account keeps its `sub`, which is exactly what each one
54
+ did before. Running `provision-identity` again creates the table, and the next
55
+ deploy picks it up. A Pulumi `getTable` fails the whole program on a missing
56
+ table, so this asks DynamoDB directly.
57
+ */
58
+ let lookupTableIfPresent = (~tableName: string): storeTable => {
59
+ let arn = Pulumi.Output.make(tableName)->Pulumi.Output.flatMap(name =>
60
+ (
61
+ async () =>
62
+ try {
63
+ let out = await AwsSdk.DynamoDb_DynamoDb.DescribeTableCommand.make({
64
+ tableName: name,
65
+ })->AwsSdk.DynamoDb_DynamoDb.DescribeTableCommand.send
66
+ out.table->Option.flatMap(t => t.tableArn)->Option.getOr("")
67
+ } catch {
68
+ | exn if Util_AwsError.isNotFound(exn) =>
69
+ log.warn(
70
+ ~comp="IdentityStore",
71
+ `no identity store "${name}" on this provider — accounts keep their pool id. Run provision-identity to create it.`,
72
+ )
73
+ ""
74
+ }
75
+ )()->Pulumi.Output.fromPromise
76
+ )
77
+ {
78
+ name: arn->Pulumi.Output.apply(found => found == "" ? "" : tableName),
79
+ arn,
80
+ }
81
+ }
82
+
83
+ /** The store a resolved provider calls for — created, or looked up if present. */
84
+ let resolveTable = (
85
+ ~choice: Auth_ActiveRoleStore.storeChoice,
86
+ ~opts: Pulumi.ComponentResource.options,
87
+ ): storeTable =>
88
+ switch choice {
89
+ | StackScoped => makeTable(~opts)
90
+ | ProviderScoped(tableName) => lookupTableIfPresent(~tableName)
91
+ }
@@ -0,0 +1,85 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Pulumi from "@pulumi/pulumi";
6
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
7
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
8
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
9
+ import * as ClientDynamodb from "@aws-sdk/client-dynamodb";
10
+ import * as DynamoDb_DynamoDb$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DynamoDb.res.mjs";
11
+ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
12
+ import * as Util_AwsError$ReventlessAws from "../../util/Util_AwsError.res.mjs";
13
+ import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
14
+ import * as Auth_IdentityStore_Schema$ReventlessAws from "./Auth_IdentityStore_Schema.res.mjs";
15
+
16
+ function chooseStore(identityProviderId) {
17
+ if (identityProviderId !== undefined) {
18
+ return {
19
+ TAG: "ProviderScoped",
20
+ _0: Auth_IdentityStore_Schema$ReventlessAws.derivedStoreName(identityProviderId)
21
+ };
22
+ } else {
23
+ return "StackScoped";
24
+ }
25
+ }
26
+
27
+ function makeTable(nameOpt, opts) {
28
+ let name = nameOpt !== undefined ? nameOpt : "IdentityStore";
29
+ let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
30
+ let table = Util_DynamoDb$ReventlessAws.makeTable([{
31
+ name: Auth_IdentityStore_Schema$ReventlessAws.partitionKey,
32
+ type: "S"
33
+ }], undefined, undefined, undefined, AWS_Tags$ReventlessAws.make(name + "Table", "Platform", "Auth", "Platform", undefined, undefined, undefined, undefined), opts$1, name);
34
+ return {
35
+ name: table.name,
36
+ arn: table.arn
37
+ };
38
+ }
39
+
40
+ let log = Logger$ReventlessCore.fromEnv();
41
+
42
+ function lookupTableIfPresent(tableName) {
43
+ let arn = Output$Pulumi.flatMap(Pulumi.output(tableName), name => (async () => {
44
+ try {
45
+ let out = await DynamoDb_DynamoDb$AwsSdk.DescribeTableCommand.send(new ClientDynamodb.DescribeTableCommand({
46
+ TableName: name
47
+ }));
48
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(out.Table, t => t.TableArn), "");
49
+ } catch (raw_exn) {
50
+ let exn = Primitive_exceptions.internalToException(raw_exn);
51
+ if (Util_AwsError$ReventlessAws.isNotFound(exn)) {
52
+ log.warn("IdentityStore", undefined, `no identity store "` + name + `" on this provider — accounts keep their pool id. Run provision-identity to create it.`);
53
+ return "";
54
+ }
55
+ throw exn;
56
+ }
57
+ })());
58
+ return {
59
+ name: arn.apply(found => {
60
+ if (found === "") {
61
+ return "";
62
+ } else {
63
+ return tableName;
64
+ }
65
+ }),
66
+ arn: arn
67
+ };
68
+ }
69
+
70
+ function resolveTable(choice, opts) {
71
+ if (typeof choice !== "object") {
72
+ return makeTable(undefined, opts);
73
+ } else {
74
+ return lookupTableIfPresent(choice._0);
75
+ }
76
+ }
77
+
78
+ export {
79
+ chooseStore,
80
+ makeTable,
81
+ log,
82
+ lookupTableIfPresent,
83
+ resolveTable,
84
+ }
85
+ /* log Not a pure module */
@@ -0,0 +1,39 @@
1
+ // The identity store's identity: what it is called, and how its rows are keyed.
2
+ // The mapping from a Cognito `sub` to the domain's own user id — see
3
+ // [ReventlessCore.Auth_UserId] for why the two differ.
4
+ //
5
+ // One definition for the deploy that creates or looks the table up
6
+ // ([Auth_IdentityStore]), the pre-token trigger that reads it
7
+ // ([Auth_ActiveRoleTrigger_Ops]), and the provisioning scripts that write it
8
+ // (`provision-identity`, `provision-accounts`). Free of Pulumi and of the AWS
9
+ // SDK for the reason [Auth_ActiveRoleStore_Schema] gives.
10
+
11
+ /** Derived from the provider, never configured, for the reason the active-role
12
+ store's name is: two stacks on one pool cannot name different stores. */
13
+ let derivedStoreName = (~identityProviderId: string): string =>
14
+ `ReventlessIdentityStore-${identityProviderId}`
15
+
16
+ /** The Cognito `sub`. The trigger reads by it on every token it mints. */
17
+ let partitionKey = "id"
18
+
19
+ /** The domain id the row maps that subject to. */
20
+ let userIdAttribute = "userId"
21
+
22
+ /** Keyed on the subject alone: one identity, one domain id, on every platform
23
+ the provider serves. The reverse direction, domain id to subject, is an
24
+ index to add when the first caller for it exists; a global index can be
25
+ added to a live table. */
26
+ let expectedKeySchema: array<(string, string)> = [(partitionKey, "HASH")]
27
+
28
+ /** Why an existing table under this name cannot serve, if it cannot. A table
29
+ with the wrong key is found by the deploy and missed by every read, so the
30
+ claim is never added and every new account silently keeps its `sub`. */
31
+ let keySchemaRefusal = (~tableName: string, ~actual: array<(string, string)>): option<string> => {
32
+ let actualKey = Auth_ActiveRoleStore_Schema.describeKeySchema(actual)
33
+ let wantedKey = Auth_ActiveRoleStore_Schema.describeKeySchema(expectedKeySchema)
34
+ actualKey == wantedKey
35
+ ? None
36
+ : Some(
37
+ `table "${tableName}" already exists with key schema [${actualKey}], but the identity store needs [${wantedKey}]. A row written under one key is invisible to a read under the other, so every new account would silently keep its provider id. Rename or remove the table; its rows are a mapping the provisioning run rewrites.`,
38
+ )
39
+ }
@@ -0,0 +1,35 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Auth_ActiveRoleStore_Schema$ReventlessAws from "./Auth_ActiveRoleStore_Schema.res.mjs";
4
+
5
+ function derivedStoreName(identityProviderId) {
6
+ return `ReventlessIdentityStore-` + identityProviderId;
7
+ }
8
+
9
+ let partitionKey = "id";
10
+
11
+ let expectedKeySchema = [[
12
+ partitionKey,
13
+ "HASH"
14
+ ]];
15
+
16
+ function keySchemaRefusal(tableName, actual) {
17
+ let actualKey = Auth_ActiveRoleStore_Schema$ReventlessAws.describeKeySchema(actual);
18
+ let wantedKey = Auth_ActiveRoleStore_Schema$ReventlessAws.describeKeySchema(expectedKeySchema);
19
+ if (actualKey === wantedKey) {
20
+ return;
21
+ } else {
22
+ return `table "` + tableName + `" already exists with key schema [` + actualKey + `], but the identity store needs [` + wantedKey + `]. A row written under one key is invisible to a read under the other, so every new account would silently keep its provider id. Rename or remove the table; its rows are a mapping the provisioning run rewrites.`;
23
+ }
24
+ }
25
+
26
+ let userIdAttribute = "userId";
27
+
28
+ export {
29
+ derivedStoreName,
30
+ partitionKey,
31
+ userIdAttribute,
32
+ expectedKeySchema,
33
+ keySchemaRefusal,
34
+ }
35
+ /* No side effect */
@@ -0,0 +1,251 @@
1
+ // The identity provider a deployed platform provisions: principals in its
2
+ // Cognito pool, and each new one's domain id in the identity store the pool's
3
+ // pre-token trigger reads. The AWS counterpart of the local platform's
4
+ // `Capability_IdentityProvider_UserStore`, and reached the same way — on the
5
+ // capability record a slice's `translate` receives.
6
+ //
7
+ // Pulumi-free so it ships in the slice Lambda. The pool calls go through an
8
+ // injected `ops` record so the sequencing below — what is undone when a later
9
+ // step fails, which failures are verdicts — is testable without a pool.
10
+ //
11
+ // The principal's handle is the username, and the username is the address the
12
+ // person is reached at, as on the local platform.
13
+
14
+ open Reventless
15
+
16
+ /** The calls a provider makes, one per pool or store operation. Each throws the
17
+ SDK's error on failure; [failureOf] classifies it. */
18
+ type ops = {
19
+ /** Creates the account and answers its `sub`. */
20
+ createUser: (~username: string, ~attributes: array<(string, string)>) => promise<option<string>>,
21
+ subOf: (~username: string) => promise<option<string>>,
22
+ setPassword: (~username: string, ~password: string) => promise<unit>,
23
+ addToGroup: (~username: string, ~group: string) => promise<unit>,
24
+ removeFromGroup: (~username: string, ~group: string) => promise<unit>,
25
+ deleteUser: (~username: string) => promise<unit>,
26
+ putUserId: (~sub: string, ~userId: string) => promise<unit>,
27
+ deleteUserId: (~sub: string) => promise<unit>,
28
+ }
29
+
30
+ @get @return(nullable) external errorName: JsExn.t => option<string> = "name"
31
+
32
+ let codeOf = (exn: exn): option<string> => exn->JsExn.fromException->Option.flatMap(errorName)
33
+
34
+ let messageOf = (exn: exn): string =>
35
+ exn
36
+ ->JsExn.fromException
37
+ ->Option.flatMap(JsExn.message)
38
+ ->Option.getOr("the identity provider failed")
39
+
40
+ /**
41
+ A pool failure as the capability's answer.
42
+
43
+ `Refused` only where the pool has judged the request itself — a password its
44
+ policy rejects, an attribute it will not take, a group that does not exist.
45
+ Everything else is `Unavailable`, retried: throttling, a network blip, a role
46
+ missing its grant. A caller reaching `createPrincipal` has already recorded that
47
+ the address was proven, and treating an outage as a verdict would strand them.
48
+ */
49
+ let failureOf = (exn: exn): IdentityProvider.failure =>
50
+ switch codeOf(exn) {
51
+ | Some("UsernameExistsException" | "AliasExistsException") => Conflict
52
+ | Some(
53
+ "InvalidPasswordException"
54
+ | "InvalidParameterException"
55
+ | "UserNotFoundException"
56
+ | "ResourceNotFoundException",
57
+ ) =>
58
+ Refused(messageOf(exn))
59
+ | _ => Unavailable(messageOf(exn))
60
+ }
61
+
62
+ let usernameOf = (contact: Messaging.recipient): result<string, IdentityProvider.failure> =>
63
+ switch contact {
64
+ | ToEmail(email) => Ok(Email.toString(email))
65
+ | ToSms(phone) => Ok(Phone.toString(phone))
66
+ | ToPush(_) => Error(Refused("a push address cannot be signed in with"))
67
+ }
68
+
69
+ /** Verified, because a principal is only created after the address was proven —
70
+ the ordering the registration chapter is built on. */
71
+ let attributesOf = (contact: Messaging.recipient): array<(string, string)> =>
72
+ switch contact {
73
+ | ToEmail(email) => [("email", Email.toString(email)), ("email_verified", "true")]
74
+ | ToSms(phone) => [("phone_number", Phone.toString(phone)), ("phone_number_verified", "true")]
75
+ | ToPush(_) => []
76
+ }
77
+
78
+ let attempt = async (run: unit => promise<'a>): result<'a, IdentityProvider.failure> =>
79
+ try Ok(await run()) catch {
80
+ | exn => Error(failureOf(exn))
81
+ }
82
+
83
+ let withOps = (ops: ops, ~hasStore: bool): IdentityProvider.t => {
84
+ // Everything after the account exists, in order. A failure here undoes the
85
+ // account before answering, so a retry starts from nothing: left in place, the
86
+ // retry would meet `UsernameExistsException` and report a `Conflict` for a
87
+ // person whose first attempt merely timed out.
88
+ let complete = async (~username, ~sub, ~credential: IdentityProvider.credential, ~groups) => {
89
+ let steps = async () => {
90
+ switch credential {
91
+ | Password(password) => await ops.setPassword(~username, ~password)
92
+ | NoCredential => ()
93
+ }
94
+ for i in 0 to groups->Array.length - 1 {
95
+ await ops.addToGroup(~username, ~group=groups->Array.getUnsafe(i))
96
+ }
97
+
98
+ // Minted here and only here: an account the pool made before the store
99
+ // existed keeps its `sub`, and nothing re-keys it.
100
+ if hasStore {
101
+ await ops.putUserId(~sub, ~userId=ReventlessCore.Auth_UserId.mint())
102
+ }
103
+ }
104
+ switch await attempt(steps) {
105
+ | Ok() => Ok({IdentityProvider.providerId: username})
106
+ | Error(_) as failed =>
107
+ let _ = await attempt(() => ops.deleteUser(~username))
108
+ failed
109
+ }
110
+ }
111
+
112
+ let createPrincipal = async (~contact, ~credential, ~groups) =>
113
+ switch usernameOf(contact) {
114
+ | Error(_) as e => e
115
+ | Ok(username) =>
116
+ switch await attempt(() => ops.createUser(~username, ~attributes=attributesOf(contact))) {
117
+ | Error(_) as e => e
118
+ | Ok(created) =>
119
+ let sub = switch created {
120
+ | Some(_) => created
121
+ | None => (await attempt(() => ops.subOf(~username)))->Result.getOr(None)
122
+ }
123
+ switch sub {
124
+ | None =>
125
+ let _ = await attempt(() => ops.deleteUser(~username))
126
+ Error(IdentityProvider.Unavailable(`the pool created ${username} but named no sub`))
127
+ | Some(sub) => await complete(~username, ~sub, ~credential, ~groups)
128
+ }
129
+ }
130
+ }
131
+
132
+ let addToGroup = (~principal: IdentityProvider.principal, ~group) =>
133
+ attempt(() => ops.addToGroup(~username=principal.providerId, ~group))
134
+
135
+ let removeFromGroup = (~principal: IdentityProvider.principal, ~group) =>
136
+ attempt(() => ops.removeFromGroup(~username=principal.providerId, ~group))
137
+
138
+ // Deleting what is gone is the retry of a delete that happened, so a missing
139
+ // account answers `Ok`. The mapping row goes last, best effort: a row whose
140
+ // principal is gone is never read on the request path, since no token is
141
+ // minted for it.
142
+ let deletePrincipal = async (~principal: IdentityProvider.principal) => {
143
+ let username = principal.providerId
144
+ switch await attempt(() => ops.subOf(~username)) {
145
+ | Error(Refused(_)) => Ok()
146
+ | Error(_) as e => e
147
+ | Ok(sub) =>
148
+ switch await attempt(() => ops.deleteUser(~username)) {
149
+ | Error(Unavailable(_)) as e => e
150
+ | Error(_) | Ok() =>
151
+ switch (sub, hasStore) {
152
+ | (Some(sub), true) =>
153
+ let _ = await attempt(() => ops.deleteUserId(~sub))
154
+ | _ => ()
155
+ }
156
+ Ok()
157
+ }
158
+ }
159
+ }
160
+
161
+ IdentityProvider.make(
162
+ ~createPrincipal,
163
+ ~addToGroup,
164
+ ~removeFromGroup,
165
+ ~deletePrincipal,
166
+ ~operations=[CreatePrincipal, AddToGroup, RemoveFromGroup, DeletePrincipal],
167
+ )
168
+ }
169
+
170
+ // ── The SDK ──────────────────────────────────────────────────────────────────
171
+
172
+ module Cognito = AwsSdk.CognitoIdentityServiceProvider
173
+
174
+ let subFrom = (attributes: option<array<Cognito.AdminCreateUserCommand.attributeType>>) =>
175
+ attributes->Option.getOr([])->Array.find(a => a.name == "sub")->Option.map(a => a.value)
176
+
177
+ let sdkOps = (~poolId: string, ~identityStore: string): ops => {
178
+ createUser: async (~username, ~attributes) => {
179
+ let out = await Cognito.AdminCreateUserCommand.make({
180
+ userPoolId: poolId,
181
+ username,
182
+ userAttributes: attributes->Array.map(((name, value)) => {
183
+ Cognito.AdminCreateUserCommand.name,
184
+ value,
185
+ }),
186
+ // No invitation: the person has proven the address already, and the
187
+ // invitation would carry a temporary password nobody should use.
188
+ messageAction: "SUPPRESS",
189
+ })->Cognito.AdminCreateUserCommand.send
190
+ out.user->Option.flatMap(u => subFrom(u.attributes))
191
+ },
192
+ subOf: async (~username) => {
193
+ let out = await Cognito.AdminGetUserCommand.make({
194
+ userPoolId: poolId,
195
+ username,
196
+ })->Cognito.AdminGetUserCommand.send
197
+ out.userAttributes->Option.getOr([])->Array.find(a => a.name == "sub")->Option.map(a => a.value)
198
+ },
199
+ setPassword: (~username, ~password) =>
200
+ Cognito.AdminSetUserPasswordCommand.make({
201
+ userPoolId: poolId,
202
+ username,
203
+ password,
204
+ permanent: true,
205
+ })->Cognito.AdminSetUserPasswordCommand.send,
206
+ addToGroup: (~username, ~group) =>
207
+ Cognito.AdminAddUserToGroupCommand.make({
208
+ userPoolId: poolId,
209
+ username,
210
+ groupName: group,
211
+ })->Cognito.AdminAddUserToGroupCommand.send,
212
+ removeFromGroup: (~username, ~group) =>
213
+ Cognito.AdminRemoveUserFromGroupCommand.make({
214
+ userPoolId: poolId,
215
+ username,
216
+ groupName: group,
217
+ })->Cognito.AdminRemoveUserFromGroupCommand.send,
218
+ deleteUser: (~username) =>
219
+ Cognito.AdminDeleteUserCommand.make({
220
+ userPoolId: poolId,
221
+ username,
222
+ })->Cognito.AdminDeleteUserCommand.send,
223
+ putUserId: async (~sub, ~userId) => {
224
+ let _ = await AwsSdk.DynamoDb_DocumentClient.PutCommand.make({
225
+ tableName: identityStore,
226
+ item: JSON.Encode.object(
227
+ Dict.fromArray([
228
+ (Auth_IdentityStore_Schema.partitionKey, JSON.Encode.string(sub)),
229
+ (Auth_IdentityStore_Schema.userIdAttribute, JSON.Encode.string(userId)),
230
+ ]),
231
+ ),
232
+ })->AwsSdk.DynamoDb_DocumentClient.PutCommand.send
233
+ },
234
+ deleteUserId: async (~sub) => {
235
+ let _ = await AwsSdk.DynamoDb_DocumentClient.DeleteCommand.make({
236
+ tableName: identityStore,
237
+ key: Dict.fromArray([(Auth_IdentityStore_Schema.partitionKey, JSON.Encode.string(sub))]),
238
+ })->AwsSdk.DynamoDb_DocumentClient.DeleteCommand.send
239
+ },
240
+ }
241
+
242
+ /**
243
+ The provider a slice Lambda is handed, from the two values the deploy put in its
244
+ environment. No pool is the deployment gap it always was, answered `Unavailable`
245
+ with no operations. No store means the pool predates it: principals are still
246
+ made, and each keeps its `sub` as its id, as every account before the store did.
247
+ */
248
+ let provider = (~poolId: string, ~identityStore: string): IdentityProvider.t =>
249
+ poolId == ""
250
+ ? IdentityProvider.unavailable(~reason="no identity provider is configured for this platform")
251
+ : withOps(sdkOps(~poolId, ~identityStore), ~hasStore=identityStore != "")