@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,351 @@
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 AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
5
+ import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
6
+ import * as ProvisionCognito$ReventlessAws from "./ProvisionCognito.res.mjs";
7
+ import * as ProvisionProvider$ReventlessAws from "./ProvisionProvider.res.mjs";
8
+
9
+ function parseArgs(argv) {
10
+ let acc = {
11
+ TAG: "Ok",
12
+ _0: {
13
+ providerId: undefined,
14
+ stack: undefined,
15
+ file: undefined,
16
+ help: false
17
+ }
18
+ };
19
+ let i = 0;
20
+ let count = argv.length;
21
+ while (i < count) {
22
+ let flag = argv[i];
23
+ let value = argv[i + 1 | 0];
24
+ let match = acc;
25
+ let exit = 0;
26
+ if (match.TAG === "Ok") {
27
+ let a = match._0;
28
+ let exit$1 = 0;
29
+ switch (flag) {
30
+ case "--file" :
31
+ if (value !== undefined) {
32
+ acc = {
33
+ TAG: "Ok",
34
+ _0: {
35
+ providerId: a.providerId,
36
+ stack: a.stack,
37
+ file: value,
38
+ help: a.help
39
+ }
40
+ };
41
+ i = i + 2 | 0;
42
+ } else {
43
+ exit = 1;
44
+ }
45
+ break;
46
+ case "--provider-id" :
47
+ if (value !== undefined) {
48
+ acc = {
49
+ TAG: "Ok",
50
+ _0: {
51
+ providerId: value,
52
+ stack: a.stack,
53
+ file: a.file,
54
+ help: a.help
55
+ }
56
+ };
57
+ i = i + 2 | 0;
58
+ } else {
59
+ exit = 1;
60
+ }
61
+ break;
62
+ case "--stack" :
63
+ if (value !== undefined) {
64
+ acc = {
65
+ TAG: "Ok",
66
+ _0: {
67
+ providerId: a.providerId,
68
+ stack: value,
69
+ file: a.file,
70
+ help: a.help
71
+ }
72
+ };
73
+ i = i + 2 | 0;
74
+ } else {
75
+ exit = 1;
76
+ }
77
+ break;
78
+ case "--help" :
79
+ case "-h" :
80
+ exit$1 = 2;
81
+ break;
82
+ default:
83
+ acc = {
84
+ TAG: "Error",
85
+ _0: `unknown argument "` + flag + `"`
86
+ };
87
+ }
88
+ if (exit$1 === 2) {
89
+ acc = {
90
+ TAG: "Ok",
91
+ _0: {
92
+ providerId: a.providerId,
93
+ stack: a.stack,
94
+ file: a.file,
95
+ help: true
96
+ }
97
+ };
98
+ i = i + 1 | 0;
99
+ }
100
+ } else {
101
+ i = count;
102
+ }
103
+ if (exit === 1) {
104
+ acc = {
105
+ TAG: "Error",
106
+ _0: flag + ` needs a value`
107
+ };
108
+ }
109
+ };
110
+ return acc;
111
+ }
112
+
113
+ let usage = `
114
+ Turn a declared cast of accounts into working sign-ins.
115
+
116
+ --provider-id <id> The identity provider to create the accounts in. Usually
117
+ omitted: it falls back to ` + ProvisionProvider$ReventlessAws.envKey + `,
118
+ then to the identityProviderId exported by the selected
119
+ Pulumi stack — which every deployment exports, whether it
120
+ created the pool or was handed one.
121
+ --stack <name> Read that output from this stack instead of the selected
122
+ one. The run always names the stack it used.
123
+ --file <path> The manifest. Defaults to .reventless/users.yaml relative
124
+ to the working directory, which is where both platforms
125
+ keep it.
126
+
127
+ For every entry: ensures the groups it names, ensures the account, sets a
128
+ permanent password, and applies the memberships. Generated passwords and the ids
129
+ the pool mints are written back into the manifest, so the seed client can read
130
+ them without anyone retyping a UUID.
131
+
132
+ A password already in the manifest is never replaced. Region and credentials come
133
+ from the environment, as for any AWS SDK call.
134
+ `;
135
+
136
+ function looksLikeEmail(username) {
137
+ if (username.includes("@")) {
138
+ return username.includes(".");
139
+ } else {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ async function checkPoolAcceptsUsernames(providerId, usernames) {
145
+ let e = await ProvisionCognito$ReventlessAws.usernameAttributes(providerId);
146
+ if (e.TAG !== "Ok") {
147
+ return e;
148
+ }
149
+ let attributes = e._0;
150
+ let signsInOnEmail = attributes.includes("email");
151
+ let unusable = signsInOnEmail ? usernames.filter(u => !looksLikeEmail(u)) : [];
152
+ if (unusable.length === 0) {
153
+ console.log(`pool signs in on ` + (
154
+ attributes.length === 0 ? "username" : attributes.join(", ")
155
+ ));
156
+ return {
157
+ TAG: "Ok",
158
+ _0: undefined
159
+ };
160
+ } else {
161
+ return {
162
+ TAG: "Error",
163
+ _0: `pool "` + providerId + `" signs in on ` + attributes.join(", ") + `, so ` + unusable.join(", ") + ` could never authenticate. The sign-in attribute is fixed at pool creation and no update can change it — either name these accounts by address in the manifest, or provision them in a pool that signs in on a username.`
164
+ };
165
+ }
166
+ }
167
+
168
+ function describe(outcome) {
169
+ if (outcome === "Created") {
170
+ return "created";
171
+ } else {
172
+ return "already present";
173
+ }
174
+ }
175
+
176
+ async function ensureGroups(providerId, entries) {
177
+ let named = [];
178
+ entries.forEach(entry => {
179
+ entry.groups.forEach(group => {
180
+ if (!named.includes(group)) {
181
+ named.push(group);
182
+ return;
183
+ }
184
+ });
185
+ });
186
+ for (let index = 0, index_finish = named.length; index < index_finish; ++index) {
187
+ let group = named[index];
188
+ let outcome = await ProvisionCognito$ReventlessAws.ensureGroup(providerId, group, "Declared in a Reventless accounts manifest");
189
+ console.log(`group ` + group + ` (` + describe(outcome) + `)`);
190
+ }
191
+ }
192
+
193
+ async function applyEntry(providerId, entry) {
194
+ let attributes = looksLikeEmail(entry.username) ? [
195
+ {
196
+ Name: "email",
197
+ Value: entry.username
198
+ },
199
+ {
200
+ Name: "email_verified",
201
+ Value: "true"
202
+ }
203
+ ] : [];
204
+ let outcome = await ProvisionCognito$ReventlessAws.ensureUser(providerId, entry.username, attributes);
205
+ console.log(`user ` + entry.username + ` (` + describe(outcome) + `)`);
206
+ await ProvisionCognito$ReventlessAws.setPassword(providerId, entry.username, entry.password);
207
+ for (let index = 0, index_finish = entry.groups.length; index < index_finish; ++index) {
208
+ let group = entry.groups[index];
209
+ await ProvisionCognito$ReventlessAws.addToGroup(providerId, entry.username, group);
210
+ console.log(`member ` + entry.username + ` in ` + group);
211
+ }
212
+ return await ProvisionCognito$ReventlessAws.subOf(providerId, entry.username);
213
+ }
214
+
215
+ function manifestNote(file) {
216
+ return `
217
+ 🚨 Generated passwords were written into ` + file + `. They are not printed here
218
+ because that file is where they are read from — by you and by \`pnpm run seed\`.
219
+ It is gitignored on every platform; keep it that way. These are bootstrap
220
+ credentials for a deployment, not secrets to reuse anywhere real.`;
221
+ }
222
+
223
+ async function provision(file, providerId, checkPoolOpt) {
224
+ let checkPool = checkPoolOpt !== undefined ? checkPoolOpt : checkPoolAcceptsUsernames;
225
+ let message = AccountsManifest$Reventless.parseFile(file);
226
+ if (message.TAG !== "Ok") {
227
+ return {
228
+ TAG: "Error",
229
+ _0: file + `: ` + message._0
230
+ };
231
+ }
232
+ let declared = message._0;
233
+ if (declared.length === 0) {
234
+ return {
235
+ TAG: "Error",
236
+ _0: file + ` declares no accounts`
237
+ };
238
+ }
239
+ let e = await checkPool(providerId, declared.map(e => e.username));
240
+ if (e.TAG !== "Ok") {
241
+ return e;
242
+ }
243
+ let message$1 = AccountsManifest$Reventless.prepare(file);
244
+ if (message$1.TAG !== "Ok") {
245
+ return {
246
+ TAG: "Error",
247
+ _0: file + `: ` + message$1._0
248
+ };
249
+ }
250
+ let prepared = message$1._0;
251
+ let generated = prepared.filter(p => p.passwordGenerated).length;
252
+ console.log(`manifest ` + file + ` (` + prepared.length.toString() + ` accounts, ` + generated.toString() + ` password(s) generated)`);
253
+ let entries = prepared.map(p => p.entry);
254
+ await ensureGroups(providerId, entries);
255
+ let fills = [];
256
+ for (let index = 0, index_finish = entries.length; index < index_finish; ++index) {
257
+ let entry = entries[index];
258
+ let userId = await applyEntry(providerId, entry);
259
+ fills.push({
260
+ index: index,
261
+ password: undefined,
262
+ userId: userId
263
+ });
264
+ }
265
+ let message$2 = AccountsManifest$Reventless.fillFile(file, fills);
266
+ if (message$2.TAG !== "Ok") {
267
+ return {
268
+ TAG: "Error",
269
+ _0: file + `: ` + message$2._0
270
+ };
271
+ }
272
+ let corrected = message$2._0.filter(r => r.userIdWritten).length;
273
+ console.log(`manifest ` + file + ` (` + corrected.toString() + ` id(s) written back)` + manifestNote(file));
274
+ return {
275
+ TAG: "Ok",
276
+ _0: undefined
277
+ };
278
+ }
279
+
280
+ async function run() {
281
+ let e = parseArgs(process.argv.slice(2, process.argv.length));
282
+ if (e.TAG !== "Ok") {
283
+ return e;
284
+ }
285
+ let args = e._0;
286
+ if (args.help) {
287
+ console.log(usage);
288
+ return {
289
+ TAG: "Ok",
290
+ _0: undefined
291
+ };
292
+ }
293
+ let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
294
+ if (e$1.TAG !== "Ok") {
295
+ return e$1;
296
+ }
297
+ let located = e$1._0;
298
+ let file = AccountsManifest$Reventless.pathOf(located);
299
+ if (located.TAG !== "Declared") {
300
+ console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
301
+ }
302
+ let message = ProvisionProvider$ReventlessAws.resolve(args.providerId, args.stack);
303
+ if (message.TAG !== "Ok") {
304
+ return {
305
+ TAG: "Error",
306
+ _0: message._0 + `. To fill in the manifest without creating anything, run prepare-accounts`
307
+ };
308
+ }
309
+ let match = message._0;
310
+ let providerId = match[0];
311
+ console.log(`provider ` + providerId + ` (from ` + ProvisionProvider$ReventlessAws.describe(match[1]) + `)`);
312
+ return await provision(file, providerId, undefined);
313
+ }
314
+
315
+ async function main() {
316
+ let message;
317
+ try {
318
+ message = await run();
319
+ } catch (raw_exn) {
320
+ let exn = Primitive_exceptions.internalToException(raw_exn);
321
+ console.error(`provision-accounts: ` + Util_AwsError$ReventlessAws.describe(exn));
322
+ process.exit(1);
323
+ return;
324
+ }
325
+ if (message.TAG === "Ok") {
326
+ return;
327
+ }
328
+ console.error(`provision-accounts: ` + message._0);
329
+ process.exit(1);
330
+ }
331
+
332
+ let Cognito;
333
+
334
+ let Manifest;
335
+
336
+ export {
337
+ Cognito,
338
+ Manifest,
339
+ parseArgs,
340
+ usage,
341
+ looksLikeEmail,
342
+ checkPoolAcceptsUsernames,
343
+ describe,
344
+ ensureGroups,
345
+ applyEntry,
346
+ manifestNote,
347
+ provision,
348
+ run,
349
+ main,
350
+ }
351
+ /* AccountsManifest-Reventless Not a pure module */
@@ -0,0 +1,275 @@
1
+ /***
2
+ Make the first administrator of a deployment.
3
+
4
+ A stack creates a user pool, an app client and — in auto mode — the
5
+ administrator group. It creates no *accounts*, and it should not: a pool full of
6
+ people is not something a `pulumi destroy` should be able to empty, and Cognito
7
+ exports no password material, so a replaced account is a lost one. Something has
8
+ to make the first one, once, outside every stack. That is this.
9
+
10
+ Until it existed the answer was the AWS console, and nothing said so. A deploy
11
+ succeeded, the app loaded, sign-in failed, and no page in the repo covered what to
12
+ do next — the most expensive kind of first impression, met by whoever has the
13
+ least context: someone evaluating the framework on their first cloud deploy.
14
+
15
+ ```
16
+ pnpm exec provision-admin --provider-id eu-west-1_AbCdEfGhI --email me@example.com
17
+ ```
18
+
19
+ Works the same on a pool the stack owns and one it does not, which is why it is
20
+ its own bin rather than a flag on `provision-identity`. That script provisions the
21
+ *provider* — a pool and the active-role store derived from its id — and both are
22
+ BYO concerns; run against an auto-mode pool it would create a second store at a
23
+ derived name that no stack ever reads. The first administrator is needed in both
24
+ modes and is infrastructure in neither, so it is asked for separately.
25
+
26
+ 🚨 **It provisions nothing a stack owns.** No pool, no store, no trigger. The
27
+ group is the one exception and only because it may be missing: on a supplied pool
28
+ no stack declares it — see [Platform_Stack] for why a pool-level fact is not a
29
+ borrowed pool's stack's to declare.
30
+
31
+ Re-running is safe, and the first thing anyone does with a provisioning script is
32
+ run it twice: an existing group and an existing user are both reported rather than
33
+ refused. The password is re-minted on every run, which is the one thing that does
34
+ change — see [passwordNote].
35
+ */
36
+
37
+ open AwsSdk
38
+
39
+ module Cognito = CognitoIdentityServiceProvider
40
+
41
+ // ── Arguments ────────────────────────────────────────────────────────────────
42
+
43
+ type args = {
44
+ providerId: option<string>,
45
+ stack: option<string>,
46
+ email: option<string>,
47
+ help: bool,
48
+ }
49
+
50
+ /** Parsed rather than positional, and unknown flags are an error — the reason
51
+ [ProvisionIdentity.parseArgs] gives, arriving somewhere worse: a typo'd
52
+ `--email` that parsed as "absent" would be caught by the required-argument check
53
+ below, but a typo'd `--provider-id` would fall through to the environment and
54
+ create the administrator in a *different pool* than the operator named. */
55
+ let parseArgs = (argv: array<string>): result<args, string> => {
56
+ let acc = ref(Ok({providerId: None, stack: None, email: None, help: false}))
57
+ let i = ref(0)
58
+ let count = argv->Array.length
59
+ while i.contents < count {
60
+ let flag = argv->Array.getUnsafe(i.contents)
61
+ let value = argv->Array.get(i.contents + 1)
62
+ switch (acc.contents, flag, value) {
63
+ | (Error(_), _, _) => i := count
64
+ | (Ok(a), "--provider-id", Some(v)) =>
65
+ acc := Ok({...a, providerId: Some(v)})
66
+ i := i.contents + 2
67
+ | (Ok(a), "--stack", Some(v)) =>
68
+ acc := Ok({...a, stack: Some(v)})
69
+ i := i.contents + 2
70
+ | (Ok(a), "--email", Some(v)) =>
71
+ acc := Ok({...a, email: Some(v)})
72
+ i := i.contents + 2
73
+ | (Ok(a), "--help", _) | (Ok(a), "-h", _) =>
74
+ acc := Ok({...a, help: true})
75
+ i := i.contents + 1
76
+ | (Ok(_), "--provider-id", None) | (Ok(_), "--email", None) | (Ok(_), "--stack", None) =>
77
+ acc := Error(`${flag} needs a value`)
78
+ | (Ok(_), unknown, _) => acc := Error(`unknown argument "${unknown}"`)
79
+ }
80
+ }
81
+ acc.contents
82
+ }
83
+
84
+ let usage = `
85
+ Make the first administrator of a Reventless deployment.
86
+
87
+ --provider-id <id> The identity provider to create the account in. Usually
88
+ omitted: it falls back to ${ProvisionProvider.envKey},
89
+ then to the identityProviderId exported by the selected
90
+ Pulumi stack — which every deployment exports, whether it
91
+ created the pool or was handed one.
92
+ --stack <name> Read that output from this stack instead of the selected
93
+ one. The run always names the stack it used.
94
+ --email <address> The address they sign in with.
95
+
96
+ Creates the "${Reventless.AdminGroup.name}" group if it is missing, the account if
97
+ it is missing, a working password, and the membership between them. Provisions
98
+ nothing that a platform stack owns. Region and credentials come from the
99
+ environment, as for any AWS SDK call.
100
+ `
101
+
102
+ // ── The pool ─────────────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ Refuse a pool this account could never sign in to.
106
+
107
+ The sign-in attribute is fixed at pool creation and no update can change it — see
108
+ [Auth_LoginIdentifier] — so an email address handed to a phone-only pool produces
109
+ a correctly-created account that simply cannot authenticate, and the symptom
110
+ arrives at a login screen far from the run that caused it. Described rather than
111
+ assumed for the same reason `provision-identity` describes a supplied pool.
112
+
113
+ No `UsernameAttributes` at all means the pool signs in on a plain username, which
114
+ takes an email-shaped one happily.
115
+ */
116
+ let checkPoolAcceptsEmail = async (~providerId: string): result<unit, string> =>
117
+ switch await ProvisionCognito.usernameAttributes(~providerId) {
118
+ | Error(_) as e => e
119
+ | Ok(attributes) =>
120
+ if attributes->Array.length == 0 || attributes->Array.includes("email") {
121
+ Console.log(`pool ${providerId}`)
122
+ Ok()
123
+ } else {
124
+ Error(
125
+ `pool "${providerId}" signs in on ${attributes->Array.join(
126
+ ", ",
127
+ )}, not email, so an account created from --email could never authenticate. The sign-in attribute is fixed at pool creation and no update can change it.`,
128
+ )
129
+ }
130
+ }
131
+
132
+ // ── The account ──────────────────────────────────────────────────────────────
133
+
134
+ let describe = (outcome: ProvisionCognito.outcome) =>
135
+ switch outcome {
136
+ | Created => "created"
137
+ | AlreadyPresent => "already present"
138
+ }
139
+
140
+ /** The group, where a stack has not already declared it. See
141
+ [ProvisionCognito.ensureGroup] for why a supplied pool needs one made here. */
142
+ let ensureGroup = async (~providerId: string, ~group: string): unit => {
143
+ let outcome = await ProvisionCognito.ensureGroup(
144
+ ~providerId,
145
+ ~group,
146
+ ~description="Reventless administrators",
147
+ )
148
+ Console.log(`group ${group} (${outcome->describe})`)
149
+ }
150
+
151
+ /**
152
+ The account, where it is not already there.
153
+
154
+ `email_verified` is stamped true because nothing here can complete a verification
155
+ round-trip, and an unverified address leaves the account able to sign in but
156
+ unable to recover a password — a bootstrap that works once and strands its owner.
157
+ */
158
+ let ensureUser = async (~providerId: string, ~email: string): unit => {
159
+ let attributes: array<Cognito.AdminCreateUserCommand.attributeType> = [
160
+ {name: "email", value: email},
161
+ {name: "email_verified", value: "true"},
162
+ ]
163
+ let outcome = await ProvisionCognito.ensureUser(~providerId, ~username=email, ~attributes)
164
+ Console.log(`user ${email} (${outcome->describe})`)
165
+ }
166
+
167
+ /** A password the account can actually be used with — see
168
+ [ProvisionCognito.setPassword] for why it is permanent rather than temporary.
169
+
170
+ Unconditional rather than skipped for an account that already exists, which is
171
+ what makes a second run useful instead of merely harmless: the operator who runs
172
+ this again is usually the one who has lost the password. */
173
+ let setPassword = async (~providerId: string, ~email: string, ~password: string): unit => {
174
+ await ProvisionCognito.setPassword(~providerId, ~username=email, ~password)
175
+ Console.log(`password (set, permanent)`)
176
+ }
177
+
178
+ let addToGroup = async (~providerId: string, ~email: string, ~group: string): unit => {
179
+ await ProvisionCognito.addToGroup(~providerId, ~username=email, ~group)
180
+ Console.log(`member ${email} in ${group}`)
181
+ }
182
+
183
+ // ── Main ─────────────────────────────────────────────────────────────────────
184
+
185
+ /**
186
+ 🚨 **Printed, and this is a deliberate trade rather than an oversight.**
187
+
188
+ Cognito keeps no recoverable copy, so a generated password exists in this output
189
+ or nowhere. Printing it is friendliest for the case this script is for — a
190
+ developer at a terminal on their first deploy — and the cost is that it lands in
191
+ scrollback, and in a build log if anyone runs this unattended. Taking the password
192
+ as a flag instead would trade the log for shell history, which is not obviously
193
+ better and is worse for the target case.
194
+
195
+ So: generated and printed, and said plainly to be a bootstrap credential. A
196
+ deployment that runs this in a pipeline should be using its own provisioning
197
+ instead. Revisit if that stops being true.
198
+ */
199
+ let passwordNote = `
200
+ 🚨 This is a bootstrap credential and it is printed above because that is the only
201
+ place it exists — Cognito keeps no recoverable copy. It is now in this
202
+ terminal's scrollback. Sign in, change it, and do not paste it where logs are
203
+ kept. Running this again mints a new one.`
204
+
205
+ let signInDetails = (~providerId: string, ~email: string, ~password: string, ~group: string) =>
206
+ `
207
+ The first administrator is ready.
208
+
209
+ provider ${providerId}
210
+ sign in as ${email}
211
+ password ${password}
212
+ group ${group}
213
+ ${passwordNote}
214
+
215
+ This script bootstraps the first account only, which is the one that cannot be
216
+ made through a signed-in session because there is no signed-in session yet. The
217
+ rest of the cast is a list rather than a command: declare it in
218
+ .reventless/users.yaml and run \`pnpm exec provision-accounts\`.`
219
+
220
+ let run = async (): result<unit, string> =>
221
+ // argv[0] is node, argv[1] this script.
222
+ switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
223
+ | Error(_) as e => e
224
+ | Ok(args) if args.help =>
225
+ Console.log(usage)
226
+ Ok()
227
+ | Ok(args) =>
228
+ // The address is checked before the provider is resolved: it costs nothing,
229
+ // while resolving may shell out to Pulumi, and a run missing both arguments
230
+ // should say the cheap thing rather than fail on a stack lookup it never
231
+ // needed.
232
+ switch args.email {
233
+ | None => Error("--email is required — it is the address the administrator signs in with")
234
+ | Some(email) =>
235
+ switch ProvisionProvider.resolve(~given=args.providerId, ~stack=args.stack) {
236
+ | Error(_) as e => e
237
+ | Ok((providerId, source)) =>
238
+ Console.log(`provider ${providerId} (from ${source->ProvisionProvider.describe})`)
239
+ switch await checkPoolAcceptsEmail(~providerId) {
240
+ | Error(_) as e => e
241
+ | Ok() =>
242
+ let group = Reventless.AdminGroup.name
243
+ let password = Reventless.Util_Password.generate()
244
+ await ensureGroup(~providerId, ~group)
245
+ await ensureUser(~providerId, ~email)
246
+ await setPassword(~providerId, ~email, ~password)
247
+ await addToGroup(~providerId, ~email, ~group)
248
+ Console.log(signInDetails(~providerId, ~email, ~password, ~group))
249
+ Ok()
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ 🚨 **Catches thrown exceptions, not only `Error` results** — the reason
257
+ [ProvisionIdentity.main] gives. Without it an SDK failure escapes an async
258
+ function nothing awaits and Node reports `UnhandledPromiseRejection ...
259
+ "#<Object>"`, naming neither the call that failed nor why.
260
+ */
261
+ let main = async () =>
262
+ switch await run() {
263
+ | Ok() => ()
264
+ | Error(message) =>
265
+ Console.error(`provision-admin: ${message}`)
266
+ NodeProcess.exit(1)
267
+ | exception exn =>
268
+ Console.error(`provision-admin: ${Util_AwsError.describe(exn)}`)
269
+ NodeProcess.exit(1)
270
+ }
271
+
272
+ // 🚨 **No top-level call.** `../run-provision-admin.mjs` invokes [main]; this
273
+ // module only defines it. A module that ran itself on import could not be
274
+ // imported by a test — which is how a guard that could never match once shipped
275
+ // in the sibling script.