@reventlessdev/reventless-seed-aws 1.0.0-alpha.2

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.
@@ -0,0 +1,316 @@
1
+ // AWS `connect` for the seed harness: resolve a deployed stack, read its
2
+ // endpoints, and log in with Cognito — returning the same provider-agnostic
3
+ // `Seed.connection` a local run produces.
4
+ //
5
+ // Stack discovery reads the sibling Pulumi project's `Pulumi.<stack>.yaml`
6
+ // files (no `pulumi login` needed to list) and calls `pulumi stack output` for
7
+ // the chosen one. Endpoints come from the host-shell `config.json` when the
8
+ // stack publishes a `hostShellUrl` (the hybrid host-shell shape), or from the
9
+ // stack outputs directly otherwise. Login is Cognito USER_PASSWORD_AUTH over a
10
+ // plain `fetch` — the host UI client has no secret, so there is no SigV4 and no
11
+ // AWS SDK dependency.
12
+
13
+ open ReventlessSeed
14
+
15
+ // ── Node bindings ─────────────────────────────────────────────────────────────
16
+
17
+ @val @scope("process") external processEnv: dict<string> = "env"
18
+
19
+ type execOptions = {cwd: string, encoding: string, env?: dict<string>}
20
+ @module("node:child_process")
21
+ external execFileSync: (string, array<string>, execOptions) => string = "execFileSync"
22
+
23
+ type fetchInit = {method: string, headers: dict<string>, body?: string}
24
+ type response
25
+ @val external fetch: (string, fetchInit) => promise<response> = "fetch"
26
+ @send external responseJson: response => promise<JSON.t> = "json"
27
+ @get external responseOk: response => bool = "ok"
28
+ @get external responseStatus: response => int = "status"
29
+
30
+ // ── JSON helpers ──────────────────────────────────────────────────────────────
31
+
32
+ let field = (json: JSON.t, key: string): option<JSON.t> =>
33
+ switch json {
34
+ | Object(obj) => obj->Dict.get(key)
35
+ | _ => None
36
+ }
37
+
38
+ let asString = (json: JSON.t): option<string> =>
39
+ switch json {
40
+ | String(s) => Some(s)
41
+ | _ => None
42
+ }
43
+
44
+ // ── Stack discovery ───────────────────────────────────────────────────────────
45
+
46
+ // Which Pulumi backend the `pulumi` subprocess reads from. When `backend` is
47
+ // set it is passed as `PULUMI_BACKEND_URL` on a *copy* of the environment, so
48
+ // the operator's persistent `pulumi login` is never mutated — each example pins
49
+ // its own backend (core → Pulumi Cloud, a self-hosted app → its S3/… bucket)
50
+ // regardless of which backend the CLI is currently logged into. When absent, the
51
+ // subprocess inherits the ambient login (or an operator-set PULUMI_BACKEND_URL).
52
+ let envForBackend = (backend: option<string>): option<dict<string>> =>
53
+ switch backend {
54
+ | None => None
55
+ | Some(url) =>
56
+ let copy = processEnv->Dict.toArray->Dict.fromArray
57
+ copy->Dict.set("PULUMI_BACKEND_URL", url)
58
+ Some(copy)
59
+ }
60
+
61
+ let pulumi = (~projectDir: string, ~backend: option<string>, args: array<string>): string =>
62
+ execFileSync("pulumi", args, {cwd: projectDir, encoding: "utf8", env: ?envForBackend(backend)})
63
+
64
+ // Stacks that actually exist in the Pulumi backend for this project — the names
65
+ // `pulumi stack output --stack <name>` accepts. A `Pulumi.<stack>.yaml` config
66
+ // file alone is NOT a deployed stack, so discovery must ask the backend (via
67
+ // `pulumi stack ls`), not the filesystem — otherwise it offers phantom stacks
68
+ // that fail with "no stack named …". Empty on any error (pulumi missing / not
69
+ // logged in / no stacks), which `resolveStack` reports as "none found".
70
+ let deployedStacks = (~projectDir: string, ~backend: option<string>): array<string> =>
71
+ try {
72
+ switch JSON.parseOrThrow(pulumi(~projectDir, ~backend, ["stack", "ls", "--json"])) {
73
+ | JSON.Array(items) => items->Array.filterMap(it => it->field("name")->Option.flatMap(asString))
74
+ | _ => []
75
+ }
76
+ } catch {
77
+ | _ => []
78
+ }
79
+
80
+ let stackOutputs = (~projectDir: string, ~backend: option<string>, stack: string): JSON.t => {
81
+ let raw = try pulumi(~projectDir, ~backend, ["stack", "output", "--stack", stack, "--json"]) catch {
82
+ | _ =>
83
+ throw(
84
+ Seed.Failed(
85
+ `pulumi stack output --stack ${stack} failed — is pulumi installed, logged in, ` ++
86
+ "and the stack deployed?",
87
+ ),
88
+ )
89
+ }
90
+ try JSON.parseOrThrow(raw) catch {
91
+ | _ => throw(Seed.Failed(`could not parse pulumi stack output for "${stack}"`))
92
+ }
93
+ }
94
+
95
+ let backendNote = (~backend: option<string>): string =>
96
+ switch backend {
97
+ | Some(url) => ` (backend: ${url})`
98
+ | None => ""
99
+ }
100
+
101
+ let resolveStack = async (~projectDir: string, ~backend: option<string>, ~stack: option<string>): string =>
102
+ switch stack {
103
+ | Some(s) => s
104
+ | None =>
105
+ switch Seed.Prompt.envValue("SEED_STACK") {
106
+ | Some(s) => s
107
+ | None =>
108
+ let stacks = deployedStacks(~projectDir, ~backend)
109
+ if stacks->Array.length == 0 {
110
+ throw(
111
+ Seed.Failed(
112
+ `no deployed Pulumi stacks for this project (\`pulumi stack ls\` is empty in ${projectDir})${backendNote(
113
+ ~backend,
114
+ )}. ` ++
115
+ "A Pulumi.<stack>.yaml config file alone is not a deployed stack — run `pulumi up` " ++
116
+ "first, log in to the backend that holds the stack, or set SEED_STACK to target one.",
117
+ ),
118
+ )
119
+ }
120
+ await Seed.Prompt.select(~title="Stack:", ~options=stacks->Array.map(s => (s, s)))
121
+ }
122
+ }
123
+
124
+ // ── Endpoints ─────────────────────────────────────────────────────────────────
125
+
126
+ let fetchConfig = async (hostShellUrl: string): JSON.t => {
127
+ let base = hostShellUrl->String.replaceRegExp(%re("/\/+$/g"), "")
128
+ let url = `${base}/config.json`
129
+ let res = try await fetch(url, {method: "GET", headers: Dict.make()}) catch {
130
+ | _ => throw(Seed.Failed(`cannot reach ${url}`))
131
+ }
132
+ if !(res->responseOk) {
133
+ throw(Seed.Failed(`GET ${url} → HTTP ${(res->responseStatus)->Int.toString}`))
134
+ }
135
+ await res->responseJson
136
+ }
137
+
138
+ // Prefer an explicit env override, else the discovered value, else fail naming
139
+ // what is missing.
140
+ let resolveField = (~envKey: string, ~fromSource: option<string>, ~human: string): string =>
141
+ switch Seed.Prompt.envValue(envKey) {
142
+ | Some(v) => v
143
+ | None =>
144
+ switch fromSource {
145
+ | Some(v) => v
146
+ | None => throw(Seed.Failed(`deployment is missing ${human} (and ${envKey} is unset)`))
147
+ }
148
+ }
149
+
150
+ // Returns (graphqlEndpoint, uploadEndpoint, cognitoRegion, cognitoClientId).
151
+ // `uploadEndpoint` is "" when the deployment publishes none — the data set's
152
+ // upload phase no-ops on empty.
153
+ let resolveEndpoints = async (~projectDir: string, ~backend: option<string>, ~stack: string): (
154
+ string,
155
+ string,
156
+ string,
157
+ string,
158
+ ) => {
159
+ let outputs = stackOutputs(~projectDir, ~backend, stack)
160
+ switch outputs->field("hostShellUrl")->Option.flatMap(asString) {
161
+ | Some(hostShellUrl) =>
162
+ let cfg = await fetchConfig(hostShellUrl)
163
+ let fromCfg = key => cfg->field(key)->Option.flatMap(asString)
164
+ let endpoint = resolveField(
165
+ ~envKey="REVENTLESS_GRAPHQL_ENDPOINT",
166
+ ~fromSource=fromCfg("apiEndpoint"),
167
+ ~human="apiEndpoint",
168
+ )
169
+ let uploadEndpoint = resolveField(
170
+ ~envKey="REVENTLESS_UPLOAD_ENDPOINT",
171
+ ~fromSource=fromCfg("uploadEndpoint"),
172
+ ~human="uploadEndpoint",
173
+ )
174
+ let region = resolveField(
175
+ ~envKey="AWS_REGION",
176
+ ~fromSource=fromCfg("region"),
177
+ ~human="region",
178
+ )
179
+ let clientId = resolveField(
180
+ ~envKey="COGNITO_CLIENT_ID",
181
+ ~fromSource=fromCfg("cognitoClientId"),
182
+ ~human="cognitoClientId",
183
+ )
184
+ (endpoint, uploadEndpoint, region, clientId)
185
+ | None =>
186
+ let out = key => outputs->field(key)->Option.flatMap(asString)
187
+ let endpoint = switch (
188
+ Seed.Prompt.envValue("REVENTLESS_GRAPHQL_ENDPOINT"),
189
+ out("domainMergedApiEndpoint"),
190
+ out("domainApiEndpoint"),
191
+ ) {
192
+ | (Some(v), _, _) | (_, Some(v), _) | (_, _, Some(v)) => v
193
+ | _ =>
194
+ throw(
195
+ Seed.Failed(
196
+ `stack "${stack}" exports neither domainMergedApiEndpoint nor domainApiEndpoint`,
197
+ ),
198
+ )
199
+ }
200
+ let region = resolveField(
201
+ ~envKey="AWS_REGION",
202
+ ~fromSource=out("cognitoRegion"),
203
+ ~human="cognitoRegion",
204
+ )
205
+ let clientId = resolveField(
206
+ ~envKey="COGNITO_CLIENT_ID",
207
+ ~fromSource=out("cognitoUserPoolClientId"),
208
+ ~human="cognitoUserPoolClientId",
209
+ )
210
+ // Absent → "" → the data set skips its upload phase.
211
+ let uploadEndpoint = Seed.Prompt.envValue("REVENTLESS_UPLOAD_ENDPOINT")->Option.getOr("")
212
+ (endpoint, uploadEndpoint, region, clientId)
213
+ }
214
+ }
215
+
216
+ // ── Cognito login ─────────────────────────────────────────────────────────────
217
+
218
+ let cognito = (~region: string, ~clientId: string) => async (
219
+ ~username: string,
220
+ ~password: string,
221
+ ): string => {
222
+ let body = JSON.stringify(
223
+ JSON.Encode.object(
224
+ Dict.fromArray([
225
+ ("AuthFlow", JSON.Encode.string("USER_PASSWORD_AUTH")),
226
+ ("ClientId", JSON.Encode.string(clientId)),
227
+ (
228
+ "AuthParameters",
229
+ JSON.Encode.object(
230
+ Dict.fromArray([
231
+ ("USERNAME", JSON.Encode.string(username)),
232
+ ("PASSWORD", JSON.Encode.string(password)),
233
+ ]),
234
+ ),
235
+ ),
236
+ ]),
237
+ ),
238
+ )
239
+ let headers = Dict.fromArray([
240
+ ("content-type", "application/x-amz-json-1.1"),
241
+ ("x-amz-target", "AWSCognitoIdentityProviderService.InitiateAuth"),
242
+ ])
243
+ let res = try await fetch(`https://cognito-idp.${region}.amazonaws.com/`, {
244
+ method: "POST",
245
+ headers,
246
+ body,
247
+ }) catch {
248
+ | _ => throw(Seed.Failed(`cannot reach Cognito in ${region}`))
249
+ }
250
+ let json = await res->responseJson
251
+ if !(res->responseOk) {
252
+ let detail =
253
+ json->field("message")->Option.flatMap(asString)->Option.getOr(JSON.stringify(json))
254
+ throw(
255
+ Seed.Failed(
256
+ `Cognito InitiateAuth failed (HTTP ${(res->responseStatus)->Int.toString}): ${detail}`,
257
+ ),
258
+ )
259
+ }
260
+ switch json->field("ChallengeName")->Option.flatMap(asString) {
261
+ | Some(challenge) =>
262
+ throw(
263
+ Seed.Failed(
264
+ `Cognito returned challenge ${challenge} — set a permanent password first ` ++
265
+ "(aws cognito-idp admin-set-user-password … --permanent).",
266
+ ),
267
+ )
268
+ | None => ()
269
+ }
270
+ switch json
271
+ ->field("AuthenticationResult")
272
+ ->Option.flatMap(ar => ar->field("IdToken"))
273
+ ->Option.flatMap(asString) {
274
+ | Some(token) => token
275
+ | None => throw(Seed.Failed(`Cognito response carried no IdToken: ${JSON.stringify(json)}`))
276
+ }
277
+ }
278
+
279
+ // ── connect ───────────────────────────────────────────────────────────────────
280
+
281
+ /**
282
+ * A `connect` thunk for a deployed AWS stack, ready to pass to `Seed.Runner.seed`.
283
+ *
284
+ * `projectDir` is where the Pulumi stacks live (relative to the seed's cwd);
285
+ * `stack` fixes the stack, else `SEED_STACK` or a menu chooses it. Resolves the
286
+ * endpoints, then prompts credentials and mints a Cognito id token.
287
+ *
288
+ * `backend` pins the Pulumi backend the stack lives in — Pulumi Cloud
289
+ * (`https://api.pulumi.com`, needs `PULUMI_ACCESS_TOKEN`) for a CI-deployed
290
+ * example, or a self-hosted store (`s3://…?region=…`, needs AWS creds) for a
291
+ * self-hosted app. It is passed to the pulumi subprocess as `PULUMI_BACKEND_URL`
292
+ * on a copy of the environment, so the operator's persistent `pulumi login` is
293
+ * untouched and each example seeds its own backend regardless of the current
294
+ * login. `SEED_PULUMI_BACKEND` overrides it; omit both to use the ambient login.
295
+ */
296
+ let connect = (~projectDir: string=".", ~stack=?, ~backend=?, ()): (
297
+ unit => promise<Seed.connection>
298
+ ) =>
299
+ async () => {
300
+ let backend = switch Seed.Prompt.envValue("SEED_PULUMI_BACKEND") {
301
+ | Some(url) => Some(url)
302
+ | None => backend
303
+ }
304
+ let stackName = await resolveStack(~projectDir, ~backend, ~stack)
305
+ let (endpoint, uploadEndpoint, region, clientId) = await resolveEndpoints(
306
+ ~projectDir,
307
+ ~backend,
308
+ ~stack=stackName,
309
+ )
310
+ await Seed.Connect.make(
311
+ ~label=stackName,
312
+ ~endpoint,
313
+ ~uploadEndpoint,
314
+ ~login=cognito(~region, ~clientId),
315
+ )
316
+ }
@@ -0,0 +1,305 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Nodechild_process from "node:child_process";
6
+ import * as Seed$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed.res.mjs";
7
+ import * as Seed_Prompt$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Prompt.res.mjs";
8
+ import * as Seed_Connect$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Connect.res.mjs";
9
+
10
+ function field(json, key) {
11
+ if (typeof json === "object" && json !== null && !Array.isArray(json)) {
12
+ return json[key];
13
+ }
14
+ }
15
+
16
+ function asString(json) {
17
+ if (typeof json === "string") {
18
+ return json;
19
+ }
20
+ }
21
+
22
+ function envForBackend(backend) {
23
+ if (backend === undefined) {
24
+ return;
25
+ }
26
+ let copy = Object.fromEntries(Object.entries(process.env));
27
+ copy["PULUMI_BACKEND_URL"] = backend;
28
+ return copy;
29
+ }
30
+
31
+ function pulumi(projectDir, backend, args) {
32
+ return Nodechild_process.execFileSync("pulumi", args, {
33
+ cwd: projectDir,
34
+ encoding: "utf8",
35
+ env: envForBackend(backend)
36
+ });
37
+ }
38
+
39
+ function deployedStacks(projectDir, backend) {
40
+ try {
41
+ let items = JSON.parse(pulumi(projectDir, backend, [
42
+ "stack",
43
+ "ls",
44
+ "--json"
45
+ ]));
46
+ if (Array.isArray(items)) {
47
+ return Stdlib_Array.filterMap(items, it => Stdlib_Option.flatMap(field(it, "name"), asString));
48
+ } else {
49
+ return [];
50
+ }
51
+ } catch (exn) {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ function stackOutputs(projectDir, backend, stack) {
57
+ let raw;
58
+ try {
59
+ raw = pulumi(projectDir, backend, [
60
+ "stack",
61
+ "output",
62
+ "--stack",
63
+ stack,
64
+ "--json"
65
+ ]);
66
+ } catch (exn) {
67
+ throw {
68
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
69
+ _1: `pulumi stack output --stack ` + stack + ` failed — is pulumi installed, logged in, ` + "and the stack deployed?",
70
+ Error: new Error()
71
+ };
72
+ }
73
+ try {
74
+ return JSON.parse(raw);
75
+ } catch (exn$1) {
76
+ throw {
77
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
78
+ _1: `could not parse pulumi stack output for "` + stack + `"`,
79
+ Error: new Error()
80
+ };
81
+ }
82
+ }
83
+
84
+ function backendNote(backend) {
85
+ if (backend !== undefined) {
86
+ return ` (backend: ` + backend + `)`;
87
+ } else {
88
+ return "";
89
+ }
90
+ }
91
+
92
+ async function resolveStack(projectDir, backend, stack) {
93
+ if (stack !== undefined) {
94
+ return stack;
95
+ }
96
+ let s = Seed_Prompt$ReventlessSeed.envValue("SEED_STACK");
97
+ if (s !== undefined) {
98
+ return s;
99
+ }
100
+ let stacks = deployedStacks(projectDir, backend);
101
+ if (stacks.length === 0) {
102
+ throw {
103
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
104
+ _1: `no deployed Pulumi stacks for this project (\`pulumi stack ls\` is empty in ` + projectDir + `)` + backendNote(backend) + `. ` + "A Pulumi.<stack>.yaml config file alone is not a deployed stack — run `pulumi up` first, log in to the backend that holds the stack, or set SEED_STACK to target one.",
105
+ Error: new Error()
106
+ };
107
+ }
108
+ return await Seed_Prompt$ReventlessSeed.select("Stack:", stacks.map(s => [
109
+ s,
110
+ s
111
+ ]), undefined);
112
+ }
113
+
114
+ async function fetchConfig(hostShellUrl) {
115
+ let base = hostShellUrl.replace(/\/+$/g, "");
116
+ let url = base + `/config.json`;
117
+ let res;
118
+ try {
119
+ res = await fetch(url, {
120
+ method: "GET",
121
+ headers: {}
122
+ });
123
+ } catch (exn) {
124
+ throw {
125
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
126
+ _1: `cannot reach ` + url,
127
+ Error: new Error()
128
+ };
129
+ }
130
+ if (!res.ok) {
131
+ throw {
132
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
133
+ _1: `GET ` + url + ` → HTTP ` + res.status.toString(),
134
+ Error: new Error()
135
+ };
136
+ }
137
+ return await res.json();
138
+ }
139
+
140
+ function resolveField(envKey, fromSource, human) {
141
+ let v = Seed_Prompt$ReventlessSeed.envValue(envKey);
142
+ if (v !== undefined) {
143
+ return v;
144
+ }
145
+ if (fromSource !== undefined) {
146
+ return fromSource;
147
+ }
148
+ throw {
149
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
150
+ _1: `deployment is missing ` + human + ` (and ` + envKey + ` is unset)`,
151
+ Error: new Error()
152
+ };
153
+ }
154
+
155
+ async function resolveEndpoints(projectDir, backend, stack) {
156
+ let outputs = stackOutputs(projectDir, backend, stack);
157
+ let hostShellUrl = Stdlib_Option.flatMap(field(outputs, "hostShellUrl"), asString);
158
+ if (hostShellUrl !== undefined) {
159
+ let cfg = await fetchConfig(hostShellUrl);
160
+ let fromCfg = key => Stdlib_Option.flatMap(field(cfg, key), asString);
161
+ let endpoint = resolveField("REVENTLESS_GRAPHQL_ENDPOINT", fromCfg("apiEndpoint"), "apiEndpoint");
162
+ let uploadEndpoint = resolveField("REVENTLESS_UPLOAD_ENDPOINT", fromCfg("uploadEndpoint"), "uploadEndpoint");
163
+ let region = resolveField("AWS_REGION", fromCfg("region"), "region");
164
+ let clientId = resolveField("COGNITO_CLIENT_ID", fromCfg("cognitoClientId"), "cognitoClientId");
165
+ return [
166
+ endpoint,
167
+ uploadEndpoint,
168
+ region,
169
+ clientId
170
+ ];
171
+ }
172
+ let out = key => Stdlib_Option.flatMap(field(outputs, key), asString);
173
+ let match = Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_GRAPHQL_ENDPOINT");
174
+ let match$1 = out("domainMergedApiEndpoint");
175
+ let match$2 = out("domainApiEndpoint");
176
+ let endpoint$1;
177
+ if (match !== undefined) {
178
+ endpoint$1 = match;
179
+ } else if (match$1 !== undefined) {
180
+ endpoint$1 = match$1;
181
+ } else if (match$2 !== undefined) {
182
+ endpoint$1 = match$2;
183
+ } else {
184
+ throw {
185
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
186
+ _1: `stack "` + stack + `" exports neither domainMergedApiEndpoint nor domainApiEndpoint`,
187
+ Error: new Error()
188
+ };
189
+ }
190
+ let region$1 = resolveField("AWS_REGION", out("cognitoRegion"), "cognitoRegion");
191
+ let clientId$1 = resolveField("COGNITO_CLIENT_ID", out("cognitoUserPoolClientId"), "cognitoUserPoolClientId");
192
+ let uploadEndpoint$1 = Stdlib_Option.getOr(Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_UPLOAD_ENDPOINT"), "");
193
+ return [
194
+ endpoint$1,
195
+ uploadEndpoint$1,
196
+ region$1,
197
+ clientId$1
198
+ ];
199
+ }
200
+
201
+ function cognito(region, clientId) {
202
+ return async (username, password) => {
203
+ let body = JSON.stringify(Object.fromEntries([
204
+ [
205
+ "AuthFlow",
206
+ "USER_PASSWORD_AUTH"
207
+ ],
208
+ [
209
+ "ClientId",
210
+ clientId
211
+ ],
212
+ [
213
+ "AuthParameters",
214
+ Object.fromEntries([
215
+ [
216
+ "USERNAME",
217
+ username
218
+ ],
219
+ [
220
+ "PASSWORD",
221
+ password
222
+ ]
223
+ ])
224
+ ]
225
+ ]));
226
+ let headers = Object.fromEntries([
227
+ [
228
+ "content-type",
229
+ "application/x-amz-json-1.1"
230
+ ],
231
+ [
232
+ "x-amz-target",
233
+ "AWSCognitoIdentityProviderService.InitiateAuth"
234
+ ]
235
+ ]);
236
+ let res;
237
+ try {
238
+ res = await fetch(`https://cognito-idp.` + region + `.amazonaws.com/`, {
239
+ method: "POST",
240
+ headers: headers,
241
+ body: body
242
+ });
243
+ } catch (exn) {
244
+ throw {
245
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
246
+ _1: `cannot reach Cognito in ` + region,
247
+ Error: new Error()
248
+ };
249
+ }
250
+ let json = await res.json();
251
+ if (!res.ok) {
252
+ let detail = Stdlib_Option.getOr(Stdlib_Option.flatMap(field(json, "message"), asString), JSON.stringify(json));
253
+ throw {
254
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
255
+ _1: `Cognito InitiateAuth failed (HTTP ` + res.status.toString() + `): ` + detail,
256
+ Error: new Error()
257
+ };
258
+ }
259
+ let challenge = Stdlib_Option.flatMap(field(json, "ChallengeName"), asString);
260
+ if (challenge !== undefined) {
261
+ throw {
262
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
263
+ _1: `Cognito returned challenge ` + challenge + ` — set a permanent password first ` + "(aws cognito-idp admin-set-user-password … --permanent).",
264
+ Error: new Error()
265
+ };
266
+ }
267
+ let token = Stdlib_Option.flatMap(Stdlib_Option.flatMap(field(json, "AuthenticationResult"), ar => field(ar, "IdToken")), asString);
268
+ if (token !== undefined) {
269
+ return token;
270
+ }
271
+ throw {
272
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
273
+ _1: `Cognito response carried no IdToken: ` + JSON.stringify(json),
274
+ Error: new Error()
275
+ };
276
+ };
277
+ }
278
+
279
+ function connect($staropt$star, stack, backend, param) {
280
+ return async () => {
281
+ let projectDir = $staropt$star !== undefined ? $staropt$star : ".";
282
+ let url = Seed_Prompt$ReventlessSeed.envValue("SEED_PULUMI_BACKEND");
283
+ let backend$1 = url !== undefined ? url : backend;
284
+ let stackName = await resolveStack(projectDir, backend$1, stack);
285
+ let match = await resolveEndpoints(projectDir, backend$1, stackName);
286
+ return await Seed_Connect$ReventlessSeed.make(stackName, match[0], match[1], cognito(match[2], match[3]), undefined);
287
+ };
288
+ }
289
+
290
+ export {
291
+ field,
292
+ asString,
293
+ envForBackend,
294
+ pulumi,
295
+ deployedStacks,
296
+ stackOutputs,
297
+ backendNote,
298
+ resolveStack,
299
+ fetchConfig,
300
+ resolveField,
301
+ resolveEndpoints,
302
+ cognito,
303
+ connect,
304
+ }
305
+ /* node:child_process Not a pure module */