@reventlessdev/reventless-seed-aws 1.0.0-alpha.3 → 1.0.0-alpha.30

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 (111) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/.sourcedirs.json +1 -0
  4. package/lib/bs/compiler-info.json +2 -2
  5. package/lib/bs/src/ReventlessSeedAws.ast +0 -0
  6. package/lib/bs/src/ReventlessSeedAws.cmi +0 -0
  7. package/lib/bs/src/ReventlessSeedAws.cmj +0 -0
  8. package/lib/bs/src/ReventlessSeedAws.cmt +0 -0
  9. package/lib/bs/src/ReventlessSeedAws.res +198 -134
  10. package/lib/bs/src/ReventlessSeedAws.res.mjs +57 -31
  11. package/lib/bs/src/ReventlessSeedAws_Quiesce.ast +0 -0
  12. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmi +0 -0
  13. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmj +0 -0
  14. package/lib/bs/src/ReventlessSeedAws_Quiesce.cmt +0 -0
  15. package/lib/bs/src/ReventlessSeedAws_Quiesce.res +307 -0
  16. package/lib/bs/src/ReventlessSeedAws_Quiesce.res.mjs +217 -0
  17. package/lib/bs/src/ReventlessSeedAws_Reconcile.ast +0 -0
  18. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmi +0 -0
  19. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmj +0 -0
  20. package/lib/bs/src/ReventlessSeedAws_Reconcile.cmt +0 -0
  21. package/lib/bs/src/ReventlessSeedAws_Reconcile.res +391 -0
  22. package/lib/bs/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
  23. package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
  24. package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
  25. package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
  26. package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
  27. package/lib/bs/src/ReventlessSeedAws_Reset.res +1103 -0
  28. package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +991 -0
  29. package/lib/bs/tests/EndpointResolutionTest.ast +0 -0
  30. package/lib/bs/tests/EndpointResolutionTest.cmi +0 -0
  31. package/lib/bs/tests/EndpointResolutionTest.cmj +0 -0
  32. package/lib/bs/tests/EndpointResolutionTest.cmt +0 -0
  33. package/lib/bs/tests/EndpointResolutionTest.res +83 -0
  34. package/lib/bs/tests/EndpointResolutionTest.res.mjs +115 -0
  35. package/lib/bs/tests/ObjectStoreReconcileTest.ast +0 -0
  36. package/lib/bs/tests/ObjectStoreReconcileTest.cmi +0 -0
  37. package/lib/bs/tests/ObjectStoreReconcileTest.cmj +0 -0
  38. package/lib/bs/tests/ObjectStoreReconcileTest.cmt +0 -0
  39. package/lib/bs/tests/ObjectStoreReconcileTest.res +83 -0
  40. package/lib/bs/tests/ObjectStoreReconcileTest.res.mjs +102 -0
  41. package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
  42. package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
  43. package/lib/bs/tests/ObjectStoreResetTest.cmj +0 -0
  44. package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
  45. package/lib/bs/tests/ObjectStoreResetTest.res +191 -0
  46. package/lib/bs/tests/ObjectStoreResetTest.res.mjs +211 -0
  47. package/lib/bs/tests/QuiesceResetTest.ast +0 -0
  48. package/lib/bs/tests/QuiesceResetTest.cmi +0 -0
  49. package/lib/bs/tests/QuiesceResetTest.cmj +0 -0
  50. package/lib/bs/tests/QuiesceResetTest.cmt +0 -0
  51. package/lib/bs/tests/QuiesceResetTest.res +169 -0
  52. package/lib/bs/tests/QuiesceResetTest.res.mjs +157 -0
  53. package/lib/ocaml/.compiler.log +2 -2
  54. package/lib/ocaml/EndpointResolutionTest.ast +0 -0
  55. package/lib/ocaml/EndpointResolutionTest.cmi +0 -0
  56. package/lib/ocaml/EndpointResolutionTest.cmj +0 -0
  57. package/lib/ocaml/EndpointResolutionTest.cmt +0 -0
  58. package/lib/ocaml/EndpointResolutionTest.res +83 -0
  59. package/lib/ocaml/ObjectStoreReconcileTest.ast +0 -0
  60. package/lib/ocaml/ObjectStoreReconcileTest.cmi +0 -0
  61. package/lib/ocaml/ObjectStoreReconcileTest.cmj +0 -0
  62. package/lib/ocaml/ObjectStoreReconcileTest.cmt +0 -0
  63. package/lib/ocaml/ObjectStoreReconcileTest.res +83 -0
  64. package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
  65. package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
  66. package/lib/ocaml/ObjectStoreResetTest.cmj +0 -0
  67. package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
  68. package/lib/ocaml/ObjectStoreResetTest.res +191 -0
  69. package/lib/ocaml/QuiesceResetTest.ast +0 -0
  70. package/lib/ocaml/QuiesceResetTest.cmi +0 -0
  71. package/lib/ocaml/QuiesceResetTest.cmj +0 -0
  72. package/lib/ocaml/QuiesceResetTest.cmt +0 -0
  73. package/lib/ocaml/QuiesceResetTest.res +169 -0
  74. package/lib/ocaml/ReventlessSeedAws.ast +0 -0
  75. package/lib/ocaml/ReventlessSeedAws.cmi +0 -0
  76. package/lib/ocaml/ReventlessSeedAws.cmj +0 -0
  77. package/lib/ocaml/ReventlessSeedAws.cmt +0 -0
  78. package/lib/ocaml/ReventlessSeedAws.res +198 -134
  79. package/lib/ocaml/ReventlessSeedAws_Quiesce.ast +0 -0
  80. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmi +0 -0
  81. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmj +0 -0
  82. package/lib/ocaml/ReventlessSeedAws_Quiesce.cmt +0 -0
  83. package/lib/ocaml/ReventlessSeedAws_Quiesce.res +307 -0
  84. package/lib/ocaml/ReventlessSeedAws_Reconcile.ast +0 -0
  85. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmi +0 -0
  86. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmj +0 -0
  87. package/lib/ocaml/ReventlessSeedAws_Reconcile.cmt +0 -0
  88. package/lib/ocaml/ReventlessSeedAws_Reconcile.res +391 -0
  89. package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
  90. package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
  91. package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
  92. package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
  93. package/lib/ocaml/ReventlessSeedAws_Reset.res +1103 -0
  94. package/package.json +7 -4
  95. package/rescript.json +9 -1
  96. package/src/ReventlessSeedAws.res +198 -134
  97. package/src/ReventlessSeedAws.res.mjs +57 -31
  98. package/src/ReventlessSeedAws_Quiesce.res +307 -0
  99. package/src/ReventlessSeedAws_Quiesce.res.mjs +217 -0
  100. package/src/ReventlessSeedAws_Reconcile.res +391 -0
  101. package/src/ReventlessSeedAws_Reconcile.res.mjs +318 -0
  102. package/src/ReventlessSeedAws_Reset.res +1103 -0
  103. package/src/ReventlessSeedAws_Reset.res.mjs +991 -0
  104. package/tests/EndpointResolutionTest.res +83 -0
  105. package/tests/EndpointResolutionTest.res.mjs +115 -0
  106. package/tests/ObjectStoreReconcileTest.res +83 -0
  107. package/tests/ObjectStoreReconcileTest.res.mjs +102 -0
  108. package/tests/ObjectStoreResetTest.res +191 -0
  109. package/tests/ObjectStoreResetTest.res.mjs +211 -0
  110. package/tests/QuiesceResetTest.res +169 -0
  111. package/tests/QuiesceResetTest.res.mjs +157 -0
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-seed-aws",
3
- "version": "1.0.0-alpha.3",
4
- "description": "AWS connect for the Reventless seed harness — stack discovery and Cognito login, no AWS SDK",
3
+ "version": "1.0.0-alpha.30",
4
+ "description": "AWS connect + guarded reset for the Reventless seed harness — stack discovery, Cognito login, and a tag-scoped, allowlisted store wipe",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
7
- "@reventlessdev/reventless-seed": "1.0.0-alpha.3"
7
+ "@reventlessdev/rescript-node": "2.0.0-alpha.11",
8
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.18",
9
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.21"
8
10
  },
9
11
  "devDependencies": {
10
- "rescript": "12.3.0"
12
+ "rescript": "12.3.0",
13
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10"
11
14
  },
12
15
  "peerDependencies": {
13
16
  "rescript": "12.3.0"
package/rescript.json CHANGED
@@ -11,10 +11,18 @@
11
11
  {
12
12
  "dir": "src",
13
13
  "subdirs": true
14
+ },
15
+ {
16
+ "dir": "tests",
17
+ "subdirs": true,
18
+ "type": "dev"
14
19
  }
15
20
  ],
16
21
  "dependencies": [
17
- "@reventlessdev/reventless-seed"
22
+ "@reventlessdev/reventless-seed",
23
+ "@reventlessdev/rescript-aws-sdk",
24
+ "@reventlessdev/rescript-node",
25
+ "@reventlessdev/rescript-jest"
18
26
  ],
19
27
  "compiler-flags": [],
20
28
  "suffix": ".res.mjs"
@@ -14,12 +14,6 @@ open ReventlessSeed
14
14
 
15
15
  // ── Node bindings ─────────────────────────────────────────────────────────────
16
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
17
  type fetchInit = {method: string, headers: dict<string>, body?: string}
24
18
  type response
25
19
  @val external fetch: (string, fetchInit) => promise<response> = "fetch"
@@ -53,13 +47,17 @@ let envForBackend = (backend: option<string>): option<dict<string>> =>
53
47
  switch backend {
54
48
  | None => None
55
49
  | Some(url) =>
56
- let copy = processEnv->Dict.toArray->Dict.fromArray
50
+ let copy = NodeProcess.env->Dict.toArray->Dict.fromArray
57
51
  copy->Dict.set("PULUMI_BACKEND_URL", url)
58
52
  Some(copy)
59
53
  }
60
54
 
61
55
  let pulumi = (~projectDir: string, ~backend: option<string>, args: array<string>): string =>
62
- execFileSync("pulumi", args, {cwd: projectDir, encoding: "utf8", env: ?envForBackend(backend)})
56
+ NodeChildProcess.execFileSync(
57
+ "pulumi",
58
+ args,
59
+ {cwd: projectDir, encoding: "utf8", env: ?envForBackend(backend)},
60
+ )
63
61
 
64
62
  // Stacks that actually exist in the Pulumi backend for this project — the names
65
63
  // `pulumi stack output --stack <name>` accepts. A `Pulumi.<stack>.yaml` config
@@ -78,12 +76,15 @@ let deployedStacks = (~projectDir: string, ~backend: option<string>): array<stri
78
76
  }
79
77
 
80
78
  let stackOutputs = (~projectDir: string, ~backend: option<string>, stack: string): JSON.t => {
81
- let raw = try pulumi(~projectDir, ~backend, ["stack", "output", "--stack", stack, "--json"]) catch {
79
+ let raw = try pulumi(
80
+ ~projectDir,
81
+ ~backend,
82
+ ["stack", "output", "--stack", stack, "--json"],
83
+ ) catch {
82
84
  | _ =>
83
85
  throw(
84
86
  Seed.Failed(
85
- `pulumi stack output --stack ${stack} failed — is pulumi installed, logged in, ` ++
86
- "and the stack deployed?",
87
+ `pulumi stack output --stack ${stack} failed — is pulumi installed, logged in, ` ++ "and the stack deployed?",
87
88
  ),
88
89
  )
89
90
  }
@@ -98,7 +99,11 @@ let backendNote = (~backend: option<string>): string =>
98
99
  | None => ""
99
100
  }
100
101
 
101
- let resolveStack = async (~projectDir: string, ~backend: option<string>, ~stack: option<string>): string =>
102
+ let resolveStack = async (
103
+ ~projectDir: string,
104
+ ~backend: option<string>,
105
+ ~stack: option<string>,
106
+ ): string =>
102
107
  switch stack {
103
108
  | Some(s) => s
104
109
  | None =>
@@ -112,8 +117,7 @@ let resolveStack = async (~projectDir: string, ~backend: option<string>, ~stack:
112
117
  `no deployed Pulumi stacks for this project (\`pulumi stack ls\` is empty in ${projectDir})${backendNote(
113
118
  ~backend,
114
119
  )}. ` ++
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.",
120
+ "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.",
117
121
  ),
118
122
  )
119
123
  }
@@ -124,13 +128,13 @@ let resolveStack = async (~projectDir: string, ~backend: option<string>, ~stack:
124
128
  // ── Endpoints ─────────────────────────────────────────────────────────────────
125
129
 
126
130
  let fetchConfig = async (hostShellUrl: string): JSON.t => {
127
- let base = hostShellUrl->String.replaceRegExp(%re("/\/+$/g"), "")
131
+ let base = hostShellUrl->String.replaceRegExp(/\/+$/g, "")
128
132
  let url = `${base}/config.json`
129
133
  let res = try await fetch(url, {method: "GET", headers: Dict.make()}) catch {
130
134
  | _ => throw(Seed.Failed(`cannot reach ${url}`))
131
135
  }
132
136
  if !(res->responseOk) {
133
- throw(Seed.Failed(`GET ${url} → HTTP ${(res->responseStatus)->Int.toString}`))
137
+ throw(Seed.Failed(`GET ${url} → HTTP ${res->responseStatus->Int.toString}`))
134
138
  }
135
139
  await res->responseJson
136
140
  }
@@ -147,132 +151,196 @@ let resolveField = (~envKey: string, ~fromSource: option<string>, ~human: string
147
151
  }
148
152
  }
149
153
 
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)
154
+ // Which document publishes the deployment's endpoints. A stack that serves a
155
+ // host shell publishes them in the shell's `config.json` under the client's key
156
+ // names; one that does not publishes them as stack outputs under Pulumi's. The
157
+ // two arms name different keys for the same values, which is why this is a
158
+ // variant and not a merged lookup.
159
+ type source = HostShellConfig(JSON.t) | StackOutputs(JSON.t)
160
+
161
+ type endpoints = {
162
+ graphql: string,
163
+ cognitoRegion: string,
164
+ cognitoClientId: string,
165
+ }
166
+
167
+ /**
168
+ * The endpoints a source document publishes — pure, so the branch selection and
169
+ * every key it reads can be exercised with synthetic documents.
170
+ *
171
+ * Upload endpoints are no longer resolved here: under route B the seed mints through
172
+ * the domain API's `Upload_Presign` mutation on `graphql`, passing the store per asset,
173
+ * so there is no per-store URL to discover.
174
+ */
175
+ let endpointsFrom = (~stack: string, source: source): endpoints =>
176
+ switch source {
177
+ | HostShellConfig(cfg) =>
163
178
  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 =>
179
+ {
180
+ graphql: resolveField(
181
+ ~envKey="REVENTLESS_GRAPHQL_ENDPOINT",
182
+ ~fromSource=fromCfg("apiEndpoint"),
183
+ ~human="apiEndpoint",
184
+ ),
185
+ cognitoRegion: resolveField(
186
+ ~envKey="AWS_REGION",
187
+ ~fromSource=fromCfg("region"),
188
+ ~human="region",
189
+ ),
190
+ cognitoClientId: resolveField(
191
+ ~envKey="COGNITO_CLIENT_ID",
192
+ ~fromSource=fromCfg("cognitoClientId"),
193
+ ~human="cognitoClientId",
194
+ ),
195
+ }
196
+ | StackOutputs(outputs) =>
186
197
  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
- )
198
+ {
199
+ graphql: switch (
200
+ Seed.Prompt.envValue("REVENTLESS_GRAPHQL_ENDPOINT"),
201
+ out("domainMergedApiEndpoint"),
202
+ out("domainApiEndpoint"),
203
+ ) {
204
+ | (Some(v), _, _) | (_, Some(v), _) | (_, _, Some(v)) => v
205
+ | _ =>
206
+ throw(
207
+ Seed.Failed(
208
+ `stack "${stack}" exports neither domainMergedApiEndpoint nor domainApiEndpoint`,
209
+ ),
210
+ )
211
+ },
212
+ cognitoRegion: resolveField(
213
+ ~envKey="AWS_REGION",
214
+ ~fromSource=out("cognitoRegion"),
215
+ ~human="cognitoRegion",
216
+ ),
217
+ cognitoClientId: resolveField(
218
+ ~envKey="COGNITO_CLIENT_ID",
219
+ ~fromSource=out("cognitoUserPoolClientId"),
220
+ ~human="cognitoUserPoolClientId",
221
+ ),
199
222
  }
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
223
  }
224
+
225
+ let resolveEndpoints = async (
226
+ ~projectDir: string,
227
+ ~backend: option<string>,
228
+ ~stack: string,
229
+ ): endpoints => {
230
+ let outputs = stackOutputs(~projectDir, ~backend, stack)
231
+ let source = switch outputs->field("hostShellUrl")->Option.flatMap(asString) {
232
+ | Some(hostShellUrl) => HostShellConfig(await fetchConfig(hostShellUrl))
233
+ | None => StackOutputs(outputs)
234
+ }
235
+ endpointsFrom(~stack, source)
214
236
  }
215
237
 
216
238
  // ── Cognito login ─────────────────────────────────────────────────────────────
217
239
 
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
- ]),
240
+ let cognito = (~region: string, ~clientId: string) =>
241
+ async (~username: string, ~password: string): string => {
242
+ let body = JSON.stringify(
243
+ JSON.Encode.object(
244
+ Dict.fromArray([
245
+ ("AuthFlow", JSON.Encode.string("USER_PASSWORD_AUTH")),
246
+ ("ClientId", JSON.Encode.string(clientId)),
247
+ (
248
+ "AuthParameters",
249
+ JSON.Encode.object(
250
+ Dict.fromArray([
251
+ ("USERNAME", JSON.Encode.string(username)),
252
+ ("PASSWORD", JSON.Encode.string(password)),
253
+ ]),
254
+ ),
234
255
  ),
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}`,
256
+ ]),
257
257
  ),
258
258
  )
259
+ let headers = Dict.fromArray([
260
+ ("content-type", "application/x-amz-json-1.1"),
261
+ ("x-amz-target", "AWSCognitoIdentityProviderService.InitiateAuth"),
262
+ ])
263
+ let res = try await fetch(
264
+ `https://cognito-idp.${region}.amazonaws.com/`,
265
+ {
266
+ method: "POST",
267
+ headers,
268
+ body,
269
+ },
270
+ ) catch {
271
+ | _ => throw(Seed.Failed(`cannot reach Cognito in ${region}`))
272
+ }
273
+ let json = await res->responseJson
274
+ if !(res->responseOk) {
275
+ let detail =
276
+ json->field("message")->Option.flatMap(asString)->Option.getOr(JSON.stringify(json))
277
+ throw(
278
+ Seed.Failed(
279
+ `Cognito InitiateAuth failed (HTTP ${res->responseStatus->Int.toString}): ${detail}`,
280
+ ),
281
+ )
282
+ }
283
+ switch json->field("ChallengeName")->Option.flatMap(asString) {
284
+ | Some(challenge) =>
285
+ throw(
286
+ Seed.Failed(
287
+ `Cognito returned challenge ${challenge} — set a permanent password first ` ++ "(aws cognito-idp admin-set-user-password … --permanent).",
288
+ ),
289
+ )
290
+ | None => ()
291
+ }
292
+ switch json
293
+ ->field("AuthenticationResult")
294
+ ->Option.flatMap(ar => ar->field("IdToken"))
295
+ ->Option.flatMap(asString) {
296
+ | Some(token) => token
297
+ | None => throw(Seed.Failed(`Cognito response carried no IdToken: ${JSON.stringify(json)}`))
298
+ }
299
+ }
300
+
301
+ // ── Active role ───────────────────────────────────────────────────────────────
302
+
303
+ /**
304
+ * The Cognito path's active-role door: `Platform_SetActiveRole` on the domain
305
+ * API, which is where the seed already sends everything else.
306
+ *
307
+ * It sits on the domain base document at `AllowAuthenticated`, deliberately not
308
+ * behind the Admin gate — the whole point is that a caller can drop into a
309
+ * narrower role — so a token already narrowed to `Shopper` can still call this
310
+ * to widen back out. There is no chicken-and-egg, and no `sub` argument: the
311
+ * mutation addresses the authorizer-verified caller's own row and nobody else's.
312
+ *
313
+ * Returns `None` always: the choice is a stored row, read by the pool's
314
+ * pre-token-generation trigger on the NEXT login, so the current bearer is
315
+ * unchanged by it and the caller must re-authenticate to see any effect.
316
+ *
317
+ * Split-API only. A unified-mode deployment's single API does not carry the
318
+ * field, so a refusal naming it is reported as "this deployment offers no role
319
+ * switch" rather than as a seeding failure.
320
+ */
321
+ let setActiveRole: Seed.Connect.roleSwitch = async (~client, ~role) => {
322
+ let argument = switch role {
323
+ | None => "null"
324
+ | Some(r) => JSON.stringify(JSON.Encode.string(r))
259
325
  }
260
- switch json->field("ChallengeName")->Option.flatMap(asString) {
261
- | Some(challenge) =>
326
+ try {
327
+ let _ = await Seed.Client.gql(
328
+ client,
329
+ ~query=`mutation { Platform_SetActiveRole(activeRole: ${argument}) { activeRole availableRoles } }`,
330
+ ~label="Platform_SetActiveRole",
331
+ )
332
+ None
333
+ } catch {
334
+ | Seed.Failed(message) =>
262
335
  throw(
263
336
  Seed.Failed(
264
- `Cognito returned challenge ${challenge} — set a permanent password first ` ++
265
- "(aws cognito-idp admin-set-user-password … --permanent).",
337
+ message->String.includes("Platform_SetActiveRole")
338
+ ? `this deployment offers no role switch — \`Platform_SetActiveRole\` is a split-API ` ++
339
+ `field, and a unified-mode API does not carry it. Change the role in the host shell ` ++
340
+ `instead.\n ${message}`
341
+ : message,
266
342
  ),
267
343
  )
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
344
  }
277
345
  }
278
346
 
@@ -302,15 +370,11 @@ let connect = (~projectDir: string=".", ~stack=?, ~backend=?, ()): (
302
370
  | None => backend
303
371
  }
304
372
  let stackName = await resolveStack(~projectDir, ~backend, ~stack)
305
- let (endpoint, uploadEndpoint, region, clientId) = await resolveEndpoints(
306
- ~projectDir,
307
- ~backend,
308
- ~stack=stackName,
309
- )
373
+ let eps = await resolveEndpoints(~projectDir, ~backend, ~stack=stackName)
310
374
  await Seed.Connect.make(
311
375
  ~label=stackName,
312
- ~endpoint,
313
- ~uploadEndpoint,
314
- ~login=cognito(~region, ~clientId),
376
+ ~endpoint=eps.graphql,
377
+ ~login=cognito(~region=eps.cognitoRegion, ~clientId=eps.cognitoClientId),
378
+ ~roleSwitch=setActiveRole,
315
379
  )
316
380
  }
@@ -4,6 +4,8 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Nodechild_process from "node:child_process";
6
6
  import * as Seed$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed.res.mjs";
7
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
+ import * as Seed_Client$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Client.res.mjs";
7
9
  import * as Seed_Prompt$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Prompt.res.mjs";
8
10
  import * as Seed_Connect$ReventlessSeed from "@reventlessdev/reventless-seed/src/Seed_Connect.res.mjs";
9
11
 
@@ -108,7 +110,7 @@ async function resolveStack(projectDir, backend, stack) {
108
110
  return await Seed_Prompt$ReventlessSeed.select("Stack:", stacks.map(s => [
109
111
  s,
110
112
  s
111
- ]), undefined);
113
+ ]), undefined, undefined);
112
114
  }
113
115
 
114
116
  async function fetchConfig(hostShellUrl) {
@@ -152,34 +154,28 @@ function resolveField(envKey, fromSource, human) {
152
154
  };
153
155
  }
154
156
 
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);
157
+ function endpointsFrom(stack, source) {
158
+ if (source.TAG === "HostShellConfig") {
159
+ let cfg = source._0;
160
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
- ];
161
+ return {
162
+ graphql: resolveField("REVENTLESS_GRAPHQL_ENDPOINT", fromCfg("apiEndpoint"), "apiEndpoint"),
163
+ cognitoRegion: resolveField("AWS_REGION", fromCfg("region"), "region"),
164
+ cognitoClientId: resolveField("COGNITO_CLIENT_ID", fromCfg("cognitoClientId"), "cognitoClientId")
165
+ };
171
166
  }
167
+ let outputs = source._0;
172
168
  let out = key => Stdlib_Option.flatMap(field(outputs, key), asString);
173
169
  let match = Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_GRAPHQL_ENDPOINT");
174
170
  let match$1 = out("domainMergedApiEndpoint");
175
171
  let match$2 = out("domainApiEndpoint");
176
- let endpoint$1;
172
+ let tmp;
177
173
  if (match !== undefined) {
178
- endpoint$1 = match;
174
+ tmp = match;
179
175
  } else if (match$1 !== undefined) {
180
- endpoint$1 = match$1;
176
+ tmp = match$1;
181
177
  } else if (match$2 !== undefined) {
182
- endpoint$1 = match$2;
178
+ tmp = match$2;
183
179
  } else {
184
180
  throw {
185
181
  RE_EXN_ID: Seed$ReventlessSeed.Failed,
@@ -187,15 +183,24 @@ async function resolveEndpoints(projectDir, backend, stack) {
187
183
  Error: new Error()
188
184
  };
189
185
  }
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
- ];
186
+ return {
187
+ graphql: tmp,
188
+ cognitoRegion: resolveField("AWS_REGION", out("cognitoRegion"), "cognitoRegion"),
189
+ cognitoClientId: resolveField("COGNITO_CLIENT_ID", out("cognitoUserPoolClientId"), "cognitoUserPoolClientId")
190
+ };
191
+ }
192
+
193
+ async function resolveEndpoints(projectDir, backend, stack) {
194
+ let outputs = stackOutputs(projectDir, backend, stack);
195
+ let hostShellUrl = Stdlib_Option.flatMap(field(outputs, "hostShellUrl"), asString);
196
+ let source = hostShellUrl !== undefined ? ({
197
+ TAG: "HostShellConfig",
198
+ _0: await fetchConfig(hostShellUrl)
199
+ }) : ({
200
+ TAG: "StackOutputs",
201
+ _0: outputs
202
+ });
203
+ return endpointsFrom(stack, source);
199
204
  }
200
205
 
201
206
  function cognito(region, clientId) {
@@ -276,14 +281,33 @@ function cognito(region, clientId) {
276
281
  };
277
282
  }
278
283
 
284
+ async function setActiveRole(client, role) {
285
+ let argument = role !== undefined ? JSON.stringify(role) : "null";
286
+ try {
287
+ await Seed_Client$ReventlessSeed.gql(client, `mutation { Platform_SetActiveRole(activeRole: ` + argument + `) { activeRole availableRoles } }`, "Platform_SetActiveRole");
288
+ return;
289
+ } catch (raw_message) {
290
+ let message = Primitive_exceptions.internalToException(raw_message);
291
+ if (message.RE_EXN_ID === Seed$ReventlessSeed.Failed) {
292
+ let message$1 = message._1;
293
+ throw {
294
+ RE_EXN_ID: Seed$ReventlessSeed.Failed,
295
+ _1: message$1.includes("Platform_SetActiveRole") ? `this deployment offers no role switch — \`Platform_SetActiveRole\` is a split-API field, and a unified-mode API does not carry it. Change the role in the host shell instead.\n ` + message$1 : message$1,
296
+ Error: new Error()
297
+ };
298
+ }
299
+ throw message;
300
+ }
301
+ }
302
+
279
303
  function connect($staropt$star, stack, backend, param) {
280
304
  return async () => {
281
305
  let projectDir = $staropt$star !== undefined ? $staropt$star : ".";
282
306
  let url = Seed_Prompt$ReventlessSeed.envValue("SEED_PULUMI_BACKEND");
283
307
  let backend$1 = url !== undefined ? url : backend;
284
308
  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);
309
+ let eps = await resolveEndpoints(projectDir, backend$1, stackName);
310
+ return await Seed_Connect$ReventlessSeed.make(stackName, eps.graphql, cognito(eps.cognitoRegion, eps.cognitoClientId), setActiveRole, undefined);
287
311
  };
288
312
  }
289
313
 
@@ -298,8 +322,10 @@ export {
298
322
  resolveStack,
299
323
  fetchConfig,
300
324
  resolveField,
325
+ endpointsFrom,
301
326
  resolveEndpoints,
302
327
  cognito,
328
+ setActiveRole,
303
329
  connect,
304
330
  }
305
331
  /* node:child_process Not a pure module */