@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/CHANGELOG.md CHANGED
@@ -3,6 +3,213 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 1.0.0-alpha.30 (2026-09-11)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
9
+
10
+
11
+
12
+
13
+
14
+ # 1.0.0-alpha.29 (2026-09-09)
15
+
16
+ ### Features
17
+
18
+ * **seed:** choose the role a run acts as, instead of inheriting a stored one ([edf41ca](https://github.com/ReventlessDev/reventless-core/commit/edf41ca80f84bf340679f200ebfeeeb4a590ae03))
19
+
20
+
21
+ # 1.0.0-alpha.28 (2026-09-08)
22
+
23
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
24
+
25
+
26
+
27
+
28
+
29
+ # 1.0.0-alpha.27 (2026-09-04)
30
+
31
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
32
+
33
+
34
+
35
+
36
+
37
+ # 1.0.0-alpha.26 (2026-08-27)
38
+
39
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
40
+
41
+
42
+
43
+
44
+
45
+ # 1.0.0-alpha.25 (2026-08-27)
46
+
47
+ ### Bug Fixes
48
+
49
+ * **seed-aws:** a throttled control plane aborts the reset hold and blames IAM ([95af263](https://github.com/ReventlessDev/reventless-core/commit/95af263afba7dfdfa9a7fc45f494b302dba4baf8))
50
+
51
+
52
+ # 1.0.0-alpha.24 (2026-08-23)
53
+
54
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
55
+
56
+
57
+
58
+
59
+
60
+ # 1.0.0-alpha.23 (2026-08-20)
61
+
62
+ ### Features
63
+
64
+ * **seed:** choose the login from the platform's users.yaml ([74e61de](https://github.com/ReventlessDev/reventless-core/commit/74e61de8fba3ee4cc62b368ff12010f162d49db7))
65
+
66
+
67
+ # 1.0.0-alpha.22 (2026-08-18)
68
+
69
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
70
+
71
+
72
+
73
+
74
+
75
+ # 1.0.0-alpha.21 (2026-08-16)
76
+
77
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
78
+
79
+
80
+
81
+
82
+
83
+ # 1.0.0-alpha.20 (2026-08-16)
84
+
85
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
86
+
87
+
88
+
89
+
90
+
91
+ # 1.0.0-alpha.19 (2026-08-15)
92
+
93
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
94
+
95
+
96
+
97
+
98
+
99
+ # 1.0.0-alpha.18 (2026-08-14)
100
+
101
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
102
+
103
+
104
+
105
+
106
+
107
+ # 1.0.0-alpha.17 (2026-08-13)
108
+
109
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
110
+
111
+
112
+
113
+
114
+
115
+ # 1.0.0-alpha.16 (2026-08-13)
116
+
117
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
118
+
119
+
120
+
121
+
122
+
123
+ # 1.0.0-alpha.15 (2026-08-11)
124
+
125
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
126
+
127
+
128
+
129
+
130
+
131
+ # 1.0.0-alpha.14 (2026-08-09)
132
+
133
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
134
+
135
+
136
+
137
+
138
+
139
+ # 1.0.0-alpha.13 (2026-08-09)
140
+
141
+ ### Bug Fixes
142
+
143
+ * **seed-aws:** hold and recycle a stack's runtimes so a reset stays empty ([4baaa38](https://github.com/ReventlessDev/reventless-core/commit/4baaa3827ef30a0adccdef5d2bccc688e7b6686f))
144
+
145
+
146
+ # 1.0.0-alpha.12 (2026-08-05)
147
+
148
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
149
+
150
+
151
+
152
+
153
+
154
+ # 1.0.0-alpha.11 (2026-08-03)
155
+
156
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
157
+
158
+
159
+
160
+
161
+
162
+ # 1.0.0-alpha.10 (2026-08-02)
163
+
164
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
165
+
166
+
167
+
168
+
169
+
170
+ # 1.0.0-alpha.9 (2026-08-02)
171
+
172
+ ### Features
173
+
174
+ * **aws,core,spec,seed-aws:** expire uploads nobody committed a reference to ([f63e84c](https://github.com/ReventlessDev/reventless-core/commit/f63e84c1a11cc350b799a6f69a2e7427cf1ea6e9))
175
+
176
+
177
+ # 1.0.0-alpha.8 (2026-08-01)
178
+
179
+ ### Features
180
+
181
+ * **seed-aws:** wipe a plugin's declared object stores in seed:reset ([2280c5f](https://github.com/ReventlessDev/reventless-core/commit/2280c5f14755d13fff0fe1ac11eb440cd0617f1d))
182
+
183
+
184
+ # 1.0.0-alpha.7 (2026-08-01)
185
+
186
+ ### Features
187
+
188
+ * **upload:** add the release half of the upload contract via the domain API ([448f887](https://github.com/ReventlessDev/reventless-core/commit/448f88714a5875be420f794a08d482fcf4ba8404))
189
+
190
+
191
+ # 1.0.0-alpha.6 (2026-07-31)
192
+
193
+ **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
194
+
195
+
196
+
197
+
198
+
199
+ # 1.0.0-alpha.5 (2026-07-31)
200
+
201
+ ### Bug Fixes
202
+
203
+ * **seed:** address a deployment's per-store upload endpoints ([45b226e](https://github.com/ReventlessDev/reventless-core/commit/45b226e00604af9fd28b6fb0b75ba7d3414791f2))
204
+
205
+
206
+ # 1.0.0-alpha.4 (2026-07-27)
207
+
208
+ ### Features
209
+
210
+ * **seed:** guarded AWS store reset (seed:reset) with per-project scope selection ([8446172](https://github.com/ReventlessDev/reventless-core/commit/8446172cbb53776a58f53220d19e96d96ce94508))
211
+
212
+
6
213
  # 1.0.0-alpha.3 (2026-07-27)
7
214
 
8
215
  **Note:** Version bump only for package @reventlessdev/reventless-seed-aws
@@ -1,2 +1,2 @@
1
- #Start(1785139469141)
2
- #Done(1785139473591)
1
+ #Start(1789087760185)
2
+ #Done(1789087760226)
@@ -0,0 +1 @@
1
+ {"cmt_scan":[{"also_scan_build_root":true,"build_root":"lib/bs","scan_dirs":["src","tests"]}],"dirs":["src","tests"],"generated":[],"pkgs":[["@reventlessdev/rescript-aws-sdk","/home/runner/work/reventless-core/reventless-core/rescript/aws-sdk"],["@reventlessdev/rescript-jest","/home/runner/work/reventless-core/reventless-core/rescript/jest"],["@reventlessdev/rescript-node","/home/runner/work/reventless-core/reventless-core/rescript/node"],["@reventlessdev/reventless-seed","/home/runner/work/reventless-core/reventless-core/reventless/seed"]],"version":2}
@@ -2,7 +2,7 @@
2
2
  "version": "12.3.0",
3
3
  "bsc_path": "/home/runner/work/reventless-core/reventless-core/node_modules/@rescript/linux-x64/bin/bsc.exe",
4
4
  "bsc_hash": "75e3a59c95cc953608fd3dd6ea6ed68141bba4414a20a52f114261b8a48385fa",
5
- "rescript_config_hash": "a0f3879e5fe6709f54eab455ba07fa768dc067bd625516a260ee882dd14e06fe",
5
+ "rescript_config_hash": "c18a0067c363fd1290f6e7f6fe6212d2e16e201c1702fac667962047f88ed468",
6
6
  "runtime_path": "/home/runner/work/reventless-core/reventless-core/node_modules/@rescript/runtime",
7
- "generated_at": "1785139473607"
7
+ "generated_at": "1789087760231"
8
8
  }
Binary file
Binary file
Binary file
Binary file
@@ -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
  }