@reventlessdev/reventless-seed 1.0.0-alpha.20 → 1.0.0-alpha.21
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 +12 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +1 -0
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed.ast +0 -0
- package/lib/bs/src/Seed.res +8 -2
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Client.ast +0 -0
- package/lib/bs/src/Seed_Client.res +169 -21
- package/lib/bs/src/Seed_Client.res.mjs +142 -7
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Connect.ast +0 -0
- package/lib/bs/src/Seed_Connect.res +258 -19
- package/lib/bs/src/Seed_Connect.res.mjs +158 -8
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Prompt.ast +0 -0
- package/lib/bs/src/Seed_Prompt.res +47 -23
- package/lib/bs/src/Seed_Prompt.res.mjs +32 -22
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Random.ast +0 -0
- package/lib/bs/src/Seed_Random.res +4 -2
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner.ast +0 -0
- package/lib/bs/src/Seed_Runner.res +7 -12
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload.ast +0 -0
- package/lib/bs/src/Seed_Upload.res +4 -2
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Users.ast +0 -0
- package/lib/bs/src/Seed_Users.res +15 -3
- package/lib/bs/src/Seed_Users.res.mjs +2 -1
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ClientTest.ast +0 -0
- package/lib/bs/tests/Seed_ClientTest.res +146 -16
- package/lib/bs/tests/Seed_ClientTest.res.mjs +102 -1
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
- package/lib/bs/tests/Seed_ConnectTest.res +64 -0
- package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_PromptTest.ast +0 -0
- package/lib/bs/tests/Seed_PromptTest.res +8 -12
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_RandomTest.ast +0 -0
- package/lib/bs/tests/Seed_RandomTest.res +130 -90
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_UsersTest.ast +0 -0
- package/lib/bs/tests/Seed_UsersTest.res +39 -2
- package/lib/bs/tests/Seed_UsersTest.res.mjs +20 -2
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ReventlessSeed.cmi +0 -0
- package/lib/ocaml/ReventlessSeed.cmt +0 -0
- package/lib/ocaml/ReventlessSeed.mlmap +1 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed.ast +0 -0
- package/lib/ocaml/Seed.res +8 -2
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Client.ast +0 -0
- package/lib/ocaml/Seed_Client.res +169 -21
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ClientTest.ast +0 -0
- package/lib/ocaml/Seed_ClientTest.res +146 -16
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Connect.ast +0 -0
- package/lib/ocaml/Seed_Connect.res +258 -19
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ConnectTest.ast +0 -0
- package/lib/ocaml/Seed_ConnectTest.res +64 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Prompt.ast +0 -0
- package/lib/ocaml/Seed_Prompt.res +47 -23
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_PromptTest.ast +0 -0
- package/lib/ocaml/Seed_PromptTest.res +8 -12
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Random.ast +0 -0
- package/lib/ocaml/Seed_Random.res +4 -2
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest.ast +0 -0
- package/lib/ocaml/Seed_RandomTest.res +130 -90
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Runner.ast +0 -0
- package/lib/ocaml/Seed_Runner.res +7 -12
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload.ast +0 -0
- package/lib/ocaml/Seed_Upload.res +4 -2
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Users.ast +0 -0
- package/lib/ocaml/Seed_Users.res +15 -3
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_UsersTest.ast +0 -0
- package/lib/ocaml/Seed_UsersTest.res +39 -2
- package/package.json +2 -2
- package/src/Seed.res +8 -2
- package/src/Seed_Client.res +169 -21
- package/src/Seed_Client.res.mjs +142 -7
- package/src/Seed_Connect.res +258 -19
- package/src/Seed_Connect.res.mjs +158 -8
- package/src/Seed_Prompt.res +47 -23
- package/src/Seed_Prompt.res.mjs +32 -22
- package/src/Seed_Random.res +4 -2
- package/src/Seed_Runner.res +7 -12
- package/src/Seed_Upload.res +4 -2
- package/src/Seed_Users.res +15 -3
- package/src/Seed_Users.res.mjs +2 -1
- package/tests/Seed_ClientTest.res +146 -16
- package/tests/Seed_ClientTest.res.mjs +102 -1
- package/tests/Seed_ConnectTest.res +64 -0
- package/tests/Seed_ConnectTest.res.mjs +56 -0
- package/tests/Seed_PromptTest.res +8 -12
- package/tests/Seed_RandomTest.res +130 -90
- package/tests/Seed_UsersTest.res +39 -2
- package/tests/Seed_UsersTest.res.mjs +20 -2
|
@@ -53,6 +53,10 @@ function envValue(key) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function hasTty() {
|
|
57
|
+
return Stdlib_Option.getOr(process.stdin.isTTY, false);
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
function requireTty() {
|
|
57
61
|
if (Stdlib_Option.getOr(process.stdin.isTTY, false)) {
|
|
58
62
|
return;
|
|
@@ -142,6 +146,15 @@ async function select(title, options, env, defaultIndex) {
|
|
|
142
146
|
return await pick();
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
function typedUser(username, password) {
|
|
150
|
+
return {
|
|
151
|
+
username: username,
|
|
152
|
+
password: password,
|
|
153
|
+
groups: [],
|
|
154
|
+
userId: undefined
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
async function askCredentials(localDefaults, envUser) {
|
|
146
159
|
let username;
|
|
147
160
|
if (envUser !== undefined) {
|
|
@@ -160,10 +173,7 @@ async function askCredentials(localDefaults, envUser) {
|
|
|
160
173
|
let entered$1 = await askHidden(localDefaults ? "Password [admin]: " : "Password: ");
|
|
161
174
|
password = entered$1 === "" && localDefaults ? "admin" : entered$1;
|
|
162
175
|
}
|
|
163
|
-
return
|
|
164
|
-
username,
|
|
165
|
-
password
|
|
166
|
-
];
|
|
176
|
+
return typedUser(username, password);
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
async function fromUsersFile(envUser) {
|
|
@@ -179,10 +189,10 @@ async function fromUsersFile(envUser) {
|
|
|
179
189
|
]), "SEED_USER", 0);
|
|
180
190
|
return Stdlib_Option.map(chosen, u => {
|
|
181
191
|
console.log(`Logging in as ` + u.username + ` (from ` + path + `)`);
|
|
182
|
-
return
|
|
183
|
-
u
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
return {
|
|
193
|
+
caller: u,
|
|
194
|
+
accounts: users
|
|
195
|
+
};
|
|
186
196
|
});
|
|
187
197
|
}
|
|
188
198
|
|
|
@@ -190,33 +200,31 @@ async function credentials(localDefaultsOpt) {
|
|
|
190
200
|
let localDefaults = localDefaultsOpt !== undefined ? localDefaultsOpt : false;
|
|
191
201
|
let envUser = envValue("REVENTLESS_DEMO_USER");
|
|
192
202
|
let match = envValue("REVENTLESS_DEMO_PASSWORD");
|
|
193
|
-
let
|
|
203
|
+
let resolved;
|
|
194
204
|
let exit = 0;
|
|
195
205
|
if (envUser !== undefined && match !== undefined) {
|
|
196
|
-
|
|
197
|
-
envUser,
|
|
198
|
-
|
|
199
|
-
|
|
206
|
+
resolved = {
|
|
207
|
+
caller: typedUser(envUser, match),
|
|
208
|
+
accounts: []
|
|
209
|
+
};
|
|
200
210
|
} else {
|
|
201
211
|
exit = 1;
|
|
202
212
|
}
|
|
203
213
|
if (exit === 1) {
|
|
204
|
-
let
|
|
205
|
-
|
|
214
|
+
let resolved$1 = await fromUsersFile(envUser);
|
|
215
|
+
resolved = resolved$1 !== undefined ? resolved$1 : ({
|
|
216
|
+
caller: await askCredentials(localDefaults, envUser),
|
|
217
|
+
accounts: []
|
|
218
|
+
});
|
|
206
219
|
}
|
|
207
|
-
|
|
208
|
-
let username = match$1[0];
|
|
209
|
-
if (username === "" || password === "") {
|
|
220
|
+
if (resolved.caller.username === "" || resolved.caller.password === "") {
|
|
210
221
|
throw {
|
|
211
222
|
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
212
223
|
_1: "username and password are required.",
|
|
213
224
|
Error: new Error()
|
|
214
225
|
};
|
|
215
226
|
}
|
|
216
|
-
return
|
|
217
|
-
username,
|
|
218
|
-
password
|
|
219
|
-
];
|
|
227
|
+
return resolved;
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
export {
|
|
@@ -225,11 +233,13 @@ export {
|
|
|
225
233
|
iface,
|
|
226
234
|
close,
|
|
227
235
|
envValue,
|
|
236
|
+
hasTty,
|
|
228
237
|
requireTty,
|
|
229
238
|
ask,
|
|
230
239
|
askHidden,
|
|
231
240
|
isAffirmative,
|
|
232
241
|
select,
|
|
242
|
+
typedUser,
|
|
233
243
|
askCredentials,
|
|
234
244
|
fromUsersFile,
|
|
235
245
|
credentials,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -10,13 +10,15 @@ type t = {mutable state: int}
|
|
|
10
10
|
|
|
11
11
|
let make = (~seed: int): t => {state: seed}
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
/** mulberry32 — small, fast, and stable across JS engines and Node versions. */
|
|
15
14
|
let float = (r: t): float => {
|
|
16
15
|
r.state = (r.state + 0x6d2b79f5)->Int.bitwiseOr(0)
|
|
17
16
|
let a = r.state
|
|
18
17
|
let b = imul(a->Int.bitwiseXor(a->Int.shiftRightUnsigned(15)), a->Int.bitwiseOr(1))
|
|
19
|
-
let c =
|
|
18
|
+
let c =
|
|
19
|
+
b->Int.bitwiseXor(
|
|
20
|
+
b + imul(b->Int.bitwiseXor(b->Int.shiftRightUnsigned(7)), b->Int.bitwiseOr(61)),
|
|
21
|
+
)
|
|
20
22
|
let bits = c->Int.bitwiseXor(c->Int.shiftRightUnsigned(14))
|
|
21
23
|
// `bits` is a signed 32-bit result; fold the sign bit back in to get the
|
|
22
24
|
// unsigned value a `>>> 0` would produce.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
open Seed_Types
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
let envOr = (key: string, fallback: string): string =>
|
|
7
6
|
NodeProcess.env->Dict.get(key)->Option.getOr(fallback)
|
|
8
7
|
|
|
@@ -58,14 +57,12 @@ let verifyViews = async (client: Seed_Client.t, ~views: array<view>): dict<int>
|
|
|
58
57
|
Console.log(` ${count->Int.toString->String.padStart(4, " ")} ${name}${note}`)
|
|
59
58
|
})
|
|
60
59
|
|
|
61
|
-
let empty =
|
|
62
|
-
|
|
63
|
-
->
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
)
|
|
60
|
+
let empty = views->Array.filterMap(v =>
|
|
61
|
+
switch v {
|
|
62
|
+
| Seeded(name) if counts->Dict.get(name)->Option.getOr(0) == 0 => Some(name)
|
|
63
|
+
| _ => None
|
|
64
|
+
}
|
|
65
|
+
)
|
|
69
66
|
if empty->Array.length > 0 {
|
|
70
67
|
throw(Failed(`these views are still empty after seeding: ${empty->Array.join(", ")}`))
|
|
71
68
|
}
|
|
@@ -138,9 +135,7 @@ let run = async (main: unit => promise<unit>): unit =>
|
|
|
138
135
|
Console.error("")
|
|
139
136
|
Console.error("Seeding aborted with an unexpected error:")
|
|
140
137
|
Console.error("")
|
|
141
|
-
Console.error(
|
|
142
|
-
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"),
|
|
143
|
-
)
|
|
138
|
+
Console.error(exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"))
|
|
144
139
|
NodeProcess.exit(1)
|
|
145
140
|
}
|
|
146
141
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -64,7 +64,9 @@ let uploadAsset = async (
|
|
|
64
64
|
// GraphQL string literals: JSON-quote the argument values (JSON string escaping is a
|
|
65
65
|
// superset-safe subset of GraphQL's for these ASCII-ish inputs).
|
|
66
66
|
let q = s => JSON.stringify(JSON.Encode.string(s))
|
|
67
|
-
let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(
|
|
67
|
+
let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(
|
|
68
|
+
fileName,
|
|
69
|
+
)}, contentType: ${q(contentType)}) { uploadUrl storageRef } }`
|
|
68
70
|
|
|
69
71
|
let presign = try Ok(await Seed_Client.gql(client, ~query, ~label="Upload_Presign")) catch {
|
|
70
72
|
| Seed_Types.Failed(m) => Error(m)
|
|
@@ -89,7 +91,7 @@ let uploadAsset = async (
|
|
|
89
91
|
| Ok(putRes) if putRes->responseOk => Ok(storageRef)
|
|
90
92
|
| Ok(putRes) =>
|
|
91
93
|
let detail = await putRes->responseText
|
|
92
|
-
Error(`upload PUT failed with HTTP ${
|
|
94
|
+
Error(`upload PUT failed with HTTP ${putRes->responseStatus->Int.toString}: ${detail}`)
|
|
93
95
|
}
|
|
94
96
|
| _ => Error("Upload_Presign response missing uploadUrl/storageRef")
|
|
95
97
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,8 +11,20 @@
|
|
|
11
11
|
|
|
12
12
|
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* One account: what to log in as, what to send with it, and — when the file
|
|
16
|
+
* records one — the id the platform stamps on rows this account writes.
|
|
17
|
+
*
|
|
18
|
+
* `userId` is optional because absence is normal on both platforms: a local file
|
|
19
|
+
* may omit it and let the auth adapter default it to the username, and a
|
|
20
|
+
* hand-maintained AWS file records subs the pool minted, so it can lag.
|
|
21
|
+
*/
|
|
22
|
+
type user = {
|
|
23
|
+
username: string,
|
|
24
|
+
password: string,
|
|
25
|
+
groups: array<string>,
|
|
26
|
+
userId: option<string>,
|
|
27
|
+
}
|
|
16
28
|
|
|
17
29
|
let asString = (json: JSON.t): option<string> =>
|
|
18
30
|
switch json {
|
|
@@ -32,7 +44,7 @@ let userOf = (json: JSON.t): option<user> =>
|
|
|
32
44
|
| Some(JSON.Array(items)) => items->Array.filterMap(asString)
|
|
33
45
|
| _ => []
|
|
34
46
|
}
|
|
35
|
-
Some({username, password, groups})
|
|
47
|
+
Some({username, password, groups, userId: obj->Dict.get("userId")->Option.flatMap(asString)})
|
|
36
48
|
| _ => None
|
|
37
49
|
}
|
|
38
50
|
| _ => None
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -15,15 +15,16 @@ open JestGlobals
|
|
|
15
15
|
|
|
16
16
|
let errors = entries => JSON.Encode.array(entries->Array.map(JSON.Encode.object))
|
|
17
17
|
|
|
18
|
-
let errorOfType = errorType =>
|
|
19
|
-
Dict.fromArray([("errorType", JSON.Encode.string(errorType))])
|
|
18
|
+
let errorOfType = errorType => Dict.fromArray([("errorType", JSON.Encode.string(errorType))])
|
|
20
19
|
|
|
21
20
|
describe("Seed_Client.isTransient:", () => {
|
|
22
21
|
testSync("resends a fault on the endpoint's own side", () =>
|
|
23
|
-
[
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
[
|
|
23
|
+
"InternalFailure",
|
|
24
|
+
"ServiceUnavailable",
|
|
25
|
+
"Throttling",
|
|
26
|
+
"TooManyRequestsException",
|
|
27
|
+
]->Array.forEach(t => expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(true))
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
// The exact payload the seed aborted on.
|
|
@@ -33,8 +34,8 @@ describe("Seed_Client.isTransient:", () => {
|
|
|
33
34
|
})
|
|
34
35
|
|
|
35
36
|
testSync("fails fast on an error that is an answer", () =>
|
|
36
|
-
["ValidationException", "Unauthorized", "FieldUndefined"]->Array.forEach(
|
|
37
|
-
expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(false)
|
|
37
|
+
["ValidationException", "Unauthorized", "FieldUndefined"]->Array.forEach(
|
|
38
|
+
t => expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(false),
|
|
38
39
|
)
|
|
39
40
|
)
|
|
40
41
|
|
|
@@ -52,8 +53,9 @@ describe("Seed_Client.isTransient:", () => {
|
|
|
52
53
|
// endpoint said errors but named none" into an endless resend.
|
|
53
54
|
testSync("treats an empty or untyped error list as final", () => {
|
|
54
55
|
expect(Seed_Client.isTransient(errors([])))->toBe(false)
|
|
55
|
-
expect(
|
|
56
|
-
|
|
56
|
+
expect(
|
|
57
|
+
Seed_Client.isTransient(errors([Dict.fromArray([("message", JSON.Encode.string("boom"))])])),
|
|
58
|
+
)->toBe(false)
|
|
57
59
|
expect(Seed_Client.isTransient(JSON.Encode.null))->toBe(false)
|
|
58
60
|
})
|
|
59
61
|
})
|
|
@@ -70,8 +72,15 @@ let b64url = payload =>
|
|
|
70
72
|
->NodeBuffer.fromStringUtf8
|
|
71
73
|
->NodeBuffer.toStringBase64Url
|
|
72
74
|
|
|
75
|
+
// A REAL JOSE header, not the literal string "header". The difference is the
|
|
76
|
+
// whole bug: `{"kid":…,"alg":"RS256"}` is base64url JSON and decodes exactly as
|
|
77
|
+
// the payload does, so `claims` took it as the claims set and found no groups
|
|
78
|
+
// and no id in any Cognito token ever issued. A fake header that happened not to
|
|
79
|
+
// decode is what let every assertion below pass against broken code.
|
|
80
|
+
let joseHeader = b64url(`{"kid":"abc123","alg":"RS256"}`)
|
|
81
|
+
|
|
73
82
|
let jwt = claims =>
|
|
74
|
-
|
|
83
|
+
`${joseHeader}.${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
|
|
75
84
|
|
|
76
85
|
let localToken = claims =>
|
|
77
86
|
`${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
|
|
@@ -84,12 +93,41 @@ let clientWith = token => {
|
|
|
84
93
|
c
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
describe("Seed_Client.claims:", () => {
|
|
97
|
+
// The regression this file could not previously express. A JWT header is a
|
|
98
|
+
// decodable JSON object sitting in front of the payload, so "the first segment
|
|
99
|
+
// that parses" is the header on every real token — and the seed then reported
|
|
100
|
+
// no groups, printed no `Acting as:` line, and attached no identity to a
|
|
101
|
+
// refusal, which is exactly the diagnosis it exists to give.
|
|
102
|
+
testSync("skips the JOSE header and reads the payload behind it", () =>
|
|
103
|
+
expect(
|
|
104
|
+
clientWith(jwt([("sub", JSON.Encode.string("real-sub"))]))
|
|
105
|
+
->Seed_Client.claims
|
|
106
|
+
->Option.flatMap(c => c->Dict.get("sub"))
|
|
107
|
+
->Option.flatMap(JSON.Decode.string),
|
|
108
|
+
)->toEqual(Some("real-sub"))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// The header is identified by `alg`, not by position — a two-segment local dev
|
|
112
|
+
// token has no header at all, and its payload must not be mistaken for one.
|
|
113
|
+
testSync("takes the first segment when there is no header", () =>
|
|
114
|
+
expect(
|
|
115
|
+
clientWith(localToken([("userId", JSON.Encode.string("local-shopper"))]))
|
|
116
|
+
->Seed_Client.claims
|
|
117
|
+
->Option.flatMap(c => c->Dict.get("userId"))
|
|
118
|
+
->Option.flatMap(JSON.Decode.string),
|
|
119
|
+
)->toEqual(Some("local-shopper"))
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
87
123
|
describe("Seed_Client.effectiveGroups:", () => {
|
|
88
124
|
// A Cognito id token carries its payload in the second segment, a local dev
|
|
89
125
|
// token in the first. Neither platform should have to announce which it is.
|
|
90
126
|
testSync("reads the payload wherever the provider put it", () => {
|
|
91
127
|
expect(
|
|
92
|
-
clientWith(
|
|
128
|
+
clientWith(
|
|
129
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
130
|
+
)->Seed_Client.effectiveGroups,
|
|
93
131
|
)->toEqual(Some(["Admin", "Shopper"]))
|
|
94
132
|
expect(
|
|
95
133
|
clientWith(localToken([("groups", strings(["Merchandiser"]))]))->Seed_Client.effectiveGroups,
|
|
@@ -101,12 +139,64 @@ describe("Seed_Client.effectiveGroups:", () => {
|
|
|
101
139
|
testSync("says nothing about a token it cannot read", () => {
|
|
102
140
|
expect(clientWith("not-a-token")->Seed_Client.effectiveGroups)->toEqual(None)
|
|
103
141
|
expect(
|
|
104
|
-
Seed_Client.make(
|
|
105
|
-
|
|
142
|
+
Seed_Client.make(
|
|
143
|
+
~config={endpoint: "http://example.invalid/graphql"},
|
|
144
|
+
)->Seed_Client.effectiveGroups,
|
|
106
145
|
)->toEqual(None)
|
|
107
146
|
})
|
|
108
147
|
})
|
|
109
148
|
|
|
149
|
+
// The id an `@owner` field is stamped with. A seed that cannot read it can only
|
|
150
|
+
// key owner-scoped rows to a literal, and a literal matches one platform's
|
|
151
|
+
// accounts at most — which is how a demo shopper came to be registered on a
|
|
152
|
+
// deployment where nobody can ever be that shopper.
|
|
153
|
+
|
|
154
|
+
describe("Seed_Client.callerId:", () => {
|
|
155
|
+
// A Cognito id token says `sub`; the local dev token is a base64url
|
|
156
|
+
// `Identity.t`, whose field is `userId`. Neither platform announces which.
|
|
157
|
+
testSync("reads the id under whichever name the provider gave it", () => {
|
|
158
|
+
expect(
|
|
159
|
+
clientWith(jwt([("sub", JSON.Encode.string("4275e4a4-00c1-70e1"))]))->Seed_Client.callerId,
|
|
160
|
+
)->toEqual(Some("4275e4a4-00c1-70e1"))
|
|
161
|
+
expect(
|
|
162
|
+
clientWith(
|
|
163
|
+
localToken([("userId", JSON.Encode.string("local-shopper"))]),
|
|
164
|
+
)->Seed_Client.callerId,
|
|
165
|
+
)->toEqual(Some("local-shopper"))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// `sub` first, so a token carrying both is read the way the pool that minted
|
|
169
|
+
// it means it.
|
|
170
|
+
testSync("prefers sub when a token carries both names", () =>
|
|
171
|
+
expect(
|
|
172
|
+
clientWith(
|
|
173
|
+
jwt([
|
|
174
|
+
("sub", JSON.Encode.string("f215e4b4-30f1-700c")),
|
|
175
|
+
("userId", JSON.Encode.string("local-shopper")),
|
|
176
|
+
]),
|
|
177
|
+
)->Seed_Client.callerId,
|
|
178
|
+
)->toEqual(Some("f215e4b4-30f1-700c"))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// `None` is not an error — it means this cannot add anything, and the caller
|
|
182
|
+
// falls back to whatever the accounts file declared.
|
|
183
|
+
testSync("says nothing about a token it cannot read, or one naming no id", () => {
|
|
184
|
+
expect(clientWith("not-a-token")->Seed_Client.callerId)->toEqual(None)
|
|
185
|
+
expect(
|
|
186
|
+
clientWith(jwt([("cognito:groups", strings(["Admin"]))]))->Seed_Client.callerId,
|
|
187
|
+
)->toEqual(None)
|
|
188
|
+
expect(
|
|
189
|
+
Seed_Client.make(~config={endpoint: "http://example.invalid/graphql"})->Seed_Client.callerId,
|
|
190
|
+
)->toEqual(None)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// A claim of the wrong shape is not an id. Taking the array's first element
|
|
194
|
+
// would be a guess, and a guessed owner id seeds rows nobody can read.
|
|
195
|
+
testSync("refuses a claim that is not a single string", () =>
|
|
196
|
+
expect(clientWith(jwt([("sub", strings(["a", "b"]))]))->Seed_Client.callerId)->toEqual(None)
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
|
|
110
200
|
describe("Seed_Client.identitySummary:", () => {
|
|
111
201
|
// The case that cost the debugging: eligible in the user list, refused by the
|
|
112
202
|
// token. Naming what it was narrowed FROM is what points at the role switch.
|
|
@@ -124,8 +214,9 @@ describe("Seed_Client.identitySummary:", () => {
|
|
|
124
214
|
// An ordinary login is not narrowed and has nothing to explain.
|
|
125
215
|
testSync("stays quiet about an unnarrowed token", () =>
|
|
126
216
|
expect(
|
|
127
|
-
clientWith(
|
|
128
|
-
|
|
217
|
+
clientWith(
|
|
218
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
219
|
+
)->Seed_Client.identitySummary,
|
|
129
220
|
)->toEqual(Some("Admin, Shopper"))
|
|
130
221
|
)
|
|
131
222
|
|
|
@@ -137,6 +228,45 @@ describe("Seed_Client.identitySummary:", () => {
|
|
|
137
228
|
)
|
|
138
229
|
})
|
|
139
230
|
|
|
231
|
+
// Whether a token is narrowed cannot be read off the group claim: a bearer
|
|
232
|
+
// carrying one group looks identical whether the account holds one role or five.
|
|
233
|
+
// `availableRoles` is written only when a role was chosen, so its presence IS
|
|
234
|
+
// the narrowing — and that is what decides whether the seed has anything to ask.
|
|
235
|
+
|
|
236
|
+
describe("Seed_Client.narrowedFrom:", () => {
|
|
237
|
+
testSync("names the membership a narrowed token was reduced from", () =>
|
|
238
|
+
expect(
|
|
239
|
+
clientWith(
|
|
240
|
+
jwt([
|
|
241
|
+
("cognito:groups", strings(["Shopper"])),
|
|
242
|
+
("availableRoles", JSON.Encode.string("Admin,Shopper")),
|
|
243
|
+
]),
|
|
244
|
+
)->Seed_Client.narrowedFrom,
|
|
245
|
+
)->toEqual(Some(["Admin", "Shopper"]))
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// The case that must NOT prompt: a single-role account is not narrowed, it is
|
|
249
|
+
// simply small. Asking it to choose would put a question in every run.
|
|
250
|
+
testSync("says nothing about an unnarrowed token", () => {
|
|
251
|
+
expect(
|
|
252
|
+
clientWith(jwt([("cognito:groups", strings(["Shopper"]))]))->Seed_Client.narrowedFrom,
|
|
253
|
+
)->toEqual(None)
|
|
254
|
+
expect(
|
|
255
|
+
clientWith(
|
|
256
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
257
|
+
)->Seed_Client.narrowedFrom,
|
|
258
|
+
)->toEqual(None)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// An empty claim is not a narrowing to nothing — it is a claim carrying no
|
|
262
|
+
// information, and offering a menu of no roles would be a dead end.
|
|
263
|
+
testSync("treats an empty availableRoles as not narrowed", () =>
|
|
264
|
+
expect(
|
|
265
|
+
clientWith(jwt([("availableRoles", JSON.Encode.string(""))]))->Seed_Client.narrowedFrom,
|
|
266
|
+
)->toEqual(None)
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
|
|
140
270
|
describe("Seed_Client.isDenied:", () => {
|
|
141
271
|
// AppSync types it; the local server carries it in the message.
|
|
142
272
|
testSync("recognises a refusal in either platform's vocabulary", () => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
3
5
|
import * as Seed_Client$ReventlessSeed from "../src/Seed_Client.res.mjs";
|
|
4
6
|
|
|
5
7
|
function errors(entries) {
|
|
@@ -61,9 +63,11 @@ function b64url(payload) {
|
|
|
61
63
|
return Buffer.from(payload, "utf8").toString("base64url");
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
let joseHeader = Buffer.from(`{"kid":"abc123","alg":"RS256"}`, "utf8").toString("base64url");
|
|
67
|
+
|
|
64
68
|
function jwt(claims) {
|
|
65
69
|
let payload = JSON.stringify(Object.fromEntries(claims));
|
|
66
|
-
return
|
|
70
|
+
return joseHeader + `.` + Buffer.from(payload, "utf8").toString("base64url") + `.signature`;
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
function localToken(claims) {
|
|
@@ -83,6 +87,21 @@ function clientWith(token) {
|
|
|
83
87
|
return c;
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
globalThis.describe("Seed_Client.claims:", () => {
|
|
91
|
+
globalThis.test("skips the JOSE header and reads the payload behind it", () => {
|
|
92
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Seed_Client$ReventlessSeed.claims(clientWith(jwt([[
|
|
93
|
+
"sub",
|
|
94
|
+
"real-sub"
|
|
95
|
+
]]))), c => c["sub"]), Stdlib_JSON.Decode.string)).toEqual("real-sub");
|
|
96
|
+
});
|
|
97
|
+
globalThis.test("takes the first segment when there is no header", () => {
|
|
98
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Seed_Client$ReventlessSeed.claims(clientWith(localToken([[
|
|
99
|
+
"userId",
|
|
100
|
+
"local-shopper"
|
|
101
|
+
]]))), c => c["userId"]), Stdlib_JSON.Decode.string)).toEqual("local-shopper");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
86
105
|
globalThis.describe("Seed_Client.effectiveGroups:", () => {
|
|
87
106
|
globalThis.test("reads the payload wherever the provider put it", () => {
|
|
88
107
|
globalThis.expect(Seed_Client$ReventlessSeed.effectiveGroups(clientWith(jwt([[
|
|
@@ -108,6 +127,50 @@ globalThis.describe("Seed_Client.effectiveGroups:", () => {
|
|
|
108
127
|
});
|
|
109
128
|
});
|
|
110
129
|
|
|
130
|
+
globalThis.describe("Seed_Client.callerId:", () => {
|
|
131
|
+
globalThis.test("reads the id under whichever name the provider gave it", () => {
|
|
132
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
133
|
+
"sub",
|
|
134
|
+
"4275e4a4-00c1-70e1"
|
|
135
|
+
]])))).toEqual("4275e4a4-00c1-70e1");
|
|
136
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(localToken([[
|
|
137
|
+
"userId",
|
|
138
|
+
"local-shopper"
|
|
139
|
+
]])))).toEqual("local-shopper");
|
|
140
|
+
});
|
|
141
|
+
globalThis.test("prefers sub when a token carries both names", () => {
|
|
142
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([
|
|
143
|
+
[
|
|
144
|
+
"sub",
|
|
145
|
+
"f215e4b4-30f1-700c"
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"userId",
|
|
149
|
+
"local-shopper"
|
|
150
|
+
]
|
|
151
|
+
])))).toEqual("f215e4b4-30f1-700c");
|
|
152
|
+
});
|
|
153
|
+
globalThis.test("says nothing about a token it cannot read, or one naming no id", () => {
|
|
154
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith("not-a-token"))).toEqual(undefined);
|
|
155
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
156
|
+
"cognito:groups",
|
|
157
|
+
["Admin"].map(prim => prim)
|
|
158
|
+
]])))).toEqual(undefined);
|
|
159
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(Seed_Client$ReventlessSeed.make({
|
|
160
|
+
endpoint: "http://example.invalid/graphql"
|
|
161
|
+
}))).toEqual(undefined);
|
|
162
|
+
});
|
|
163
|
+
globalThis.test("refuses a claim that is not a single string", () => {
|
|
164
|
+
globalThis.expect(Seed_Client$ReventlessSeed.callerId(clientWith(jwt([[
|
|
165
|
+
"sub",
|
|
166
|
+
[
|
|
167
|
+
"a",
|
|
168
|
+
"b"
|
|
169
|
+
].map(prim => prim)
|
|
170
|
+
]])))).toEqual(undefined);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
111
174
|
globalThis.describe("Seed_Client.identitySummary:", () => {
|
|
112
175
|
globalThis.test("names the membership a narrowed token gave up", () => {
|
|
113
176
|
globalThis.expect(Seed_Client$ReventlessSeed.identitySummary(clientWith(jwt([
|
|
@@ -138,6 +201,43 @@ globalThis.describe("Seed_Client.identitySummary:", () => {
|
|
|
138
201
|
});
|
|
139
202
|
});
|
|
140
203
|
|
|
204
|
+
globalThis.describe("Seed_Client.narrowedFrom:", () => {
|
|
205
|
+
globalThis.test("names the membership a narrowed token was reduced from", () => {
|
|
206
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([
|
|
207
|
+
[
|
|
208
|
+
"cognito:groups",
|
|
209
|
+
["Shopper"].map(prim => prim)
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
"availableRoles",
|
|
213
|
+
"Admin,Shopper"
|
|
214
|
+
]
|
|
215
|
+
])))).toEqual([
|
|
216
|
+
"Admin",
|
|
217
|
+
"Shopper"
|
|
218
|
+
]);
|
|
219
|
+
});
|
|
220
|
+
globalThis.test("says nothing about an unnarrowed token", () => {
|
|
221
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
222
|
+
"cognito:groups",
|
|
223
|
+
["Shopper"].map(prim => prim)
|
|
224
|
+
]])))).toEqual(undefined);
|
|
225
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
226
|
+
"cognito:groups",
|
|
227
|
+
[
|
|
228
|
+
"Admin",
|
|
229
|
+
"Shopper"
|
|
230
|
+
].map(prim => prim)
|
|
231
|
+
]])))).toEqual(undefined);
|
|
232
|
+
});
|
|
233
|
+
globalThis.test("treats an empty availableRoles as not narrowed", () => {
|
|
234
|
+
globalThis.expect(Seed_Client$ReventlessSeed.narrowedFrom(clientWith(jwt([[
|
|
235
|
+
"availableRoles",
|
|
236
|
+
""
|
|
237
|
+
]])))).toEqual(undefined);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
141
241
|
globalThis.describe("Seed_Client.isDenied:", () => {
|
|
142
242
|
globalThis.test("recognises a refusal in either platform's vocabulary", () => {
|
|
143
243
|
let entries = [errorOfType("Unauthorized")];
|
|
@@ -159,6 +259,7 @@ export {
|
|
|
159
259
|
errors,
|
|
160
260
|
errorOfType,
|
|
161
261
|
b64url,
|
|
262
|
+
joseHeader,
|
|
162
263
|
jwt,
|
|
163
264
|
localToken,
|
|
164
265
|
strings,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|