@reventlessdev/reventless-aws 3.0.0-alpha.345 → 3.0.0-alpha.347

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 (112) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +19 -12
  3. package/rescript.json +2 -1
  4. package/run-bake-manifest.mjs +3 -0
  5. package/run-deploy-app.mjs +3 -0
  6. package/run-provision-accounts.mjs +3 -0
  7. package/run-provision-admin.mjs +3 -0
  8. package/scripts/BakeManifest.res +395 -0
  9. package/scripts/BakeManifest.res.mjs +487 -0
  10. package/scripts/DeployApp.res +708 -0
  11. package/scripts/DeployApp.res.mjs +1015 -0
  12. package/scripts/DeployManifest.res +84 -0
  13. package/scripts/DeployManifest.res.mjs +112 -0
  14. package/scripts/ProvisionAccounts.res +339 -0
  15. package/scripts/ProvisionAccounts.res.mjs +351 -0
  16. package/scripts/ProvisionAdmin.res +275 -0
  17. package/scripts/ProvisionAdmin.res.mjs +289 -0
  18. package/scripts/ProvisionCognito.res +149 -0
  19. package/scripts/ProvisionCognito.res.mjs +98 -0
  20. package/scripts/ProvisionIdentity.res +76 -11
  21. package/scripts/ProvisionIdentity.res.mjs +72 -7
  22. package/scripts/ProvisionProvider.res +104 -0
  23. package/scripts/ProvisionProvider.res.mjs +123 -0
  24. package/scripts/PulumiCli.res +43 -0
  25. package/scripts/PulumiCli.res.mjs +106 -0
  26. package/src/Platform.res +51 -4
  27. package/src/Platform.res.mjs +32 -14
  28. package/src/Platform_Stack.res +119 -9
  29. package/src/Platform_Stack.res.mjs +42 -4
  30. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
  31. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  32. package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
  33. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
  34. package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
  35. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
  36. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
  37. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
  38. package/src/adapter/Auth/Auth_LoginIdentifier.res +66 -0
  39. package/src/adapter/Auth/Auth_LoginIdentifier.res.mjs +70 -0
  40. package/src/adapter/Auth/Auth_SignUpMode.res +78 -0
  41. package/src/adapter/Auth/Auth_SignUpMode.res.mjs +56 -0
  42. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
  43. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
  44. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
  45. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
  46. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
  47. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
  48. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
  49. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
  50. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  51. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
  52. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +5 -0
  53. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +5 -1
  54. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
  55. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
  56. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
  57. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
  58. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
  59. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
  60. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
  61. package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
  62. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
  63. package/src/adapter/Task/TaskBucket_S3.res +1 -3
  64. package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
  65. package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
  66. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
  67. package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
  68. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
  69. package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
  70. package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
  71. package/src/components/Api/AppSync_Adapter.res +56 -72
  72. package/src/components/Api/AppSync_Adapter.res.mjs +26 -60
  73. package/src/components/Api/AppSync_SdlDecorate.res +82 -1
  74. package/src/components/Api/AppSync_SdlDecorate.res.mjs +45 -0
  75. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
  76. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
  77. package/src/plugin/stack/Plugin_Stack.res +4 -0
  78. package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
  79. package/src/util/Util_DeadLetterQueue.res +1 -5
  80. package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
  81. package/src/util/Util_StoreLayout.res +15 -2
  82. package/src/util/Util_StoreLayout.res.mjs +11 -3
  83. package/tests/AppSync_AdapterTest.res +83 -0
  84. package/tests/AppSync_AdapterTest.res.mjs +69 -0
  85. package/tests/Auth_LoginIdentifierTest.res +51 -0
  86. package/tests/Auth_LoginIdentifierTest.res.mjs +51 -0
  87. package/tests/Auth_SignUpModeTest.res +56 -0
  88. package/tests/Auth_SignUpModeTest.res.mjs +53 -0
  89. package/tests/BakeManifestTest.res +204 -0
  90. package/tests/BakeManifestTest.res.mjs +259 -0
  91. package/tests/DcbColdStartPartitionTest.res +32 -0
  92. package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
  93. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
  94. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
  95. package/tests/DcbUnresolvedPartitionSlice.res +28 -0
  96. package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
  97. package/tests/DeployAppTest.res +90 -0
  98. package/tests/DeployAppTest.res.mjs +133 -0
  99. package/tests/LambdaLayerLookupTest.res +104 -0
  100. package/tests/LambdaLayerLookupTest.res.mjs +117 -0
  101. package/tests/ProvisionAccountsTest.res +168 -0
  102. package/tests/ProvisionAccountsTest.res.mjs +184 -0
  103. package/tests/ProvisionAdminTest.res +55 -0
  104. package/tests/ProvisionAdminTest.res.mjs +66 -0
  105. package/tests/ProvisionIdentityTest.res +89 -3
  106. package/tests/ProvisionIdentityTest.res.mjs +66 -1
  107. package/tests/Util_OwnerScopeEnvTest.res +25 -0
  108. package/tests/Util_OwnerScopeEnvTest.res.mjs +13 -0
  109. package/tests/Util_StoreLayoutTest.res +6 -0
  110. package/tests/Util_StoreLayoutTest.res.mjs +7 -4
  111. package/tests/dcbColdStartPartition.mjs +66 -0
  112. package/tests/setup/layerArn.cjs +5 -0
@@ -0,0 +1,184 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodefs from "node:fs";
4
+ import * as Nodeos from "node:os";
5
+ import * as Nodepath from "node:path";
6
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
7
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
8
+ import * as ProvisionAccounts$ReventlessAws from "../scripts/ProvisionAccounts.res.mjs";
9
+ import * as ProvisionProvider$ReventlessAws from "../scripts/ProvisionProvider.res.mjs";
10
+
11
+ globalThis.describe("ProvisionAccounts.parseArgs", () => {
12
+ globalThis.test("no arguments leaves every choice unanswered", () => {
13
+ globalThis.expect(ProvisionAccounts$ReventlessAws.parseArgs([])).toEqual({
14
+ TAG: "Ok",
15
+ _0: {
16
+ providerId: undefined,
17
+ stack: undefined,
18
+ file: undefined,
19
+ help: false
20
+ }
21
+ });
22
+ });
23
+ globalThis.test("a pool and a manifest parse", () => {
24
+ globalThis.expect(ProvisionAccounts$ReventlessAws.parseArgs([
25
+ "--provider-id",
26
+ "eu-west-1_AbCdEfGhI",
27
+ "--file",
28
+ "cast.yaml"
29
+ ])).toEqual({
30
+ TAG: "Ok",
31
+ _0: {
32
+ providerId: "eu-west-1_AbCdEfGhI",
33
+ stack: undefined,
34
+ file: "cast.yaml",
35
+ help: false
36
+ }
37
+ });
38
+ });
39
+ globalThis.test("--stack picks the deployment to read the pool id from", () => {
40
+ globalThis.expect(ProvisionAccounts$ReventlessAws.parseArgs([
41
+ "--stack",
42
+ "beta"
43
+ ])).toEqual({
44
+ TAG: "Ok",
45
+ _0: {
46
+ providerId: undefined,
47
+ stack: "beta",
48
+ file: undefined,
49
+ help: false
50
+ }
51
+ });
52
+ });
53
+ globalThis.test("an unknown flag refuses rather than being ignored", () => {
54
+ globalThis.expect(ProvisionAccounts$ReventlessAws.parseArgs([
55
+ "--pool",
56
+ "eu-west-1_AbCdEfGhI"
57
+ ])).toEqual({
58
+ TAG: "Error",
59
+ _0: `unknown argument "--pool"`
60
+ });
61
+ });
62
+ globalThis.test("a flag with no value refuses", () => {
63
+ globalThis.expect(ProvisionAccounts$ReventlessAws.parseArgs(["--provider-id"])).toEqual({
64
+ TAG: "Error",
65
+ _0: "--provider-id needs a value"
66
+ });
67
+ });
68
+ globalThis.test("-h asks for the usage", () => {
69
+ globalThis.expect(ProvisionAccounts$ReventlessAws.parseArgs(["-h"])).toEqual({
70
+ TAG: "Ok",
71
+ _0: {
72
+ providerId: undefined,
73
+ stack: undefined,
74
+ file: undefined,
75
+ help: true
76
+ }
77
+ });
78
+ });
79
+ });
80
+
81
+ globalThis.describe("ProvisionProvider.resolve", () => {
82
+ let withoutEnv = fn => {
83
+ let saved = process.env[ProvisionProvider$ReventlessAws.envKey];
84
+ Stdlib_Dict.$$delete(process.env, ProvisionProvider$ReventlessAws.envKey);
85
+ let outcome = fn();
86
+ if (saved !== undefined) {
87
+ process.env[ProvisionProvider$ReventlessAws.envKey] = saved;
88
+ }
89
+ return outcome;
90
+ };
91
+ globalThis.test("an explicit id wins, and says so", () => {
92
+ globalThis.expect(withoutEnv(() => ProvisionProvider$ReventlessAws.resolve("eu-west-1_Explicit", "alpha"))).toEqual({
93
+ TAG: "Ok",
94
+ _0: [
95
+ "eu-west-1_Explicit",
96
+ "Flag"
97
+ ]
98
+ });
99
+ });
100
+ globalThis.test("an explicit id beats the environment", () => {
101
+ process.env[ProvisionProvider$ReventlessAws.envKey] = "eu-west-1_FromEnv";
102
+ let resolved = ProvisionProvider$ReventlessAws.resolve("eu-west-1_Explicit", undefined);
103
+ Stdlib_Dict.$$delete(process.env, ProvisionProvider$ReventlessAws.envKey);
104
+ globalThis.expect(resolved).toEqual({
105
+ TAG: "Ok",
106
+ _0: [
107
+ "eu-west-1_Explicit",
108
+ "Flag"
109
+ ]
110
+ });
111
+ });
112
+ globalThis.test("the environment answers when no flag does", () => {
113
+ process.env[ProvisionProvider$ReventlessAws.envKey] = "eu-west-1_FromEnv";
114
+ let resolved = ProvisionProvider$ReventlessAws.resolve(undefined, undefined);
115
+ Stdlib_Dict.$$delete(process.env, ProvisionProvider$ReventlessAws.envKey);
116
+ globalThis.expect(resolved).toEqual({
117
+ TAG: "Ok",
118
+ _0: [
119
+ "eu-west-1_FromEnv",
120
+ "Environment"
121
+ ]
122
+ });
123
+ });
124
+ globalThis.test("an empty environment variable is not an answer", () => {
125
+ process.env[ProvisionProvider$ReventlessAws.envKey] = " ";
126
+ let resolved = ProvisionProvider$ReventlessAws.resolve(undefined, "no-such-stack-here");
127
+ globalThis.expect((Stdlib_Dict.$$delete(process.env, ProvisionProvider$ReventlessAws.envKey), Stdlib_Result.isError(resolved))).toBe(true);
128
+ });
129
+ globalThis.test("each source describes itself", () => {
130
+ globalThis.expect(ProvisionProvider$ReventlessAws.describe("Flag")).toBe("--provider-id");
131
+ globalThis.expect(ProvisionProvider$ReventlessAws.describe("Environment")).toBe("REVENTLESS_IDENTITY_PROVIDER_ID");
132
+ globalThis.expect(ProvisionProvider$ReventlessAws.describe({
133
+ TAG: "Stack",
134
+ _0: "beta"
135
+ })).toBe("stack beta");
136
+ });
137
+ });
138
+
139
+ globalThis.describe("ProvisionAccounts.looksLikeEmail", () => {
140
+ globalThis.test("an address is one", () => {
141
+ globalThis.expect(ProvisionAccounts$ReventlessAws.looksLikeEmail("me@example.com")).toBe(true);
142
+ });
143
+ globalThis.test("a demo username is not", () => {
144
+ globalThis.expect(ProvisionAccounts$ReventlessAws.looksLikeEmail("shopper")).toBe(false);
145
+ });
146
+ globalThis.test("an @ with no domain dot is not", () => {
147
+ globalThis.expect(ProvisionAccounts$ReventlessAws.looksLikeEmail("admin@localhost")).toBe(false);
148
+ });
149
+ });
150
+
151
+ globalThis.describe("ProvisionAccounts.provision", () => {
152
+ let manifest = `# the cast
153
+ - username: shopper
154
+ password: ""
155
+ groups: [Shopper]
156
+ `;
157
+ globalThis.test("a refused pool leaves the manifest untouched", async () => {
158
+ let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "provision-accounts-"));
159
+ let file = Nodepath.join(dir, "users.yaml");
160
+ Nodefs.writeFileSync(file, manifest, "utf8");
161
+ let refuse = async (param, param$1) => ({
162
+ TAG: "Error",
163
+ _0: "pool signs in on email"
164
+ });
165
+ let outcome = await ProvisionAccounts$ReventlessAws.provision(file, "eu-west-1_Refuses", refuse);
166
+ let after = Nodefs.readFileSync(file, "utf8");
167
+ Nodefs.rmSync(dir, {
168
+ recursive: true,
169
+ force: true
170
+ });
171
+ globalThis.expect(outcome).toEqual({
172
+ TAG: "Error",
173
+ _0: "pool signs in on email"
174
+ });
175
+ globalThis.expect(after).toBe(manifest);
176
+ });
177
+ });
178
+
179
+ let Provision;
180
+
181
+ export {
182
+ Provision,
183
+ }
184
+ /* Not a pure module */
@@ -0,0 +1,55 @@
1
+ open JestGlobals
2
+
3
+ // The bootstrap script's decidable part: which pool and which person the run is
4
+ // about. Reachable at all only because the module defines `main` without calling
5
+ // it — `run-provision-admin.mjs` does that — which is the same reason its sibling
6
+ // is testable.
7
+ //
8
+ // The AWS calls themselves are not covered here; what is covered is everything
9
+ // that can be wrong *before* one is made. The credential this script hands out is
10
+ // no longer its own — the generator moved to `spec` so the manifest's
11
+ // platform-neutral half could reach it, and `Util_PasswordTest` holds the policy
12
+ // it has to satisfy.
13
+
14
+ module Provision = ProvisionAdmin
15
+
16
+ describe("ProvisionAdmin.parseArgs", () => {
17
+ testSync("both arguments parse", () =>
18
+ expect(
19
+ Provision.parseArgs(["--provider-id", "eu-west-1_AbCdEfGhI", "--email", "me@example.com"]),
20
+ )->toEqual(
21
+ Ok({
22
+ Provision.providerId: Some("eu-west-1_AbCdEfGhI"),
23
+ stack: None,
24
+ email: Some("me@example.com"),
25
+ help: false,
26
+ }),
27
+ )
28
+ )
29
+
30
+ testSync("no arguments leaves both unanswered", () =>
31
+ expect(Provision.parseArgs([]))->toEqual(
32
+ Ok({Provision.providerId: None, stack: None, email: None, help: false}),
33
+ )
34
+ )
35
+
36
+ // The reason unknown flags refuse rather than being skipped: a typo'd
37
+ // `--provider-id` would fall through to the environment and make the
38
+ // administrator in a different pool than the operator named — a successful run
39
+ // against the wrong deployment, which is worse than a failed one.
40
+ testSync("an unknown flag refuses rather than being ignored", () =>
41
+ expect(Provision.parseArgs(["--provider", "eu-west-1_AbCdEfGhI"]))->toEqual(
42
+ Error(`unknown argument "--provider"`),
43
+ )
44
+ )
45
+
46
+ testSync("a flag with no value refuses", () =>
47
+ expect(Provision.parseArgs(["--email"]))->toEqual(Error("--email needs a value"))
48
+ )
49
+
50
+ testSync("-h asks for the usage", () =>
51
+ expect(Provision.parseArgs(["-h"]))->toEqual(
52
+ Ok({Provision.providerId: None, stack: None, email: None, help: true}),
53
+ )
54
+ )
55
+ })
@@ -0,0 +1,66 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as ProvisionAdmin$ReventlessAws from "../scripts/ProvisionAdmin.res.mjs";
4
+
5
+ globalThis.describe("ProvisionAdmin.parseArgs", () => {
6
+ globalThis.test("both arguments parse", () => {
7
+ globalThis.expect(ProvisionAdmin$ReventlessAws.parseArgs([
8
+ "--provider-id",
9
+ "eu-west-1_AbCdEfGhI",
10
+ "--email",
11
+ "me@example.com"
12
+ ])).toEqual({
13
+ TAG: "Ok",
14
+ _0: {
15
+ providerId: "eu-west-1_AbCdEfGhI",
16
+ stack: undefined,
17
+ email: "me@example.com",
18
+ help: false
19
+ }
20
+ });
21
+ });
22
+ globalThis.test("no arguments leaves both unanswered", () => {
23
+ globalThis.expect(ProvisionAdmin$ReventlessAws.parseArgs([])).toEqual({
24
+ TAG: "Ok",
25
+ _0: {
26
+ providerId: undefined,
27
+ stack: undefined,
28
+ email: undefined,
29
+ help: false
30
+ }
31
+ });
32
+ });
33
+ globalThis.test("an unknown flag refuses rather than being ignored", () => {
34
+ globalThis.expect(ProvisionAdmin$ReventlessAws.parseArgs([
35
+ "--provider",
36
+ "eu-west-1_AbCdEfGhI"
37
+ ])).toEqual({
38
+ TAG: "Error",
39
+ _0: `unknown argument "--provider"`
40
+ });
41
+ });
42
+ globalThis.test("a flag with no value refuses", () => {
43
+ globalThis.expect(ProvisionAdmin$ReventlessAws.parseArgs(["--email"])).toEqual({
44
+ TAG: "Error",
45
+ _0: "--email needs a value"
46
+ });
47
+ });
48
+ globalThis.test("-h asks for the usage", () => {
49
+ globalThis.expect(ProvisionAdmin$ReventlessAws.parseArgs(["-h"])).toEqual({
50
+ TAG: "Ok",
51
+ _0: {
52
+ providerId: undefined,
53
+ stack: undefined,
54
+ email: undefined,
55
+ help: true
56
+ }
57
+ });
58
+ });
59
+ });
60
+
61
+ let Provision;
62
+
63
+ export {
64
+ Provision,
65
+ }
66
+ /* Not a pure module */
@@ -16,6 +16,8 @@ describe("ProvisionIdentity.parseArgs", () => {
16
16
  Ok({
17
17
  Provision.poolName: Provision.defaultPoolName,
18
18
  providerId: None,
19
+ loginIdentifier: Auth_LoginIdentifier.Email,
20
+ signUpMode: Auth_SignUpMode.AdminOnly,
19
21
  help: false,
20
22
  }),
21
23
  )
@@ -26,6 +28,8 @@ describe("ProvisionIdentity.parseArgs", () => {
26
28
  Ok({
27
29
  Provision.poolName: Provision.defaultPoolName,
28
30
  providerId: Some("eu-west-1_AbCdEfGhI"),
31
+ loginIdentifier: Auth_LoginIdentifier.Email,
32
+ signUpMode: Auth_SignUpMode.AdminOnly,
29
33
  help: false,
30
34
  }),
31
35
  )
@@ -33,13 +37,60 @@ describe("ProvisionIdentity.parseArgs", () => {
33
37
 
34
38
  testSync("--name selects which pool to adopt by name", () =>
35
39
  expect(Provision.parseArgs(["--name", "examples-dev"]))->toEqual(
36
- Ok({Provision.poolName: "examples-dev", providerId: None, help: false}),
40
+ Ok({
41
+ Provision.poolName: "examples-dev",
42
+ providerId: None,
43
+ loginIdentifier: Auth_LoginIdentifier.Email,
44
+ signUpMode: Auth_SignUpMode.AdminOnly,
45
+ help: false,
46
+ }),
37
47
  )
38
48
  )
39
49
 
40
50
  testSync("both together are accepted, and the id wins at resolve time", () =>
41
51
  expect(Provision.parseArgs(["--name", "ignored", "--provider-id", "eu-west-1_x"]))->toEqual(
42
- Ok({Provision.poolName: "ignored", providerId: Some("eu-west-1_x"), help: false}),
52
+ Ok({
53
+ Provision.poolName: "ignored",
54
+ providerId: Some("eu-west-1_x"),
55
+ loginIdentifier: Auth_LoginIdentifier.Email,
56
+ signUpMode: Auth_SignUpMode.AdminOnly,
57
+ help: false,
58
+ }),
59
+ )
60
+ )
61
+
62
+ testSync("--login-identifier picks the sign-in attribute", () =>
63
+ expect(
64
+ Provision.parseArgs(["--login-identifier", "emailOrPhone"])->Result.map(
65
+ a => a.loginIdentifier,
66
+ ),
67
+ )->toEqual(Ok(Auth_LoginIdentifier.EmailOrPhone))
68
+ )
69
+
70
+ // 🚨 The sign-in attribute cannot be changed once the pool exists, so a typo
71
+ // that fell back to the default would create a pool the operator has to
72
+ // rebuild — and re-register every account in — to correct.
73
+ testSync("an unrecognised sign-in attribute is refused, never defaulted", () =>
74
+ expect(Provision.parseArgs(["--login-identifier", "e-mail"])->Result.isError)->toBe(true)
75
+ )
76
+
77
+ testSync("--sign-up-mode opens the pool to self-registration", () =>
78
+ expect(
79
+ Provision.parseArgs(["--sign-up-mode", "selfService"])->Result.map(a => a.signUpMode),
80
+ )->toEqual(Ok(Auth_SignUpMode.SelfService))
81
+ )
82
+
83
+ // This one IS correctable later, unlike the sign-in attribute — it is refused
84
+ // because the mistake is silent, not because it is permanent. Defaulting past
85
+ // a typo leaves a pool that refuses every registration while the command line
86
+ // reads as though it allows them.
87
+ testSync("an unrecognised sign-up mode is refused, never defaulted", () =>
88
+ expect(Provision.parseArgs(["--sign-up-mode", "self-service"])->Result.isError)->toBe(true)
89
+ )
90
+
91
+ testSync("sign-up mode defaults to admin-only, so an existing pool is unchanged", () =>
92
+ expect(Provision.parseArgs([])->Result.map(a => a.signUpMode))->toEqual(
93
+ Ok(Auth_SignUpMode.AdminOnly),
43
94
  )
44
95
  )
45
96
 
@@ -62,6 +113,12 @@ describe("ProvisionIdentity.parseArgs", () => {
62
113
  expect(Provision.parseArgs(["--name"]))->toEqual(Error("--name needs a value"))
63
114
  )
64
115
 
116
+ testSync("--login-identifier with no value is refused too", () =>
117
+ expect(Provision.parseArgs(["--login-identifier"]))->toEqual(
118
+ Error("--login-identifier needs a value"),
119
+ )
120
+ )
121
+
65
122
  testSync("--help and -h both ask for the usage text", () =>
66
123
  expect((
67
124
  Provision.parseArgs(["--help"])->Result.map(a => a.help),
@@ -83,7 +140,11 @@ describe("ProvisionIdentity.parseArgs", () => {
83
140
  // touches it — so its settings are not a detail an operator can revise cheaply
84
141
  // once accounts exist in it.
85
142
  describe("ProvisionIdentity.poolSettings", () => {
86
- let settings = Provision.poolSettings(~poolName="MyIdentity")
143
+ let settings = Provision.poolSettings(
144
+ ~poolName="MyIdentity",
145
+ ~loginIdentifier=Auth_LoginIdentifier.default,
146
+ ~signUpMode=Auth_SignUpMode.default,
147
+ )
87
148
 
88
149
  testSync("carries the name it was asked for", () => expect(settings.poolName)->toBe("MyIdentity"))
89
150
 
@@ -95,6 +156,31 @@ describe("ProvisionIdentity.poolSettings", () => {
95
156
  ))->toEqual((Some(["email"]), Some("OFF"), Some(true)))
96
157
  )
97
158
 
159
+ testSync("a phone pool is created signing in on phone_number", () =>
160
+ expect(
161
+ Provision.poolSettings(
162
+ ~poolName="MyIdentity",
163
+ ~loginIdentifier=Auth_LoginIdentifier.Phone,
164
+ ~signUpMode=Auth_SignUpMode.default,
165
+ ).usernameAttributes,
166
+ )->toEqual(Some(["phone_number"]))
167
+ )
168
+
169
+ // The negation is the part worth pinning: this type asks who may sign up, and
170
+ // Cognito's field asks the opposite question. Reading it backwards would open a
171
+ // pool that was meant to stay closed.
172
+ testSync("selfService clears allowAdminCreateUserOnly, adminOnly sets it", () => {
173
+ let openPool = Provision.poolSettings(
174
+ ~poolName="MyIdentity",
175
+ ~loginIdentifier=Auth_LoginIdentifier.default,
176
+ ~signUpMode=Auth_SignUpMode.SelfService,
177
+ )
178
+ expect((
179
+ openPool.adminCreateUserConfig->Option.flatMap(c => c.allowAdminCreateUserOnly),
180
+ settings.adminCreateUserConfig->Option.flatMap(c => c.allowAdminCreateUserOnly),
181
+ ))->toEqual((Some(false), Some(true)))
182
+ })
183
+
98
184
  testSync("keeps the 12-character password policy", () =>
99
185
  expect(
100
186
  settings.policies
@@ -2,7 +2,9 @@
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
  import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
5
+ import * as Auth_SignUpMode$ReventlessAws from "../src/adapter/Auth/Auth_SignUpMode.res.mjs";
5
6
  import * as ProvisionIdentity$ReventlessAws from "../scripts/ProvisionIdentity.res.mjs";
7
+ import * as Auth_LoginIdentifier$ReventlessAws from "../src/adapter/Auth/Auth_LoginIdentifier.res.mjs";
6
8
 
7
9
  globalThis.describe("ProvisionIdentity.parseArgs", () => {
8
10
  globalThis.test("no arguments means adopt-or-create the default name", () => {
@@ -11,6 +13,8 @@ globalThis.describe("ProvisionIdentity.parseArgs", () => {
11
13
  _0: {
12
14
  poolName: ProvisionIdentity$ReventlessAws.defaultPoolName,
13
15
  providerId: undefined,
16
+ loginIdentifier: "Email",
17
+ signUpMode: "AdminOnly",
14
18
  help: false
15
19
  }
16
20
  });
@@ -24,6 +28,8 @@ globalThis.describe("ProvisionIdentity.parseArgs", () => {
24
28
  _0: {
25
29
  poolName: ProvisionIdentity$ReventlessAws.defaultPoolName,
26
30
  providerId: "eu-west-1_AbCdEfGhI",
31
+ loginIdentifier: "Email",
32
+ signUpMode: "AdminOnly",
27
33
  help: false
28
34
  }
29
35
  });
@@ -37,6 +43,8 @@ globalThis.describe("ProvisionIdentity.parseArgs", () => {
37
43
  _0: {
38
44
  poolName: "examples-dev",
39
45
  providerId: undefined,
46
+ loginIdentifier: "Email",
47
+ signUpMode: "AdminOnly",
40
48
  help: false
41
49
  }
42
50
  });
@@ -52,10 +60,48 @@ globalThis.describe("ProvisionIdentity.parseArgs", () => {
52
60
  _0: {
53
61
  poolName: "ignored",
54
62
  providerId: "eu-west-1_x",
63
+ loginIdentifier: "Email",
64
+ signUpMode: "AdminOnly",
55
65
  help: false
56
66
  }
57
67
  });
58
68
  });
69
+ globalThis.test("--login-identifier picks the sign-in attribute", () => {
70
+ globalThis.expect(Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs([
71
+ "--login-identifier",
72
+ "emailOrPhone"
73
+ ]), a => a.loginIdentifier)).toEqual({
74
+ TAG: "Ok",
75
+ _0: "EmailOrPhone"
76
+ });
77
+ });
78
+ globalThis.test("an unrecognised sign-in attribute is refused, never defaulted", () => {
79
+ globalThis.expect(Stdlib_Result.isError(ProvisionIdentity$ReventlessAws.parseArgs([
80
+ "--login-identifier",
81
+ "e-mail"
82
+ ]))).toBe(true);
83
+ });
84
+ globalThis.test("--sign-up-mode opens the pool to self-registration", () => {
85
+ globalThis.expect(Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs([
86
+ "--sign-up-mode",
87
+ "selfService"
88
+ ]), a => a.signUpMode)).toEqual({
89
+ TAG: "Ok",
90
+ _0: "SelfService"
91
+ });
92
+ });
93
+ globalThis.test("an unrecognised sign-up mode is refused, never defaulted", () => {
94
+ globalThis.expect(Stdlib_Result.isError(ProvisionIdentity$ReventlessAws.parseArgs([
95
+ "--sign-up-mode",
96
+ "self-service"
97
+ ]))).toBe(true);
98
+ });
99
+ globalThis.test("sign-up mode defaults to admin-only, so an existing pool is unchanged", () => {
100
+ globalThis.expect(Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs([]), a => a.signUpMode)).toEqual({
101
+ TAG: "Ok",
102
+ _0: "AdminOnly"
103
+ });
104
+ });
59
105
  globalThis.test("a misspelled flag is refused, not ignored", () => {
60
106
  globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs([
61
107
  "--provider_id",
@@ -77,6 +123,12 @@ globalThis.describe("ProvisionIdentity.parseArgs", () => {
77
123
  _0: "--name needs a value"
78
124
  });
79
125
  });
126
+ globalThis.test("--login-identifier with no value is refused too", () => {
127
+ globalThis.expect(ProvisionIdentity$ReventlessAws.parseArgs(["--login-identifier"])).toEqual({
128
+ TAG: "Error",
129
+ _0: "--login-identifier needs a value"
130
+ });
131
+ });
80
132
  globalThis.test("--help and -h both ask for the usage text", () => {
81
133
  globalThis.expect([
82
134
  Stdlib_Result.map(ProvisionIdentity$ReventlessAws.parseArgs(["--help"]), a => a.help),
@@ -104,7 +156,7 @@ globalThis.describe("ProvisionIdentity.parseArgs", () => {
104
156
  });
105
157
 
106
158
  globalThis.describe("ProvisionIdentity.poolSettings", () => {
107
- let settings = ProvisionIdentity$ReventlessAws.poolSettings("MyIdentity");
159
+ let settings = ProvisionIdentity$ReventlessAws.poolSettings("MyIdentity", Auth_LoginIdentifier$ReventlessAws.default, Auth_SignUpMode$ReventlessAws.default);
108
160
  globalThis.test("carries the name it was asked for", () => {
109
161
  globalThis.expect(settings.PoolName).toBe("MyIdentity");
110
162
  });
@@ -119,6 +171,19 @@ globalThis.describe("ProvisionIdentity.poolSettings", () => {
119
171
  true
120
172
  ]);
121
173
  });
174
+ globalThis.test("a phone pool is created signing in on phone_number", () => {
175
+ globalThis.expect(ProvisionIdentity$ReventlessAws.poolSettings("MyIdentity", "Phone", Auth_SignUpMode$ReventlessAws.default).UsernameAttributes).toEqual(["phone_number"]);
176
+ });
177
+ globalThis.test("selfService clears allowAdminCreateUserOnly, adminOnly sets it", () => {
178
+ let openPool = ProvisionIdentity$ReventlessAws.poolSettings("MyIdentity", Auth_LoginIdentifier$ReventlessAws.default, "SelfService");
179
+ globalThis.expect([
180
+ Stdlib_Option.flatMap(openPool.AdminCreateUserConfig, c => c.AllowAdminCreateUserOnly),
181
+ Stdlib_Option.flatMap(settings.AdminCreateUserConfig, c => c.AllowAdminCreateUserOnly)
182
+ ]).toEqual([
183
+ false,
184
+ true
185
+ ]);
186
+ });
122
187
  globalThis.test("keeps the 12-character password policy", () => {
123
188
  globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(settings.Policies, p => p.PasswordPolicy), p => p.MinimumLength)).toEqual(12);
124
189
  });
@@ -59,4 +59,29 @@ describe("Util_OwnerScopeEnv — the elevated-groups carrier", () => {
59
59
  withElevated([], () => Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables))
60
60
  expect(variables->Dict.get(Util_OwnerScopeEnv.key)->Option.isNone)->toBe(true)
61
61
  })
62
+
63
+ // The whole path the "administrator sees empty screens" trap ran through: the
64
+ // platform entry point defaults the list, and this composes it into a Lambda's
65
+ // environment. Asserted end to end rather than at either half, because both
66
+ // halves were already correct while the trap was open — the pipe was laid and
67
+ // nothing ever put a value in.
68
+ testSync("the platform's administrator default reaches a composed environment", () => {
69
+ Reventless.OwnerScope.clearElevatedGroups()
70
+ NodeProcess.env->Dict.delete(Util_OwnerScopeEnv.key)
71
+ Reventless.OwnerScope.defaultElevatedGroups([Reventless.AdminGroup.name])
72
+
73
+ let variables = Dict.make()
74
+ Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables)
75
+ let encoded = switch variables->Dict.get(Util_OwnerScopeEnv.key) {
76
+ | Some(value) => value->Obj.magic
77
+ | None => ""
78
+ }
79
+ Reventless.OwnerScope.clearElevatedGroups()
80
+
81
+ // Round-tripped, like the first case above: the runtime reads this back with
82
+ // `parseElevatedGroups`, so the encoding is asserted rather than the spelling.
83
+ expect(Reventless.OwnerScope.parseElevatedGroups(encoded))->toEqual([
84
+ Reventless.AdminGroup.name,
85
+ ])
86
+ })
62
87
  })
@@ -1,6 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
3
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
4
6
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
5
7
  import * as Util_OwnerScopeEnv$ReventlessAws from "../src/util/Util_OwnerScopeEnv.res.mjs";
6
8
 
@@ -54,6 +56,17 @@ globalThis.describe("Util_OwnerScopeEnv — the elevated-groups carrier", () =>
54
56
  withElevated([], () => Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables));
55
57
  globalThis.expect(Stdlib_Option.isNone(variables[Util_OwnerScopeEnv$ReventlessAws.key])).toBe(true);
56
58
  });
59
+ globalThis.test("the platform's administrator default reaches a composed environment", () => {
60
+ OwnerScope$Reventless.clearElevatedGroups();
61
+ Stdlib_Dict.$$delete(process.env, Util_OwnerScopeEnv$ReventlessAws.key);
62
+ OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
63
+ let variables = {};
64
+ Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables);
65
+ let value = variables[Util_OwnerScopeEnv$ReventlessAws.key];
66
+ let encoded = value !== undefined ? value : "";
67
+ OwnerScope$Reventless.clearElevatedGroups();
68
+ globalThis.expect(OwnerScope$Reventless.parseElevatedGroups(encoded)).toEqual([AdminGroup$Reventless.name]);
69
+ });
57
70
  });
58
71
 
59
72
  export {
@@ -179,6 +179,12 @@ describe("Util_StoreLayout.protectionFor", () => {
179
179
  testSync("'prepare' is not a PR stack — the prefix is 'pr-', not 'pr'", () =>
180
180
  expect(Util_StoreLayout.protectionFor(~stack="prepare"))->toEqual(Util_StoreLayout.Protected)
181
181
  )
182
+
183
+ testSync("a stack declared disposable is unprotected whatever its name", () =>
184
+ expect(Util_StoreLayout.protectionFor(~stack="dev", ~disposable=true))->toEqual(
185
+ Util_StoreLayout.Unprotected,
186
+ )
187
+ )
182
188
  })
183
189
 
184
190
  describe("Util_StoreLayout.bucketNameFor", () => {
@@ -87,16 +87,19 @@ globalThis.describe("Util_StoreLayout.coverageFor", () => {
87
87
  globalThis.describe("Util_StoreLayout.protectionFor", () => {
88
88
  globalThis.test("alpha shares a bucket and is still protected", () => {
89
89
  globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("alpha", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("SharedBucket");
90
- globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("alpha", undefined)).toEqual("Protected");
90
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("alpha", undefined, undefined)).toEqual("Protected");
91
91
  });
92
92
  globalThis.test("prod is protected", () => {
93
- globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("prod", undefined)).toEqual("Protected");
93
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("prod", undefined, undefined)).toEqual("Protected");
94
94
  });
95
95
  globalThis.test("a PR stack is unprotected so teardown does not leak buckets", () => {
96
- globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("pr-42", undefined)).toEqual("Unprotected");
96
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("pr-42", undefined, undefined)).toEqual("Unprotected");
97
97
  });
98
98
  globalThis.test("'prepare' is not a PR stack — the prefix is 'pr-', not 'pr'", () => {
99
- globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("prepare", undefined)).toEqual("Protected");
99
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("prepare", undefined, undefined)).toEqual("Protected");
100
+ });
101
+ globalThis.test("a stack declared disposable is unprotected whatever its name", () => {
102
+ globalThis.expect(Util_StoreLayout$ReventlessAws.protectionFor("dev", true, undefined)).toEqual("Unprotected");
100
103
  });
101
104
  });
102
105