@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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +202 -0
- package/lib/bs/.compiler.log +2 -0
- package/lib/bs/build.ninja +0 -0
- package/lib/bs/compiler-info.json +8 -0
- package/lib/bs/src/ReventlessSeedAws.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws.res +316 -0
- package/lib/bs/src/ReventlessSeedAws.res.mjs +305 -0
- package/lib/ocaml/.compiler.log +2 -0
- package/lib/ocaml/ReventlessSeedAws.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws.res +316 -0
- package/package.json +29 -0
- package/rescript.json +21 -0
- package/src/ReventlessSeedAws.res +316 -0
- package/src/ReventlessSeedAws.res.mjs +305 -0
|
@@ -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 */
|