@reventlessdev/reventless-spec 3.0.0-alpha.134 → 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.
@@ -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 */
@@ -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 */