@reventlessdev/reventless-spec 3.0.0-alpha.133 → 3.0.0-alpha.135
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 +33 -0
- package/package.json +4 -3
- package/run-check-lifecycle.mjs +2 -1
- package/run-prepare-accounts.mjs +3 -0
- package/src/AnsiStyle.res +0 -1
- package/src/LogPrefix.res +4 -13
- package/src/components/AutomationSlice.res +1 -5
- package/src/components/CapabilityManifest.res +6 -3
- package/src/components/CapabilityManifest.res.mjs +13 -2
- package/src/components/DcbDecode.res +6 -2
- package/src/components/DcbScopeInference.res +16 -12
- package/src/components/DcbTag.res +74 -46
- package/src/components/DcbValidation.res +114 -89
- package/src/components/DisplayName.res +4 -2
- package/src/components/ExtensionPoint.res +0 -1
- package/src/components/FieldDefault.res +2 -4
- package/src/components/InboundTranslationSlice.res +0 -2
- package/src/components/OutboundTranslationSlice.res +10 -8
- package/src/components/Owner.res +4 -4
- package/src/components/Plugin.res +10 -4
- package/src/components/ReadModel.res +0 -1
- package/src/components/Reference.res +2 -8
- package/src/components/Sensitive.res +4 -4
- package/src/components/StateAnnotations.res +4 -2
- package/src/components/StateChangeSlice.res +0 -2
- package/src/components/StateViewSlice.res +0 -2
- package/src/components/TaggedUnion.res +1 -2
- package/src/components/Task.res +0 -1
- package/src/components/TraitCertificate.res +2 -2
- package/src/components/TraitManifest.res +1 -1
- package/src/generator/CertifyTrait.res +2 -4
- package/src/generator/Codegen.res +77 -68
- package/src/generator/Discovery.res +29 -12
- package/src/generator/GraftTrait.res +17 -9
- package/src/generator/Pairing.res +39 -13
- package/src/generator/PlatformCodegen.res +14 -11
- package/src/generator/PlatformCodegen.res.mjs +5 -0
- package/src/generator/PlatformManifests.res +1 -3
- package/src/generator/PluginGenerator.res +0 -1
- package/src/generator/TraitManifestCli.res +7 -7
- package/src/lifecycle/CheckLifecycleModel.res +206 -148
- package/src/lifecycle/CheckLifecycleModel.res.mjs +23 -5
- package/src/semantic/Bytes.res +0 -1
- package/src/semantic/CalendarDate.res +2 -2
- package/src/semantic/Capabilities.res +21 -6
- package/src/semantic/Capabilities.res.mjs +18 -12
- package/src/semantic/CapabilityNeed.res +16 -5
- package/src/semantic/CapabilityNeed.res.mjs +9 -4
- package/src/semantic/CaptionedImage.res +0 -1
- package/src/semantic/Color.res +0 -1
- package/src/semantic/Currency.res +189 -170
- package/src/semantic/DateRange.res +3 -4
- package/src/semantic/DateTime.res +5 -5
- package/src/semantic/Duration.res +2 -2
- package/src/semantic/Email.res +0 -1
- package/src/semantic/FileRef.res +2 -2
- package/src/semantic/GeoPoint.res +17 -21
- package/src/semantic/Geocoding.res +8 -9
- package/src/semantic/Geolocation.res +6 -9
- package/src/semantic/IdentityProvider.res +127 -0
- package/src/semantic/IdentityProvider.res.mjs +65 -0
- package/src/semantic/ImageRef.res +2 -2
- package/src/semantic/MemberRef.res +0 -1
- package/src/semantic/Messaging.res +141 -21
- package/src/semantic/Messaging.res.mjs +51 -8
- package/src/semantic/Money.res +23 -20
- package/src/semantic/Offload.res +6 -4
- package/src/semantic/Percent.res +3 -3
- package/src/semantic/Phone.res +0 -1
- package/src/semantic/RowImage.res +4 -3
- package/src/semantic/Secrets.res +76 -0
- package/src/semantic/Secrets.res.mjs +49 -0
- package/src/semantic/Semantic.res +8 -11
- package/src/semantic/StorageRef.res +17 -17
- package/src/semantic/Template.res +5 -7
- package/src/semantic/UploadableFile.res +0 -1
- package/src/semantic/UploadableImage.res +0 -1
- package/src/semantic/Url.res +3 -3
- package/src/types/AccountsManifest.res +281 -0
- package/src/types/AccountsManifest.res.mjs +311 -0
- package/src/types/AdminGroup.res +27 -0
- package/src/types/AdminGroup.res.mjs +9 -0
- package/src/types/Authorization.res +1 -2
- package/src/types/Handler.res +0 -1
- package/src/types/Identity.res +1 -3
- package/src/types/Lifecycle.res +7 -6
- package/src/types/Message.res +6 -4
- package/src/types/OwnerScope.res +28 -3
- package/src/types/OwnerScope.res.mjs +12 -0
- package/src/types/PrepareAccounts.res +119 -0
- package/src/types/PrepareAccounts.res.mjs +148 -0
- package/src/types/Projection.res +9 -2
- package/src/types/StoredEvent.res +2 -4
- package/src/types/Transition.res +8 -4
- package/src/util/Util_Password.res +60 -0
- package/src/util/Util_Password.res.mjs +53 -0
- package/src/util/Util_Sury.res +1 -4
package/src/types/Message.res
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
/** Identifies the service that produced or is addressed by a message. */
|
|
3
2
|
@schema
|
|
4
3
|
type service = string
|
|
@@ -179,7 +178,11 @@ type commandJson = {
|
|
|
179
178
|
//
|
|
180
179
|
// `scalarFills` is an out-parameter: the walker pushes `path := value` for each scalar it
|
|
181
180
|
// invented.
|
|
182
|
-
let fillMissingDefaults: (
|
|
181
|
+
let fillMissingDefaults: (
|
|
182
|
+
S.t<'a>,
|
|
183
|
+
JSON.t,
|
|
184
|
+
array<string>,
|
|
185
|
+
) => JSON.t = %raw(`function(schema, json, scalarFills){
|
|
183
186
|
function isSchema(x){ return x && typeof x === "object" && typeof x.type === "string"; }
|
|
184
187
|
function firstConst(anyOf){ var m=(anyOf||[]).find(function(s){return s.const!==undefined;}); return m ? m.const : undefined; }
|
|
185
188
|
// Which object-typed member of an anyOf a plain (untagged) object value belongs to.
|
|
@@ -274,8 +277,7 @@ let parseJsonTolerant = (json, schema) =>
|
|
|
274
277
|
->Array.length
|
|
275
278
|
->Int.toString} missing scalar field(s): ${scalarFills->Array.join(
|
|
276
279
|
", ",
|
|
277
|
-
)}. A required scalar was added to a persisted type after this message was ` ++
|
|
278
|
-
`written; the value above is fabricated, not recovered. Prefer an optional field.`,
|
|
280
|
+
)}. A required scalar was added to a persisted type after this message was ` ++ `written; the value above is fabricated, not recovered. Prefer an optional field.`,
|
|
279
281
|
)
|
|
280
282
|
}
|
|
281
283
|
value
|
package/src/types/OwnerScope.res
CHANGED
|
@@ -20,7 +20,6 @@ fields this module reads are typed non-optional and are, at runtime, sometimes
|
|
|
20
20
|
absent. Every read here goes through a nullable cast for that reason; deleting
|
|
21
21
|
one restores a silent failure rather than a type error.
|
|
22
22
|
*/
|
|
23
|
-
|
|
24
23
|
/** Whether a JS value is a primitive string — `Cognito` and `InMemory` compile to
|
|
25
24
|
bare strings while `Custom(_)` compiles to an object, so this is how a modelled
|
|
26
25
|
provider is told from an unmodelled one that arrived as raw JSON. */
|
|
@@ -95,6 +94,33 @@ let setElevatedGroups = (groups: array<string>) => explicitElevatedGroups := Som
|
|
|
95
94
|
/** Forget an explicit setting and fall back to the environment again. */
|
|
96
95
|
let clearElevatedGroups = () => explicitElevatedGroups := None
|
|
97
96
|
|
|
97
|
+
/**
|
|
98
|
+
Name the elevated groups only where the deployment has not already answered.
|
|
99
|
+
|
|
100
|
+
For a platform that wants a working default without overriding an operator who
|
|
101
|
+
stated one — a stack that declares an administrator group has every reason to
|
|
102
|
+
exempt it, and no business overruling a deployment that named different operators.
|
|
103
|
+
|
|
104
|
+
"Already answered" includes the **environment**, not only an explicit call. A
|
|
105
|
+
deployment that set `REVENTLESS_ELEVATED_GROUPS` in CI has named its operators,
|
|
106
|
+
and a default that beat it would leave the runtime acting on a value no source in
|
|
107
|
+
that deployment states. This is the same contract, one level up, that
|
|
108
|
+
`Util_OwnerScopeEnv.applyElevatedGroupsDefault` keeps with a Lambda's own
|
|
109
|
+
environment: the more specific statement wins, and the default only fills silence.
|
|
110
|
+
|
|
111
|
+
🚨 **An explicit empty list is an answer, and this must not fill it in.** A
|
|
112
|
+
deployment that deliberately elevates nobody has decided that; a default landing
|
|
113
|
+
on top would silently re-grant the cross-owner read it withheld. That is why this
|
|
114
|
+
reads [explicitElevatedGroups] rather than [elevatedGroups] — the latter answers
|
|
115
|
+
`[]` for "nobody" and for "nothing said" alike, and those must not be treated the
|
|
116
|
+
same here.
|
|
117
|
+
*/
|
|
118
|
+
let defaultElevatedGroups = (groups: array<string>) =>
|
|
119
|
+
switch (explicitElevatedGroups.contents, _elevatedGroupsEnv) {
|
|
120
|
+
| (None, None) => explicitElevatedGroups := Some(groups)
|
|
121
|
+
| _ => ()
|
|
122
|
+
}
|
|
123
|
+
|
|
98
124
|
let parseElevatedGroups = (raw: string): array<string> =>
|
|
99
125
|
raw
|
|
100
126
|
->String.split(",")
|
|
@@ -135,8 +161,7 @@ let classify = (identity: Identity.t, ~elevated: array<string>): t => {
|
|
|
135
161
|
switch identity.userId->asNullableString->Nullable.toOption {
|
|
136
162
|
| None => Unidentified("identity carries no userId")
|
|
137
163
|
| Some("") => Unidentified("identity carries an empty userId")
|
|
138
|
-
| Some(userId) if userId == Identity.anonymous.userId =>
|
|
139
|
-
Unidentified("caller is anonymous")
|
|
164
|
+
| Some(userId) if userId == Identity.anonymous.userId => Unidentified("caller is anonymous")
|
|
140
165
|
| Some(userId) =>
|
|
141
166
|
let groups = identity.groups->asNullableArray->Nullable.toOption->Option.getOr([])
|
|
142
167
|
groups->Array.some(g => elevated->Array.includes(g))
|
|
@@ -21,6 +21,17 @@ function clearElevatedGroups() {
|
|
|
21
21
|
explicitElevatedGroups.contents = undefined;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function defaultElevatedGroups(groups) {
|
|
25
|
+
let match = explicitElevatedGroups.contents;
|
|
26
|
+
let match$1 = process.env.REVENTLESS_ELEVATED_GROUPS;
|
|
27
|
+
if (match !== undefined || match$1 !== undefined) {
|
|
28
|
+
return;
|
|
29
|
+
} else {
|
|
30
|
+
explicitElevatedGroups.contents = groups;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
function parseElevatedGroups(raw) {
|
|
25
36
|
return raw.split(",").map(prim => prim.trim()).filter(part => part.length > 0);
|
|
26
37
|
}
|
|
@@ -226,6 +237,7 @@ export {
|
|
|
226
237
|
explicitElevatedGroups,
|
|
227
238
|
setElevatedGroups,
|
|
228
239
|
clearElevatedGroups,
|
|
240
|
+
defaultElevatedGroups,
|
|
229
241
|
parseElevatedGroups,
|
|
230
242
|
elevatedGroups,
|
|
231
243
|
classify,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Make a platform's accounts manifest ready to use.
|
|
3
|
+
|
|
4
|
+
```
|
|
5
|
+
pnpm exec prepare-accounts
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Finds `.reventless/users.yaml` — copying the package's committed
|
|
9
|
+
`users.example.yaml` into place when there is none — validates it, and generates a
|
|
10
|
+
password into every field left empty.
|
|
11
|
+
|
|
12
|
+
🚨 **On the local platform this is the whole of provisioning.** The manifest *is*
|
|
13
|
+
the user store there: the in-memory auth adapter hydrates from it at startup, so
|
|
14
|
+
once every account has a password there is nothing left to create. On AWS the
|
|
15
|
+
accounts still have to be made in a pool, which is `provision-accounts` in
|
|
16
|
+
`reventless-aws`.
|
|
17
|
+
|
|
18
|
+
Here, rather than as a flag on that command, because it belongs to neither
|
|
19
|
+
platform. A local developer reaching it through an AWS package's binary would be
|
|
20
|
+
the only thing about this half that mentioned AWS at all — and the local platform
|
|
21
|
+
does not depend on `reventless-aws`, so for a project that never deploys it would
|
|
22
|
+
not resolve.
|
|
23
|
+
|
|
24
|
+
It creates nothing, contacts nothing, and needs no credentials. Re-running is safe:
|
|
25
|
+
a password already in the file is never replaced.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type args = {
|
|
29
|
+
file: option<string>,
|
|
30
|
+
help: bool,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let parseArgs = (argv: array<string>): result<args, string> => {
|
|
34
|
+
let acc = ref(Ok({file: None, help: false}))
|
|
35
|
+
let i = ref(0)
|
|
36
|
+
let count = argv->Array.length
|
|
37
|
+
while i.contents < count {
|
|
38
|
+
let flag = argv->Array.getUnsafe(i.contents)
|
|
39
|
+
let value = argv->Array.get(i.contents + 1)
|
|
40
|
+
switch (acc.contents, flag, value) {
|
|
41
|
+
| (Error(_), _, _) => i := count
|
|
42
|
+
| (Ok(a), "--file", Some(v)) =>
|
|
43
|
+
acc := Ok({...a, file: Some(v)})
|
|
44
|
+
i := i.contents + 2
|
|
45
|
+
| (Ok(a), "--help", _) | (Ok(a), "-h", _) =>
|
|
46
|
+
acc := Ok({...a, help: true})
|
|
47
|
+
i := i.contents + 1
|
|
48
|
+
| (Ok(_), "--file", None) => acc := Error(`${flag} needs a value`)
|
|
49
|
+
| (Ok(_), unknown, _) => acc := Error(`unknown argument "${unknown}"`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
acc.contents
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let usage = `
|
|
56
|
+
Make a platform's accounts manifest ready to use.
|
|
57
|
+
|
|
58
|
+
--file <path> The manifest. Defaults to .reventless/users.yaml relative to the
|
|
59
|
+
working directory, which is where every platform keeps it. A
|
|
60
|
+
named path is read, never created.
|
|
61
|
+
|
|
62
|
+
Copies ${AccountsManifest.templateName} into place when there is no manifest yet,
|
|
63
|
+
then generates a password into every empty field. Creates no accounts and needs no
|
|
64
|
+
credentials — on the local platform that is all provisioning is. A password already
|
|
65
|
+
in the file is never replaced.
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
/** The cast, as the file now declares it. Printed on a fresh copy because that is
|
|
69
|
+
the moment somebody should read it: on AWS the next command turns each of these
|
|
70
|
+
into a real account in a real pool. */
|
|
71
|
+
let castNote = (entries: array<AccountsManifest.entry>): string =>
|
|
72
|
+
entries
|
|
73
|
+
->Array.map(entry =>
|
|
74
|
+
` ${entry.username}${entry.groups->Array.length == 0
|
|
75
|
+
? ""
|
|
76
|
+
: ` [${entry.groups->Array.join(", ")}]`}`
|
|
77
|
+
)
|
|
78
|
+
->Array.join("\n")
|
|
79
|
+
|
|
80
|
+
let run = (): result<unit, string> =>
|
|
81
|
+
// argv[0] is node, argv[1] this script.
|
|
82
|
+
switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
|
|
83
|
+
| Error(_) as e => e
|
|
84
|
+
| Ok(args) if args.help =>
|
|
85
|
+
Console.log(usage)
|
|
86
|
+
Ok()
|
|
87
|
+
| Ok(args) =>
|
|
88
|
+
switch AccountsManifest.locate(~given=?args.file, ()) {
|
|
89
|
+
| Error(_) as e => e
|
|
90
|
+
| Ok(located) =>
|
|
91
|
+
let file = located->AccountsManifest.pathOf
|
|
92
|
+
switch located {
|
|
93
|
+
| SeededFrom(_, template) => Console.log(`manifest ${file} (new, copied from ${template})`)
|
|
94
|
+
| Declared(_) => Console.log(`manifest ${file}`)
|
|
95
|
+
}
|
|
96
|
+
switch AccountsManifest.prepare(~path=file) {
|
|
97
|
+
| Error(message) => Error(`${file}: ${message}`)
|
|
98
|
+
| Ok([]) => Error(`${file} declares no accounts`)
|
|
99
|
+
| Ok(prepared) =>
|
|
100
|
+
let generated = prepared->Array.filter(p => p.passwordGenerated)->Array.length
|
|
101
|
+
Console.log(
|
|
102
|
+
`accounts ${prepared
|
|
103
|
+
->Array.length
|
|
104
|
+
->Int.toString} declared, ${generated->Int.toString} password(s) generated\n\n${castNote(
|
|
105
|
+
prepared->Array.map(p => p.entry),
|
|
106
|
+
)}`,
|
|
107
|
+
)
|
|
108
|
+
Ok()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let main = async () =>
|
|
114
|
+
switch run() {
|
|
115
|
+
| Ok() => ()
|
|
116
|
+
| Error(message) =>
|
|
117
|
+
Console.error(`prepare-accounts: ${message}`)
|
|
118
|
+
NodeProcess.exit(1)
|
|
119
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as AccountsManifest$Reventless from "./AccountsManifest.res.mjs";
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
let acc = {
|
|
7
|
+
TAG: "Ok",
|
|
8
|
+
_0: {
|
|
9
|
+
file: undefined,
|
|
10
|
+
help: false
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
let i = 0;
|
|
14
|
+
let count = argv.length;
|
|
15
|
+
while (i < count) {
|
|
16
|
+
let flag = argv[i];
|
|
17
|
+
let value = argv[i + 1 | 0];
|
|
18
|
+
let match = acc;
|
|
19
|
+
if (match.TAG === "Ok") {
|
|
20
|
+
let a = match._0;
|
|
21
|
+
let exit = 0;
|
|
22
|
+
switch (flag) {
|
|
23
|
+
case "--file" :
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
acc = {
|
|
26
|
+
TAG: "Ok",
|
|
27
|
+
_0: {
|
|
28
|
+
file: value,
|
|
29
|
+
help: a.help
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
i = i + 2 | 0;
|
|
33
|
+
} else {
|
|
34
|
+
acc = {
|
|
35
|
+
TAG: "Error",
|
|
36
|
+
_0: flag + ` needs a value`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case "--help" :
|
|
41
|
+
case "-h" :
|
|
42
|
+
exit = 1;
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
acc = {
|
|
46
|
+
TAG: "Error",
|
|
47
|
+
_0: `unknown argument "` + flag + `"`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (exit === 1) {
|
|
51
|
+
acc = {
|
|
52
|
+
TAG: "Ok",
|
|
53
|
+
_0: {
|
|
54
|
+
file: a.file,
|
|
55
|
+
help: true
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
i = i + 1 | 0;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
i = count;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return acc;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let usage = `
|
|
68
|
+
Make a platform's accounts manifest ready to use.
|
|
69
|
+
|
|
70
|
+
--file <path> The manifest. Defaults to .reventless/users.yaml relative to the
|
|
71
|
+
working directory, which is where every platform keeps it. A
|
|
72
|
+
named path is read, never created.
|
|
73
|
+
|
|
74
|
+
Copies ` + AccountsManifest$Reventless.templateName + ` into place when there is no manifest yet,
|
|
75
|
+
then generates a password into every empty field. Creates no accounts and needs no
|
|
76
|
+
credentials — on the local platform that is all provisioning is. A password already
|
|
77
|
+
in the file is never replaced.
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
function castNote(entries) {
|
|
81
|
+
return entries.map(entry => ` ` + entry.username + (
|
|
82
|
+
entry.groups.length === 0 ? "" : ` [` + entry.groups.join(", ") + `]`
|
|
83
|
+
)).join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function run() {
|
|
87
|
+
let e = parseArgs(process.argv.slice(2, process.argv.length));
|
|
88
|
+
if (e.TAG !== "Ok") {
|
|
89
|
+
return e;
|
|
90
|
+
}
|
|
91
|
+
let args = e._0;
|
|
92
|
+
if (args.help) {
|
|
93
|
+
console.log(usage);
|
|
94
|
+
return {
|
|
95
|
+
TAG: "Ok",
|
|
96
|
+
_0: undefined
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
|
|
100
|
+
if (e$1.TAG !== "Ok") {
|
|
101
|
+
return e$1;
|
|
102
|
+
}
|
|
103
|
+
let located = e$1._0;
|
|
104
|
+
let file = AccountsManifest$Reventless.pathOf(located);
|
|
105
|
+
if (located.TAG === "Declared") {
|
|
106
|
+
console.log(`manifest ` + file);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
|
|
109
|
+
}
|
|
110
|
+
let message = AccountsManifest$Reventless.prepare(file);
|
|
111
|
+
if (message.TAG !== "Ok") {
|
|
112
|
+
return {
|
|
113
|
+
TAG: "Error",
|
|
114
|
+
_0: file + `: ` + message._0
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
let prepared = message._0;
|
|
118
|
+
if (prepared.length === 0) {
|
|
119
|
+
return {
|
|
120
|
+
TAG: "Error",
|
|
121
|
+
_0: file + ` declares no accounts`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
let generated = prepared.filter(p => p.passwordGenerated).length;
|
|
125
|
+
console.log(`accounts ` + prepared.length.toString() + ` declared, ` + generated.toString() + ` password(s) generated\n\n` + castNote(prepared.map(p => p.entry)));
|
|
126
|
+
return {
|
|
127
|
+
TAG: "Ok",
|
|
128
|
+
_0: undefined
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
let message = run();
|
|
134
|
+
if (message.TAG === "Ok") {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
console.error(`prepare-accounts: ` + message._0);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
parseArgs,
|
|
143
|
+
usage,
|
|
144
|
+
castNote,
|
|
145
|
+
run,
|
|
146
|
+
main,
|
|
147
|
+
}
|
|
148
|
+
/* AccountsManifest-Reventless Not a pure module */
|
package/src/types/Projection.res
CHANGED
|
@@ -145,8 +145,15 @@ module Mapping = {
|
|
|
145
145
|
module Make = (
|
|
146
146
|
Source: Source,
|
|
147
147
|
Target: Target,
|
|
148
|
-
MappingImpl: MappingImpl
|
|
149
|
-
|
|
148
|
+
MappingImpl: MappingImpl
|
|
149
|
+
with type sourceEvent := Source.event
|
|
150
|
+
and type targetState := Target.state,
|
|
151
|
+
): (
|
|
152
|
+
Mapping
|
|
153
|
+
with type targetState = Target.state
|
|
154
|
+
and type sourceEvent = Source.event
|
|
155
|
+
and module SourceId = Source.Id
|
|
156
|
+
) => {
|
|
150
157
|
module SourceId = Source.Id
|
|
151
158
|
@schema
|
|
152
159
|
type sourceEvent = Source.event
|
|
@@ -38,9 +38,7 @@ let toStoredEventSchema = (idSchema: S.t<'id>): S.t<storedEvent<'id>> =>
|
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
/** Decode a `storedEvent<'id>` from JSON. */
|
|
41
|
-
let decode = (json, idSchema) =>
|
|
42
|
-
json->Util_Sury.fromJson(toStoredEventSchema(idSchema))
|
|
41
|
+
let decode = (json, idSchema) => json->Util_Sury.fromJson(toStoredEventSchema(idSchema))
|
|
43
42
|
|
|
44
43
|
/** Encode a `storedEvent<'id>` to JSON. */
|
|
45
|
-
let encode = (stored, idSchema) =>
|
|
46
|
-
stored->Util_Sury.toJson(toStoredEventSchema(idSchema))
|
|
44
|
+
let encode = (stored, idSchema) => stored->Util_Sury.toJson(toStoredEventSchema(idSchema))
|
package/src/types/Transition.res
CHANGED
|
@@ -69,9 +69,11 @@ let allowedStates = (transition: t<'state>): option<array<'state>> =>
|
|
|
69
69
|
switch transition {
|
|
70
70
|
| Undeclared
|
|
71
71
|
| Unrestricted
|
|
72
|
-
| Creates(_) =>
|
|
72
|
+
| Creates(_) =>
|
|
73
|
+
None
|
|
73
74
|
| Guards(states)
|
|
74
|
-
| Moves(states, _) =>
|
|
75
|
+
| Moves(states, _) =>
|
|
76
|
+
Some(states)
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** The state the command's handler writes, or `None` for one that moves nothing. */
|
|
@@ -79,9 +81,11 @@ let targetState = (transition: t<'state>): option<'state> =>
|
|
|
79
81
|
switch transition {
|
|
80
82
|
| Undeclared
|
|
81
83
|
| Unrestricted
|
|
82
|
-
| Guards(_) =>
|
|
84
|
+
| Guards(_) =>
|
|
85
|
+
None
|
|
83
86
|
| Creates(state)
|
|
84
|
-
| Moves(_, state) =>
|
|
87
|
+
| Moves(_, state) =>
|
|
88
|
+
Some(state)
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/** Whether the command is claimed legal in every state, as opposed to nothing
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/***
|
|
2
|
+
A password a Reventless pool will accept, minted rather than chosen.
|
|
3
|
+
|
|
4
|
+
Here rather than beside the AWS provisioning script because two callers need it
|
|
5
|
+
and only one of them is AWS: `provision-admin` mints the first administrator's
|
|
6
|
+
credential, and [AccountsManifest.prepare] fills a manifest's empty password
|
|
7
|
+
fields on any platform, with no cloud account involved. A generator living in
|
|
8
|
+
`reventless/aws` would make the platform-neutral half reach into the AWS package
|
|
9
|
+
for arithmetic that has nothing to do with AWS.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Excludes the glyphs a person confuses when retyping a printed credential —
|
|
13
|
+
`l`/`I`/`1` and `O`/`0`. These passwords are read off a terminal or out of a
|
|
14
|
+
file once, and a bootstrap that fails on a misread character sends the operator
|
|
15
|
+
back to the console this exists to avoid. */
|
|
16
|
+
let lower = "abcdefghijkmnopqrstuvwxyz"
|
|
17
|
+
let upper = "ABCDEFGHJKLMNPQRSTUVWXYZ"
|
|
18
|
+
let digits = "23456789"
|
|
19
|
+
let alphabet = lower ++ upper ++ digits
|
|
20
|
+
|
|
21
|
+
/** Comfortably over the 12 every Reventless pool is created with, since nobody
|
|
22
|
+
has to remember it. */
|
|
23
|
+
let length = 24
|
|
24
|
+
|
|
25
|
+
/** `count` uniform bytes as ints.
|
|
26
|
+
|
|
27
|
+
Via the hex encoding because that is what [NodeCrypto] exposes of a Buffer, and
|
|
28
|
+
a binding returning raw byte values would be a new one added for this alone.
|
|
29
|
+
Two hex digits always parse, so the fallback below is unreachable — it is there
|
|
30
|
+
because the parse is *typed* as partial, not because it can fail. */
|
|
31
|
+
let randomInts = (count: int): array<int> => {
|
|
32
|
+
let hex = NodeCrypto.randomBytes(count)->NodeCrypto.bufferToString("hex")
|
|
33
|
+
Array.fromInitializer(~length=count, i =>
|
|
34
|
+
hex->String.substring(~start=i * 2, ~end=i * 2 + 2)->Int.fromString(~radix=16)->Option.getOr(0)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let charAt = (source: string, n: int): string =>
|
|
39
|
+
source->String.charAt(mod(n, source->String.length))
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
A password that satisfies the pool policy every Reventless pool is created with
|
|
43
|
+
(12+ characters, with a lowercase, an uppercase and a digit).
|
|
44
|
+
|
|
45
|
+
Satisfies it *by construction*: the three required classes are written into three
|
|
46
|
+
non-overlapping thirds of the string, so one of each is always present. The
|
|
47
|
+
obvious alternative — generate, test, regenerate — would leave a branch that
|
|
48
|
+
almost never runs and, when it did, would hand Cognito a password it refuses.
|
|
49
|
+
Almost-never is exactly the branch nobody has tested.
|
|
50
|
+
*/
|
|
51
|
+
let generate = (): string => {
|
|
52
|
+
let ints = randomInts(length + 6)
|
|
53
|
+
let chars = ints->Array.slice(~start=0, ~end=length)->Array.map(n => alphabet->charAt(n))
|
|
54
|
+
let third = length / 3
|
|
55
|
+
[lower, upper, digits]->Array.forEachWithIndex((classAlphabet, block) => {
|
|
56
|
+
let position = block * third + mod(ints->Array.getUnsafe(length + block), third)
|
|
57
|
+
chars->Array.set(position, classAlphabet->charAt(ints->Array.getUnsafe(length + 3 + block)))
|
|
58
|
+
})
|
|
59
|
+
chars->Array.join("")
|
|
60
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as Nodecrypto from "node:crypto";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
7
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
|
|
9
|
+
let lower = "abcdefghijkmnopqrstuvwxyz";
|
|
10
|
+
|
|
11
|
+
let upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
12
|
+
|
|
13
|
+
let digits = "23456789";
|
|
14
|
+
|
|
15
|
+
let alphabet = lower + upper + digits;
|
|
16
|
+
|
|
17
|
+
function randomInts(count) {
|
|
18
|
+
let hex = Nodecrypto.randomBytes(count).toString("hex");
|
|
19
|
+
return Stdlib_Array.fromInitializer(count, i => Stdlib_Option.getOr(Stdlib_Int.fromString(hex.substring((i << 1), (i << 1) + 2 | 0), 16), 0));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function charAt(source, n) {
|
|
23
|
+
return source.charAt(Primitive_int.mod_(n, source.length));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generate() {
|
|
27
|
+
let ints = randomInts(30);
|
|
28
|
+
let chars = ints.slice(0, 24).map(n => charAt(alphabet, n));
|
|
29
|
+
let third = 8;
|
|
30
|
+
[
|
|
31
|
+
lower,
|
|
32
|
+
upper,
|
|
33
|
+
digits
|
|
34
|
+
].forEach((classAlphabet, block) => {
|
|
35
|
+
let position = (block * third | 0) + Primitive_int.mod_(ints[24 + block | 0], third) | 0;
|
|
36
|
+
chars[position] = charAt(classAlphabet, ints[27 + block | 0]);
|
|
37
|
+
});
|
|
38
|
+
return chars.join("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let length = 24;
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
lower,
|
|
45
|
+
upper,
|
|
46
|
+
digits,
|
|
47
|
+
alphabet,
|
|
48
|
+
length,
|
|
49
|
+
randomInts,
|
|
50
|
+
charAt,
|
|
51
|
+
generate,
|
|
52
|
+
}
|
|
53
|
+
/* node:crypto Not a pure module */
|
package/src/util/Util_Sury.res
CHANGED
|
@@ -11,10 +11,7 @@ let toJson: ('a, S.t<'a>) => JSON.t = (value, schema) =>
|
|
|
11
11
|
|
|
12
12
|
// `~space` defaults to compact, matching the removed `reverseConvertToJsonStringOrThrow`.
|
|
13
13
|
let toJsonString = (value: 'a, schema: S.t<'a>, ~space: int=0): string =>
|
|
14
|
-
value->S.decodeOrThrow(
|
|
15
|
-
~from=schema,
|
|
16
|
-
~to=space == 0 ? S.jsonString : S.jsonStringWithSpace(space),
|
|
17
|
-
)
|
|
14
|
+
value->S.decodeOrThrow(~from=schema, ~to=space == 0 ? S.jsonString : S.jsonStringWithSpace(space))
|
|
18
15
|
|
|
19
16
|
let fromJson: (JSON.t, S.t<'a>) => 'a = (json, schema) => json->S.parseOrThrow(~to=schema)
|
|
20
17
|
|