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

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 +8 -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 +16 -7
  14. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +16 -3
  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 +52 -0
  20. package/src/adapter/Auth/Auth_IdentityStore.res.mjs +46 -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,46 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
4
+ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
5
+ import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
6
+ import * as Auth_ActiveRoleStore$ReventlessAws from "./Auth_ActiveRoleStore.res.mjs";
7
+ import * as Auth_IdentityStore_Schema$ReventlessAws from "./Auth_IdentityStore_Schema.res.mjs";
8
+
9
+ function chooseStore(identityProviderId) {
10
+ if (identityProviderId !== undefined) {
11
+ return {
12
+ TAG: "ProviderScoped",
13
+ _0: Auth_IdentityStore_Schema$ReventlessAws.derivedStoreName(identityProviderId)
14
+ };
15
+ } else {
16
+ return "StackScoped";
17
+ }
18
+ }
19
+
20
+ function makeTable(nameOpt, opts) {
21
+ let name = nameOpt !== undefined ? nameOpt : "IdentityStore";
22
+ let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
23
+ let table = Util_DynamoDb$ReventlessAws.makeTable([{
24
+ name: Auth_IdentityStore_Schema$ReventlessAws.partitionKey,
25
+ type: "S"
26
+ }], undefined, undefined, undefined, AWS_Tags$ReventlessAws.make(name + "Table", "Platform", "Auth", "Platform", undefined, undefined, undefined, undefined), opts$1, name);
27
+ return {
28
+ name: table.name,
29
+ arn: table.arn
30
+ };
31
+ }
32
+
33
+ function resolveTable(choice, opts) {
34
+ if (typeof choice !== "object") {
35
+ return makeTable(undefined, opts);
36
+ } else {
37
+ return Auth_ActiveRoleStore$ReventlessAws.lookupTable(choice._0);
38
+ }
39
+ }
40
+
41
+ export {
42
+ chooseStore,
43
+ makeTable,
44
+ resolveTable,
45
+ }
46
+ /* AWS_Tags-ReventlessAws 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 != "")
@@ -0,0 +1,300 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
+ import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
9
+ import * as Auth_UserId$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_UserId.res.mjs";
10
+ import * as IdentityProvider$Reventless from "@reventlessdev/reventless-spec/src/semantic/IdentityProvider.res.mjs";
11
+ import * as DynamoDb_DocumentClient$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DocumentClient.res.mjs";
12
+ import * as CognitoIdentityServiceProvider$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/CognitoIdentityServiceProvider.res.mjs";
13
+ import * as Auth_IdentityStore_Schema$ReventlessAws from "../Auth/Auth_IdentityStore_Schema.res.mjs";
14
+ import * as ClientCognitoIdentityProvider from "@aws-sdk/client-cognito-identity-provider";
15
+
16
+ function codeOf(exn) {
17
+ return Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), prim => Primitive_option.fromNullable(prim.name));
18
+ }
19
+
20
+ function messageOf(exn) {
21
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "the identity provider failed");
22
+ }
23
+
24
+ function failureOf(exn) {
25
+ let match = codeOf(exn);
26
+ if (match === undefined) {
27
+ return {
28
+ TAG: "Unavailable",
29
+ _0: messageOf(exn)
30
+ };
31
+ }
32
+ switch (match) {
33
+ case "InvalidParameterException" :
34
+ case "InvalidPasswordException" :
35
+ case "ResourceNotFoundException" :
36
+ case "UserNotFoundException" :
37
+ return {
38
+ TAG: "Refused",
39
+ _0: messageOf(exn)
40
+ };
41
+ case "AliasExistsException" :
42
+ case "UsernameExistsException" :
43
+ return "Conflict";
44
+ default:
45
+ return {
46
+ TAG: "Unavailable",
47
+ _0: messageOf(exn)
48
+ };
49
+ }
50
+ }
51
+
52
+ function usernameOf(contact) {
53
+ switch (contact.TAG) {
54
+ case "ToEmail" :
55
+ case "ToSms" :
56
+ return {
57
+ TAG: "Ok",
58
+ _0: contact._0
59
+ };
60
+ case "ToPush" :
61
+ return {
62
+ TAG: "Error",
63
+ _0: {
64
+ TAG: "Refused",
65
+ _0: "a push address cannot be signed in with"
66
+ }
67
+ };
68
+ }
69
+ }
70
+
71
+ function attributesOf(contact) {
72
+ switch (contact.TAG) {
73
+ case "ToEmail" :
74
+ return [
75
+ [
76
+ "email",
77
+ contact._0
78
+ ],
79
+ [
80
+ "email_verified",
81
+ "true"
82
+ ]
83
+ ];
84
+ case "ToSms" :
85
+ return [
86
+ [
87
+ "phone_number",
88
+ contact._0
89
+ ],
90
+ [
91
+ "phone_number_verified",
92
+ "true"
93
+ ]
94
+ ];
95
+ case "ToPush" :
96
+ return [];
97
+ }
98
+ }
99
+
100
+ async function attempt(run) {
101
+ try {
102
+ return {
103
+ TAG: "Ok",
104
+ _0: await run()
105
+ };
106
+ } catch (raw_exn) {
107
+ let exn = Primitive_exceptions.internalToException(raw_exn);
108
+ return {
109
+ TAG: "Error",
110
+ _0: failureOf(exn)
111
+ };
112
+ }
113
+ }
114
+
115
+ function withOps(ops, hasStore) {
116
+ let complete = async (username, sub, credential, groups) => {
117
+ let steps = async () => {
118
+ if (typeof credential === "object") {
119
+ await ops.setPassword(username, credential._0);
120
+ }
121
+ for (let i = 0, i_finish = groups.length; i < i_finish; ++i) {
122
+ await ops.addToGroup(username, groups[i]);
123
+ }
124
+ if (hasStore) {
125
+ return await ops.putUserId(sub, Auth_UserId$ReventlessCore.mint());
126
+ }
127
+ };
128
+ let failed = await attempt(steps);
129
+ if (failed.TAG === "Ok") {
130
+ return {
131
+ TAG: "Ok",
132
+ _0: {
133
+ providerId: username
134
+ }
135
+ };
136
+ }
137
+ await attempt(() => ops.deleteUser(username));
138
+ return failed;
139
+ };
140
+ let createPrincipal = async (contact, credential, groups) => {
141
+ let e = usernameOf(contact);
142
+ if (e.TAG !== "Ok") {
143
+ return e;
144
+ }
145
+ let username = e._0;
146
+ let e$1 = await attempt(() => ops.createUser(username, attributesOf(contact)));
147
+ if (e$1.TAG !== "Ok") {
148
+ return e$1;
149
+ }
150
+ let created = e$1._0;
151
+ let sub = created !== undefined ? created : Stdlib_Result.getOr(await attempt(() => ops.subOf(username)), undefined);
152
+ if (sub !== undefined) {
153
+ return await complete(username, sub, credential, groups);
154
+ } else {
155
+ await attempt(() => ops.deleteUser(username));
156
+ return {
157
+ TAG: "Error",
158
+ _0: {
159
+ TAG: "Unavailable",
160
+ _0: `the pool created ` + username + ` but named no sub`
161
+ }
162
+ };
163
+ }
164
+ };
165
+ let addToGroup = (principal, group) => attempt(() => ops.addToGroup(principal.providerId, group));
166
+ let removeFromGroup = (principal, group) => attempt(() => ops.removeFromGroup(principal.providerId, group));
167
+ let deletePrincipal = async principal => {
168
+ let username = principal.providerId;
169
+ let e = await attempt(() => ops.subOf(username));
170
+ if (e.TAG === "Ok") {
171
+ let sub = e._0;
172
+ let e$1 = await attempt(() => ops.deleteUser(username));
173
+ if (e$1.TAG !== "Ok") {
174
+ let tmp = e$1._0;
175
+ if (typeof tmp === "object" && tmp.TAG === "Unavailable") {
176
+ return e$1;
177
+ }
178
+ }
179
+ if (sub !== undefined && hasStore) {
180
+ await attempt(() => ops.deleteUserId(sub));
181
+ }
182
+ return {
183
+ TAG: "Ok",
184
+ _0: undefined
185
+ };
186
+ }
187
+ let tmp$1 = e._0;
188
+ if (typeof tmp$1 !== "object" || tmp$1.TAG === "Unavailable") {
189
+ return e;
190
+ } else {
191
+ return {
192
+ TAG: "Ok",
193
+ _0: undefined
194
+ };
195
+ }
196
+ };
197
+ return IdentityProvider$Reventless.make(createPrincipal, addToGroup, removeFromGroup, deletePrincipal, [
198
+ "CreatePrincipal",
199
+ "AddToGroup",
200
+ "RemoveFromGroup",
201
+ "DeletePrincipal"
202
+ ]);
203
+ }
204
+
205
+ function subFrom(attributes) {
206
+ return Stdlib_Option.map(Stdlib_Option.getOr(attributes, []).find(a => a.Name === "sub"), a => a.Value);
207
+ }
208
+
209
+ function sdkOps(poolId, identityStore) {
210
+ return {
211
+ createUser: async (username, attributes) => {
212
+ let out = await CognitoIdentityServiceProvider$AwsSdk.AdminCreateUserCommand.send(new ClientCognitoIdentityProvider.AdminCreateUserCommand({
213
+ UserPoolId: poolId,
214
+ Username: username,
215
+ UserAttributes: attributes.map(param => ({
216
+ Name: param[0],
217
+ Value: param[1]
218
+ })),
219
+ MessageAction: "SUPPRESS"
220
+ }));
221
+ return Stdlib_Option.flatMap(out.User, u => subFrom(u.Attributes));
222
+ },
223
+ subOf: async username => {
224
+ let out = await CognitoIdentityServiceProvider$AwsSdk.AdminGetUserCommand.send(new ClientCognitoIdentityProvider.AdminGetUserCommand({
225
+ UserPoolId: poolId,
226
+ Username: username
227
+ }));
228
+ return Stdlib_Option.map(Stdlib_Option.getOr(out.UserAttributes, []).find(a => a.Name === "sub"), a => a.Value);
229
+ },
230
+ setPassword: (username, password) => CognitoIdentityServiceProvider$AwsSdk.AdminSetUserPasswordCommand.send(new ClientCognitoIdentityProvider.AdminSetUserPasswordCommand({
231
+ UserPoolId: poolId,
232
+ Username: username,
233
+ Password: password,
234
+ Permanent: true
235
+ })),
236
+ addToGroup: (username, group) => CognitoIdentityServiceProvider$AwsSdk.AdminAddUserToGroupCommand.send(new ClientCognitoIdentityProvider.AdminAddUserToGroupCommand({
237
+ Username: username,
238
+ GroupName: group,
239
+ UserPoolId: poolId
240
+ })),
241
+ removeFromGroup: (username, group) => CognitoIdentityServiceProvider$AwsSdk.AdminRemoveUserFromGroupCommand.send(new ClientCognitoIdentityProvider.AdminRemoveUserFromGroupCommand({
242
+ Username: username,
243
+ GroupName: group,
244
+ UserPoolId: poolId
245
+ })),
246
+ deleteUser: username => CognitoIdentityServiceProvider$AwsSdk.AdminDeleteUserCommand.send(new ClientCognitoIdentityProvider.AdminDeleteUserCommand({
247
+ UserPoolId: poolId,
248
+ Username: username
249
+ })),
250
+ putUserId: async (sub, userId) => {
251
+ await DynamoDb_DocumentClient$AwsSdk.PutCommand.send(new LibDynamodb.PutCommand({
252
+ Item: Object.fromEntries([
253
+ [
254
+ Auth_IdentityStore_Schema$ReventlessAws.partitionKey,
255
+ sub
256
+ ],
257
+ [
258
+ Auth_IdentityStore_Schema$ReventlessAws.userIdAttribute,
259
+ userId
260
+ ]
261
+ ]),
262
+ TableName: identityStore
263
+ }));
264
+ },
265
+ deleteUserId: async sub => {
266
+ await DynamoDb_DocumentClient$AwsSdk.DeleteCommand.send(new LibDynamodb.DeleteCommand({
267
+ TableName: identityStore,
268
+ Key: Object.fromEntries([[
269
+ Auth_IdentityStore_Schema$ReventlessAws.partitionKey,
270
+ sub
271
+ ]])
272
+ }));
273
+ }
274
+ };
275
+ }
276
+
277
+ function provider(poolId, identityStore) {
278
+ if (poolId === "") {
279
+ return IdentityProvider$Reventless.unavailable("no identity provider is configured for this platform");
280
+ } else {
281
+ return withOps(sdkOps(poolId, identityStore), identityStore !== "");
282
+ }
283
+ }
284
+
285
+ let Cognito;
286
+
287
+ export {
288
+ codeOf,
289
+ messageOf,
290
+ failureOf,
291
+ usernameOf,
292
+ attributesOf,
293
+ attempt,
294
+ withOps,
295
+ Cognito,
296
+ subFrom,
297
+ sdkOps,
298
+ provider,
299
+ }
300
+ /* Stdlib_JsExn Not a pure module */
@@ -420,10 +420,13 @@ let capabilities = (): Reventless.Capabilities.t => {
420
420
  ~text,
421
421
  ),
422
422
  messaging: messagingEmailProvider(),
423
- // Refusing until the Cognito backend lands. The pool is provisioned, but the
424
- // store mapping a domain user id to a provider handle is not, and creating a
425
- // principal before that exists would write a handle with nowhere to live.
426
- identityProvider: Reventless.Capabilities.none.identityProvider,
423
+ // The platform's pool and identity store, both read per call like the sender
424
+ // above. An empty pool id is a plugin deployed with no platform to read one
425
+ // from, and answers `Unavailable` with no operations.
426
+ identityProvider: IdentityProvider_Cognito_Backend.provider(
427
+ ~poolId=NodeProcess.env->Dict.get("IDENTITY_PROVIDER_ID")->Option.getOr(""),
428
+ ~identityStore=NodeProcess.env->Dict.get("IDENTITY_STORE_TABLE")->Option.getOr(""),
429
+ ),
427
430
  secrets: ReventlessCore.Secrets_Node_Backend.provider,
428
431
  }
429
432