@reventlessdev/reventless-aws 3.0.0-alpha.319 → 3.0.0-alpha.320

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 (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +9 -5
  3. package/rescript.json +4 -0
  4. package/run-provision-identity.mjs +3 -0
  5. package/scripts/ProvisionIdentity.res +303 -0
  6. package/scripts/ProvisionIdentity.res.mjs +376 -0
  7. package/src/Platform.res +54 -30
  8. package/src/Platform.res.mjs +34 -32
  9. package/src/Platform_Stack.res +102 -19
  10. package/src/Platform_Stack.res.mjs +43 -7
  11. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +267 -32
  12. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +124 -14
  13. package/src/adapter/Auth/Auth_ActiveRoleStore.res +93 -5
  14. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +53 -4
  15. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +50 -4
  16. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +24 -3
  17. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res +92 -0
  18. package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res.mjs +49 -0
  19. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +21 -7
  20. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +15 -7
  21. package/src/util/Util_AwsError.res +56 -0
  22. package/src/util/Util_AwsError.res.mjs +59 -0
  23. package/src/util/Util_ShellConfig.res +28 -0
  24. package/src/util/Util_ShellConfig.res.mjs +22 -0
  25. package/tests/Auth_ActiveRolePoolAttachmentTest.res +175 -0
  26. package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +136 -0
  27. package/tests/Auth_ActiveRoleStoreTest.res +65 -0
  28. package/tests/Auth_ActiveRoleStoreTest.res.mjs +48 -0
  29. package/tests/Auth_ActiveRoleStore_SchemaTest.res +98 -0
  30. package/tests/Auth_ActiveRoleStore_SchemaTest.res.mjs +97 -0
  31. package/tests/AwsErrorFixtures.mjs +16 -0
  32. package/tests/ProvisionIdentityTest.res +109 -0
  33. package/tests/ProvisionIdentityTest.res.mjs +132 -0
  34. package/tests/Util_AwsErrorTest.res +127 -0
  35. package/tests/Util_AwsErrorTest.res.mjs +76 -0
  36. package/tests/Util_ShellConfigTest.res +51 -0
  37. package/tests/Util_ShellConfigTest.res.mjs +42 -0
@@ -0,0 +1,132 @@
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_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
5
+ import * as ProvisionIdentity$ReventlessAws from "../scripts/ProvisionIdentity.res.mjs";
6
+
7
+ globalThis.describe("ProvisionIdentity.parseArgs", () => {
8
+ globalThis.test("no arguments means adopt-or-create the default name", () => {
9
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([])).toEqual({
10
+ TAG: "Ok",
11
+ _0: {
12
+ poolName: ProvisionIdentity$ReventlessAws.defaultPoolName,
13
+ providerId: undefined,
14
+ help: false
15
+ }
16
+ });
17
+ });
18
+ globalThis.test("--provider-id names an existing pool", () => {
19
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
20
+ "--provider-id",
21
+ "eu-west-1_AbCdEfGhI"
22
+ ])).toEqual({
23
+ TAG: "Ok",
24
+ _0: {
25
+ poolName: ProvisionIdentity$ReventlessAws.defaultPoolName,
26
+ providerId: "eu-west-1_AbCdEfGhI",
27
+ help: false
28
+ }
29
+ });
30
+ });
31
+ globalThis.test("--name selects which pool to adopt by name", () => {
32
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
33
+ "--name",
34
+ "examples-dev"
35
+ ])).toEqual({
36
+ TAG: "Ok",
37
+ _0: {
38
+ poolName: "examples-dev",
39
+ providerId: undefined,
40
+ help: false
41
+ }
42
+ });
43
+ });
44
+ globalThis.test("both together are accepted, and the id wins at resolve time", () => {
45
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
46
+ "--name",
47
+ "ignored",
48
+ "--provider-id",
49
+ "eu-west-1_x"
50
+ ])).toEqual({
51
+ TAG: "Ok",
52
+ _0: {
53
+ poolName: "ignored",
54
+ providerId: "eu-west-1_x",
55
+ help: false
56
+ }
57
+ });
58
+ });
59
+ globalThis.test("a misspelled flag is refused, not ignored", () => {
60
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
61
+ "--provider_id",
62
+ "eu-west-1_x"
63
+ ])).toEqual({
64
+ TAG: "Error",
65
+ _0: `unknown argument "--provider_id"`
66
+ });
67
+ });
68
+ globalThis.test("a flag with no value is refused", () => {
69
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs(["--provider-id"])).toEqual({
70
+ TAG: "Error",
71
+ _0: "--provider-id needs a value"
72
+ });
73
+ });
74
+ globalThis.test("--name with no value is refused too", () => {
75
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs(["--name"])).toEqual({
76
+ TAG: "Error",
77
+ _0: "--name needs a value"
78
+ });
79
+ });
80
+ globalThis.test("--help and -h both ask for the usage text", () => {
81
+ globalThis.expect([
82
+ Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs(["--help"]), a => a.help),
83
+ Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs(["-h"]), a => a.help)
84
+ ]).toEqual([
85
+ {
86
+ TAG: "Ok",
87
+ _0: true
88
+ },
89
+ {
90
+ TAG: "Ok",
91
+ _0: true
92
+ }
93
+ ]);
94
+ });
95
+ globalThis.test("the first refusal is the one reported", () => {
96
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
97
+ "--nope",
98
+ "--also-nope"
99
+ ])).toEqual({
100
+ TAG: "Error",
101
+ _0: `unknown argument "--nope"`
102
+ });
103
+ });
104
+ });
105
+
106
+ globalThis.describe("ProvisionIdentity.poolSettings", () => {
107
+ let settings = ProvisionIdentity$ReventlessAws.poolSettings("MyIdentity");
108
+ globalThis.test("carries the name it was asked for", () => {
109
+ globalThis.expect(settings.PoolName).toBe("MyIdentity");
110
+ });
111
+ globalThis.test("matches what auto mode declares: email sign-in, no MFA, admin-only", () => {
112
+ globalThis.expect([
113
+ settings.UsernameAttributes,
114
+ settings.MfaConfiguration,
115
+ Stdlib_Option.flatMap(settings.AdminCreateUserConfig, c => c.AllowAdminCreateUserOnly)
116
+ ]).toEqual([
117
+ ["email"],
118
+ "OFF",
119
+ true
120
+ ]);
121
+ });
122
+ globalThis.test("keeps the 12-character password policy", () => {
123
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(settings.Policies, p => p.PasswordPolicy), p => p.MinimumLength)).toEqual(12);
124
+ });
125
+ });
126
+
127
+ let Provision;
128
+
129
+ export {
130
+ Provision,
131
+ }
132
+ /* Not a pure module */
@@ -0,0 +1,127 @@
1
+ open JestGlobals
2
+
3
+ // 🚨 These exist because the bug they cover shipped, and shipped precisely
4
+ // because the predicate lived inside a script that runs on import — nothing could
5
+ // reach it to assert anything.
6
+ //
7
+ // The fixtures below are the **real** shapes, captured from live calls rather
8
+ // than imagined. That is the whole point: the original guard tested the message
9
+ // for the error code, and the message never contains it.
10
+
11
+ module AwsError = Util_AwsError
12
+
13
+ @module("./AwsErrorFixtures.mjs")
14
+ external throwAwsError: (string, string) => unit = "throwAwsError"
15
+
16
+ /** The shape an AWS SDK v3 client actually throws, caught the way the real code
17
+ catches it.
18
+
19
+ 🚨 **Thrown and caught, not constructed.** ReScript's `exn` for a JS error is
20
+ the wrapper its own `catch` builds — a bare `Error` value is a different thing
21
+ and `JsExn.fromException` does not recognise it. A fixture that returned one
22
+ would make every predicate here look broken while the code was fine, which is
23
+ exactly the wrong way round for a regression test. */
24
+ let awsError = (~name: string, ~message: string): exn =>
25
+ try {
26
+ throwAwsError(name, message)
27
+ Not_found
28
+ } catch {
29
+ | e => e
30
+ }
31
+
32
+ describe("Util_AwsError.isNotFound", () => {
33
+ // Captured from `DescribeTable` on a table that does not exist. Note the
34
+ // message: it does NOT contain "ResourceNotFoundException" anywhere, which is
35
+ // exactly why a message-only guard could never match it.
36
+ let dynamoNotFound = awsError(
37
+ ~name="ResourceNotFoundException",
38
+ ~message="Requested resource not found: Table: ReventlessNoSuchTable-probe not found",
39
+ )
40
+
41
+ // Captured from `DescribeUserPool` on a pool that does not exist.
42
+ let cognitoNotFound = awsError(
43
+ ~name="ResourceNotFoundException",
44
+ ~message="User pool eu-west-1_zzzNoSuchPl does not exist.",
45
+ )
46
+
47
+ testSync("a missing DynamoDB table is recognised from the name alone", () =>
48
+ expect(AwsError.isNotFound(dynamoNotFound))->toBe(true)
49
+ )
50
+
51
+ // The regression, stated as the property rather than as the fix: this is what
52
+ // the original guard tested, and it is false for the real error.
53
+ testSync("...and its message does not carry the code, which is why", () =>
54
+ expect(
55
+ AwsError.describe(dynamoNotFound)->String.includes(
56
+ "Requested resource not found",
57
+ ),
58
+ )->toBe(true)
59
+ )
60
+
61
+ testSync("a missing Cognito pool is recognised too", () =>
62
+ expect(AwsError.isNotFound(cognitoNotFound))->toBe(true)
63
+ )
64
+
65
+ // A wrapped or re-thrown error can lose its `name` while keeping the code in
66
+ // the text, so the fallback earns its place.
67
+ testSync("the code in the message alone still matches", () =>
68
+ expect(
69
+ AwsError.isNotFound(
70
+ awsError(~name="Error", ~message="ResourceNotFoundException: wrapped somewhere"),
71
+ ),
72
+ )->toBe(true)
73
+ )
74
+
75
+ // 🚨 The direction that matters for safety. `describeTable` swallows this
76
+ // exception; a guard that said `true` here would report a table as absent when
77
+ // the call was actually refused, and the script would then try to create a
78
+ // table that already exists.
79
+ testSync("a permission failure is not an absence", () =>
80
+ expect(
81
+ AwsError.isNotFound(
82
+ awsError(
83
+ ~name="AccessDeniedException",
84
+ ~message="User is not authorized to perform: dynamodb:DescribeTable",
85
+ ),
86
+ ),
87
+ )->toBe(false)
88
+ )
89
+
90
+ testSync("a throttle is not an absence either", () =>
91
+ expect(
92
+ AwsError.isNotFound(
93
+ awsError(~name="ProvisionedThroughputExceededException", ~message="slow down"),
94
+ ),
95
+ )->toBe(false)
96
+ )
97
+
98
+ testSync("something that is not a JS error at all is not an absence", () =>
99
+ expect(AwsError.isNotFound(Not_found))->toBe(false)
100
+ )
101
+ })
102
+
103
+ // Without this the top of a CLI reports Node's own
104
+ // `UnhandledPromiseRejection ... "#<Object>"`, which names neither the failing
105
+ // call nor the reason.
106
+ describe("Util_AwsError.describe", () => {
107
+ testSync("names the code and the reason", () =>
108
+ expect(
109
+ AwsError.describe(awsError(~name="AccessDeniedException", ~message="not authorized")),
110
+ )->toBe("AccessDeniedException: not authorized")
111
+ )
112
+
113
+ testSync("a non-error value still produces a sentence", () =>
114
+ expect(AwsError.describe(Not_found))->toBe("an unexpected error escaped")
115
+ )
116
+
117
+ // Total on purpose: a describer that threw would replace one unreadable
118
+ // failure with another.
119
+ testSync("every shape yields something an operator can read", () =>
120
+ expect(
121
+ [
122
+ awsError(~name="X", ~message="y"),
123
+ Not_found,
124
+ ]->Array.every(e => AwsError.describe(e)->String.length > 0),
125
+ )->toBe(true)
126
+ )
127
+ })
@@ -0,0 +1,76 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
4
+ import * as AwsErrorFixturesMjs from "./AwsErrorFixtures.mjs";
5
+ import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
6
+
7
+ function throwAwsError(prim0, prim1) {
8
+ AwsErrorFixturesMjs.throwAwsError(prim0, prim1);
9
+ }
10
+
11
+ function awsError(name, message) {
12
+ try {
13
+ AwsErrorFixturesMjs.throwAwsError(name, message);
14
+ return {
15
+ RE_EXN_ID: "Not_found"
16
+ };
17
+ } catch (raw_e) {
18
+ return Primitive_exceptions.internalToException(raw_e);
19
+ }
20
+ }
21
+
22
+ globalThis.describe("Util_AwsError.isNotFound", () => {
23
+ let dynamoNotFound = awsError("ResourceNotFoundException", "Requested resource not found: Table: ReventlessNoSuchTable-probe not found");
24
+ let cognitoNotFound = awsError("ResourceNotFoundException", "User pool eu-west-1_zzzNoSuchPl does not exist.");
25
+ globalThis.test("a missing DynamoDB table is recognised from the name alone", () => {
26
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(dynamoNotFound)).toBe(true);
27
+ });
28
+ globalThis.test("...and its message does not carry the code, which is why", () => {
29
+ globalThis.expect(Util_AwsError$ReventlessAws.describe(dynamoNotFound).includes("Requested resource not found")).toBe(true);
30
+ });
31
+ globalThis.test("a missing Cognito pool is recognised too", () => {
32
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(cognitoNotFound)).toBe(true);
33
+ });
34
+ globalThis.test("the code in the message alone still matches", () => {
35
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(awsError("Error", "ResourceNotFoundException: wrapped somewhere"))).toBe(true);
36
+ });
37
+ globalThis.test("a permission failure is not an absence", () => {
38
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(awsError("AccessDeniedException", "User is not authorized to perform: dynamodb:DescribeTable"))).toBe(false);
39
+ });
40
+ globalThis.test("a throttle is not an absence either", () => {
41
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound(awsError("ProvisionedThroughputExceededException", "slow down"))).toBe(false);
42
+ });
43
+ globalThis.test("something that is not a JS error at all is not an absence", () => {
44
+ globalThis.expect(Util_AwsError$ReventlessAws.isNotFound({
45
+ RE_EXN_ID: "Not_found"
46
+ })).toBe(false);
47
+ });
48
+ });
49
+
50
+ globalThis.describe("Util_AwsError.describe", () => {
51
+ globalThis.test("names the code and the reason", () => {
52
+ globalThis.expect(Util_AwsError$ReventlessAws.describe(awsError("AccessDeniedException", "not authorized"))).toBe("AccessDeniedException: not authorized");
53
+ });
54
+ globalThis.test("a non-error value still produces a sentence", () => {
55
+ globalThis.expect(Util_AwsError$ReventlessAws.describe({
56
+ RE_EXN_ID: "Not_found"
57
+ })).toBe("an unexpected error escaped");
58
+ });
59
+ globalThis.test("every shape yields something an operator can read", () => {
60
+ globalThis.expect([
61
+ awsError("X", "y"),
62
+ {
63
+ RE_EXN_ID: "Not_found"
64
+ }
65
+ ].every(e => Util_AwsError$ReventlessAws.describe(e).length > 0)).toBe(true);
66
+ });
67
+ });
68
+
69
+ let AwsError;
70
+
71
+ export {
72
+ AwsError,
73
+ throwAwsError,
74
+ awsError,
75
+ }
76
+ /* Not a pure module */
@@ -224,3 +224,54 @@ describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
224
224
  }
225
225
  })
226
226
  })
227
+
228
+ // 🚨 The identity keys travel under both spellings through the rename. A shell
229
+ // reads config.json at runtime and CloudFront serves the previous bundle until
230
+ // someone invalidates it, so switching them in one deploy leaves a window where
231
+ // the served bundle and the served config disagree — and `cognitoClientId` is
232
+ // read into an option, so a bundle that cannot find its key does not error, it
233
+ // gets `None` and login silently stops working.
234
+ //
235
+ // These assert the property that removes that window, not the spelling.
236
+ describe("Util_ShellConfig.identityFields", () => {
237
+ let fields = Util_ShellConfig.identityFields(~providerId="eu-west-1_x", ~clientId="7cl13nt")
238
+ let get = key =>
239
+ fields->Array.find(((k, _)) => k == key)->Option.map(((_, v)) => v)
240
+
241
+ testSync("the new spelling is present", () =>
242
+ expect((get("identityProviderId"), get("identityProviderClientId")))->toEqual((
243
+ Some(JSON.Encode.string("eu-west-1_x")),
244
+ Some(JSON.Encode.string("7cl13nt")),
245
+ ))
246
+ )
247
+
248
+ // Not "still" — deliberately. Every shipped shell reads these, and dropping
249
+ // them is a later release gated on the pinned bundle, not on this one.
250
+ testSync("the deprecated spelling is present too", () =>
251
+ expect((get("cognitoUserPoolId"), get("cognitoClientId")))->toEqual((
252
+ Some(JSON.Encode.string("eu-west-1_x")),
253
+ Some(JSON.Encode.string("7cl13nt")),
254
+ ))
255
+ )
256
+
257
+ // The whole point: a bundle reading either name gets the same answer. Two keys
258
+ // carrying different values would be worse than one key, not better.
259
+ testSync("both spellings carry identical values", () =>
260
+ expect((
261
+ get("identityProviderId") == get("cognitoUserPoolId"),
262
+ get("identityProviderClientId") == get("cognitoClientId"),
263
+ ))->toEqual((true, true))
264
+ )
265
+
266
+ // The client id is the load-bearing one — it is what the shell's auth provider
267
+ // branches on. The pool id is currently discarded by the shell, which is
268
+ // exactly why a `cognito*` sweep is dangerous: the safe one gives no warning.
269
+ testSync("nothing else sneaks into the identity field set", () =>
270
+ expect(fields->Array.map(((k, _)) => k)->Array.toSorted(String.compare))->toEqual([
271
+ "cognitoClientId",
272
+ "cognitoUserPoolId",
273
+ "identityProviderClientId",
274
+ "identityProviderId",
275
+ ])
276
+ )
277
+ })
@@ -4,6 +4,8 @@ import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.m
4
4
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
9
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
10
  import * as Util_ShellConfig$ReventlessAws from "../src/util/Util_ShellConfig.res.mjs";
9
11
  import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
@@ -252,6 +254,46 @@ globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () =>
252
254
  });
253
255
  });
254
256
 
257
+ globalThis.describe("Util_ShellConfig.identityFields", () => {
258
+ let fields = Util_ShellConfig$ReventlessAws.identityFields("eu-west-1_x", "7cl13nt");
259
+ let get = key => Stdlib_Option.map(fields.find(param => param[0] === key), param => param[1]);
260
+ globalThis.test("the new spelling is present", () => {
261
+ globalThis.expect([
262
+ get("identityProviderId"),
263
+ get("identityProviderClientId")
264
+ ]).toEqual([
265
+ "eu-west-1_x",
266
+ "7cl13nt"
267
+ ]);
268
+ });
269
+ globalThis.test("the deprecated spelling is present too", () => {
270
+ globalThis.expect([
271
+ get("cognitoUserPoolId"),
272
+ get("cognitoClientId")
273
+ ]).toEqual([
274
+ "eu-west-1_x",
275
+ "7cl13nt"
276
+ ]);
277
+ });
278
+ globalThis.test("both spellings carry identical values", () => {
279
+ globalThis.expect([
280
+ Primitive_object.equal(get("identityProviderId"), get("cognitoUserPoolId")),
281
+ Primitive_object.equal(get("identityProviderClientId"), get("cognitoClientId"))
282
+ ]).toEqual([
283
+ true,
284
+ true
285
+ ]);
286
+ });
287
+ globalThis.test("nothing else sneaks into the identity field set", () => {
288
+ globalThis.expect(fields.map(param => param[0]).toSorted(Primitive_string.compare)).toEqual([
289
+ "cognitoClientId",
290
+ "cognitoUserPoolId",
291
+ "identityProviderClientId",
292
+ "identityProviderId"
293
+ ]);
294
+ });
295
+ });
296
+
255
297
  export {
256
298
  computed,
257
299
  get,