arcadible 1.0.2 → 1.0.5
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/bin/arc-build.js +7 -22
- package/bin/arc-default.js +8 -8
- package/bin/arc-deploy.js +20 -22
- package/bin/arc-init.js +4 -3
- package/bin/arc-login.js +2 -2
- package/bin/arc-pack.js +8 -22
- package/bin/arc-play.js +1 -1
- package/bin/arc-serve.js +4 -3
- package/bin/arc-validate.js +2 -1
- package/bin/arc-whoami.js +1 -1
- package/package.json +1 -1
- package/src/account.js +3 -1
- package/src/action/build/tool.js +7 -3
- package/src/action/deploy/tool-git.js +8 -3
- package/src/action/deploy/tool.js +5 -4
- package/src/action/init/tool.js +11 -3
- package/src/action/pack/tool.js +11 -4
- package/src/action/predeploy/ac.js +4 -1
- package/src/action/predeploy/git.js +2 -2
- package/src/action/serve/tool.js +19 -3
- package/src/action/validate/checked.js +37 -0
- package/src/api.js +28 -13
- package/src/config/code.js +2 -2
- package/src/parser/readme.js +1 -1
- package/src/phrase.js +123 -62
- package/src/schema/game.js +32 -20
- package/src/schema/readme.js +2 -1
- package/src/schema/workflow.js +8 -10
- package/src/serve/game.js +7 -1
- package/src/serve/page.js +13 -13
- package/src/serve/reload.js +5 -0
- package/src/util/log.js +9 -2
- package/src/util/md.js +1 -1
- package/src/util/output.js +17 -2
- package/src/util/rc.js +23 -1
- package/src/util/s3.js +22 -3
- package/src/util/schema.js +53 -1
package/src/schema/game.js
CHANGED
|
@@ -123,16 +123,20 @@ const GALLERY_DIMENSIONS = {
|
|
|
123
123
|
// leaving a field out is how to unset it, so an empty one is always a mistake.
|
|
124
124
|
const atLeast = function (field, min, unit) {
|
|
125
125
|
return min === 1
|
|
126
|
-
? message(field, ["Must not be empty. Leave the field out instead."])
|
|
127
|
-
: message(field, [
|
|
126
|
+
? message(field, ["Must not be empty.", "Leave the field out instead."])
|
|
127
|
+
: message(field, [
|
|
128
|
+
unit === "character"
|
|
129
|
+
? `Must be at least ${min} characters.`
|
|
130
|
+
: `Must have at least ${min} ${unit}s.`,
|
|
131
|
+
]);
|
|
128
132
|
};
|
|
129
133
|
|
|
130
134
|
// [function]
|
|
131
135
|
|
|
132
|
-
// a required string's error: "Must be
|
|
136
|
+
// a required string's error: "Must be set." and `lines` when it's missing,
|
|
133
137
|
// and zod's own type message (null) when it's there but isn't a string.
|
|
134
138
|
const required = function (field, lines) {
|
|
135
|
-
const missing = message(field, ["Must be
|
|
139
|
+
const missing = message(field, ["Must be set.", ...lines]).message;
|
|
136
140
|
return {
|
|
137
141
|
error(issue) {
|
|
138
142
|
return typeof issue.input === "undefined" ? missing : null;
|
|
@@ -144,14 +148,14 @@ const required = function (field, lines) {
|
|
|
144
148
|
|
|
145
149
|
// a list of distinct values. Without a max, its length is up to the values.
|
|
146
150
|
const zSet = function (field, schema, { max, min }) {
|
|
147
|
-
const list = z.array(schema).min(min, atLeast(field, min, "
|
|
151
|
+
const list = z.array(schema).min(min, atLeast(field, min, "item"));
|
|
148
152
|
return (
|
|
149
153
|
max
|
|
150
|
-
? list.max(max, message(field, [`Must
|
|
154
|
+
? list.max(max, message(field, [`Must have at most ${max} items.`]))
|
|
151
155
|
: list
|
|
152
156
|
).refine(
|
|
153
157
|
(items) => new Set(items).size === items.length,
|
|
154
|
-
message(field, ["Must not
|
|
158
|
+
message(field, ["Must not repeat an item."], ISSUE.DUPLICATE),
|
|
155
159
|
);
|
|
156
160
|
};
|
|
157
161
|
|
|
@@ -161,12 +165,12 @@ const zSetBasefile = function (field, schema, { max, min }) {
|
|
|
161
165
|
return z
|
|
162
166
|
.array(schema)
|
|
163
167
|
.min(min, atLeast(field, min, "file"))
|
|
164
|
-
.max(max, message(field, [`Must
|
|
168
|
+
.max(max, message(field, [`Must have at most ${max} files.`]))
|
|
165
169
|
.refine(
|
|
166
170
|
(items) => new Set(items.map(basefile)).size === items.length,
|
|
167
171
|
message(
|
|
168
172
|
field,
|
|
169
|
-
["Must not
|
|
173
|
+
["Must not have two files of one name, whatever their extensions."],
|
|
170
174
|
ISSUE.DUPLICATE,
|
|
171
175
|
),
|
|
172
176
|
);
|
|
@@ -183,7 +187,7 @@ const zString = function (field, char, { max, min }) {
|
|
|
183
187
|
.min(min, { ...atLeast(field, min, "character"), abort: min === 1 })
|
|
184
188
|
.refine(
|
|
185
189
|
(str) => [...str].length <= max,
|
|
186
|
-
message(field, [`Must
|
|
190
|
+
message(field, [`Must be at most ${max} characters.`], ISSUE.TOO_LONG),
|
|
187
191
|
)
|
|
188
192
|
.superRefine((str, ctx) => {
|
|
189
193
|
const invalid = invalidChars(str, char);
|
|
@@ -458,7 +462,7 @@ const zHandle = function () {
|
|
|
458
462
|
const field = "handle";
|
|
459
463
|
return z.optional(
|
|
460
464
|
zString(field, /[a-z0-9-]/u, { max: 24, min: 1 })
|
|
461
|
-
.min(3, atLeast(field, 3, "
|
|
465
|
+
.min(3, atLeast(field, 3, "character"))
|
|
462
466
|
.refine(
|
|
463
467
|
(str) => !str.startsWith("-") && !str.endsWith("-"),
|
|
464
468
|
message(
|
|
@@ -487,7 +491,7 @@ const zId = function () {
|
|
|
487
491
|
required(field, [`Run ${phrase.cmdInit()} to generate a new game ID.`]),
|
|
488
492
|
)
|
|
489
493
|
.min(1, { ...atLeast(field, 1), abort: true })
|
|
490
|
-
.uuid(message(field, ["Must be a
|
|
494
|
+
.uuid(message(field, ["Must be a UUID."]))
|
|
491
495
|
.refine(
|
|
492
496
|
(value) => value === value.toLowerCase(),
|
|
493
497
|
message(field, ["Must be lowercase."], ISSUE.UUID_INVALID),
|
|
@@ -514,8 +518,8 @@ const zPrice = function () {
|
|
|
514
518
|
return z.optional(
|
|
515
519
|
z
|
|
516
520
|
.int()
|
|
517
|
-
.min(0, message(field, ["Must
|
|
518
|
-
.max(25, message(field, ["Must
|
|
521
|
+
.min(0, message(field, ["Must be 0 or more."]))
|
|
522
|
+
.max(25, message(field, ["Must be 25 or less."])),
|
|
519
523
|
);
|
|
520
524
|
};
|
|
521
525
|
|
|
@@ -547,7 +551,7 @@ const zTrailer = function () {
|
|
|
547
551
|
return z
|
|
548
552
|
.string()
|
|
549
553
|
.min(1, { ...atLeast(field, 1), abort: true })
|
|
550
|
-
.max(255, message(field, ["Must
|
|
554
|
+
.max(255, message(field, ["Must be at most 255 characters."]))
|
|
551
555
|
.refine(
|
|
552
556
|
(url) =>
|
|
553
557
|
zVimeo().safeParse(url).success || zYouTube().safeParse(url).success,
|
|
@@ -561,18 +565,26 @@ const zTrailer = function () {
|
|
|
561
565
|
const zVersion = function () {
|
|
562
566
|
const field = "version";
|
|
563
567
|
return z
|
|
564
|
-
.string(
|
|
565
|
-
required(field, ["A version is required unless one can be derived."]),
|
|
566
|
-
)
|
|
568
|
+
.string(required(field, ["Set it here, or in package.json."]))
|
|
567
569
|
.min(1, { ...atLeast(field, 1), abort: true })
|
|
568
570
|
.refine((value) => validSemver(value) !== null, {
|
|
569
|
-
...message(
|
|
571
|
+
...message(
|
|
572
|
+
field,
|
|
573
|
+
["Must be a version, such as 1.2.0."],
|
|
574
|
+
ISSUE.VERSION_INVALID,
|
|
575
|
+
),
|
|
570
576
|
abort: true,
|
|
571
577
|
})
|
|
572
578
|
.refine(
|
|
573
579
|
// lowercase too, since the database refuses an uppercase prerelease.
|
|
574
580
|
(value) => cleanSemver(value) === value && value === value.toLowerCase(),
|
|
575
|
-
message(
|
|
581
|
+
message(
|
|
582
|
+
field,
|
|
583
|
+
[
|
|
584
|
+
"Must be written as 1.2.0 or 1.2.0-beta.1: lowercase, with no v, spaces, or build metadata.",
|
|
585
|
+
],
|
|
586
|
+
ISSUE.VERSION_INVALID,
|
|
587
|
+
),
|
|
576
588
|
);
|
|
577
589
|
};
|
|
578
590
|
|
package/src/schema/readme.js
CHANGED
|
@@ -29,7 +29,8 @@ const main = function (value, { file }) {
|
|
|
29
29
|
...message(
|
|
30
30
|
"description",
|
|
31
31
|
[
|
|
32
|
-
|
|
32
|
+
"Needs two --- rules around the description.",
|
|
33
|
+
`${file} has content after its title, tagline, and bio.`,
|
|
33
34
|
],
|
|
34
35
|
cfgcode.ISSUE.DESCRIPTION_RULES_MISSING,
|
|
35
36
|
),
|
package/src/schema/workflow.js
CHANGED
|
@@ -68,7 +68,7 @@ const zDeployDir = function (root, { build }) {
|
|
|
68
68
|
return z
|
|
69
69
|
.string()
|
|
70
70
|
.min(1, {
|
|
71
|
-
...message(field, ["Must not be empty. Leave the field out instead."]),
|
|
71
|
+
...message(field, ["Must not be empty.", "Leave the field out instead."]),
|
|
72
72
|
abort: true,
|
|
73
73
|
})
|
|
74
74
|
.refine((file) => isRelativeCheck(root, file), {
|
|
@@ -82,24 +82,20 @@ const zDeployDir = function (root, { build }) {
|
|
|
82
82
|
.refine((file) => !isRootCheck(root, file), {
|
|
83
83
|
...message(
|
|
84
84
|
field,
|
|
85
|
-
["Must be a
|
|
85
|
+
["Must be a folder in the project, not its root."],
|
|
86
86
|
ISSUE.PATH_INVALID,
|
|
87
87
|
),
|
|
88
88
|
abort: true,
|
|
89
89
|
})
|
|
90
90
|
.refine((file) => avoidsCheck(root, file, MEDIA), {
|
|
91
|
-
...message(
|
|
92
|
-
field,
|
|
93
|
-
[`Must not contain ${MEDIA} in path.`],
|
|
94
|
-
ISSUE.PATH_INVALID,
|
|
95
|
-
),
|
|
91
|
+
...message(field, [`Must not be in ${MEDIA}.`], ISSUE.PATH_INVALID),
|
|
96
92
|
abort: true,
|
|
97
93
|
})
|
|
98
94
|
.refine((file) => hasBuild(build) || existsCheck(root, file), {
|
|
99
95
|
...message(
|
|
100
96
|
field,
|
|
101
97
|
[
|
|
102
|
-
"Must exist
|
|
98
|
+
"Must exist, since there's no build.",
|
|
103
99
|
`Run ${phrase.cmdInspect()} for details.`,
|
|
104
100
|
],
|
|
105
101
|
ISSUE.PATH_INVALID,
|
|
@@ -118,7 +114,8 @@ const zDeployDir = function (root, { build }) {
|
|
|
118
114
|
...message(
|
|
119
115
|
field,
|
|
120
116
|
[
|
|
121
|
-
`Must not have ${RESERVED.join(" or ")} at its root
|
|
117
|
+
`Must not have ${RESERVED.join(" or ")} at its root.`,
|
|
118
|
+
`They're reserved for the files ${cfgproduct.title} adds.`,
|
|
122
119
|
],
|
|
123
120
|
ISSUE.PATH_INVALID,
|
|
124
121
|
),
|
|
@@ -128,7 +125,8 @@ const zDeployDir = function (root, { build }) {
|
|
|
128
125
|
...message(
|
|
129
126
|
field,
|
|
130
127
|
[
|
|
131
|
-
`Must not have an ${RESERVED_FOLDER} folder at its root, in any letter case
|
|
128
|
+
`Must not have an ${RESERVED_FOLDER} folder at its root, in any letter case.`,
|
|
129
|
+
`It's reserved for ${cfgproduct.title}'s own files.`,
|
|
132
130
|
],
|
|
133
131
|
ISSUE.PATH_INVALID,
|
|
134
132
|
),
|
package/src/serve/game.js
CHANGED
|
@@ -98,7 +98,13 @@ const sendFile = function (req, res, file, headers) {
|
|
|
98
98
|
res.end();
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
file
|
|
101
|
+
// the file can go between finding and reading it, as a rebuild empties
|
|
102
|
+
// deploy_dir: the headers are sent, so the response just ends
|
|
103
|
+
const stream = file.open(range);
|
|
104
|
+
stream.on("error", () => {
|
|
105
|
+
res.destroy();
|
|
106
|
+
});
|
|
107
|
+
stream.pipe(res);
|
|
102
108
|
};
|
|
103
109
|
|
|
104
110
|
// [function]
|
package/src/serve/page.js
CHANGED
|
@@ -24,6 +24,17 @@ const SANDBOX =
|
|
|
24
24
|
const HOST = "/_arcadible/host.js";
|
|
25
25
|
const RELOAD = "/_arcadible/reload";
|
|
26
26
|
|
|
27
|
+
// [declare]
|
|
28
|
+
|
|
29
|
+
// the headers of the page's responses, as the play page's: it's
|
|
30
|
+
// cross-origin isolated, and nothing frames it.
|
|
31
|
+
const HEADERS = {
|
|
32
|
+
"cache-control": "no-store",
|
|
33
|
+
"content-security-policy": "frame-ancestors 'none';",
|
|
34
|
+
"cross-origin-embedder-policy": "require-corp",
|
|
35
|
+
"cross-origin-opener-policy": "same-origin",
|
|
36
|
+
};
|
|
37
|
+
|
|
27
38
|
// [function]
|
|
28
39
|
|
|
29
40
|
// `value` as JSON that's safe inside a <script>.
|
|
@@ -86,10 +97,10 @@ const html = function (game) {
|
|
|
86
97
|
version: config.version,
|
|
87
98
|
});
|
|
88
99
|
} catch (err) {
|
|
89
|
-
console.error("
|
|
100
|
+
console.error("Arcadible SDK: the play page couldn't host the game, so it stands alone.", err);
|
|
90
101
|
}
|
|
91
102
|
} else {
|
|
92
|
-
console.error("
|
|
103
|
+
console.error("Arcadible SDK: the play page's host didn't load, so the game stands alone.");
|
|
93
104
|
}
|
|
94
105
|
frame.src = config.src;
|
|
95
106
|
document.body.prepend(frame);
|
|
@@ -105,17 +116,6 @@ const html = function (game) {
|
|
|
105
116
|
|
|
106
117
|
// [function]
|
|
107
118
|
|
|
108
|
-
// the headers of the page's responses, as the play page's: it's
|
|
109
|
-
// cross-origin isolated, and nothing frames it.
|
|
110
|
-
const HEADERS = {
|
|
111
|
-
"cache-control": "no-store",
|
|
112
|
-
"content-security-policy": "frame-ancestors 'none';",
|
|
113
|
-
"cross-origin-embedder-policy": "require-corp",
|
|
114
|
-
"cross-origin-opener-policy": "same-origin",
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// [function]
|
|
118
|
-
|
|
119
119
|
// the request handler for the page's origin. `reloads` keeps the pages
|
|
120
120
|
// listening for a reload; `remote` has the host.
|
|
121
121
|
const pageHandler = function ({
|
package/src/serve/reload.js
CHANGED
|
@@ -52,6 +52,11 @@ const watchDir = function (dir, onChange) {
|
|
|
52
52
|
clearTimeout(timer);
|
|
53
53
|
timer = setTimeout(onChange, SETTLE);
|
|
54
54
|
});
|
|
55
|
+
// deploy_dir removed while it's watched (EPERM on Windows): stop watching
|
|
56
|
+
// rather than crash the server
|
|
57
|
+
watcher.on("error", () => {
|
|
58
|
+
watcher.close();
|
|
59
|
+
});
|
|
55
60
|
return () => {
|
|
56
61
|
clearTimeout(timer);
|
|
57
62
|
watcher.close();
|
package/src/util/log.js
CHANGED
|
@@ -30,6 +30,13 @@ const log = function (out, message, { bold = false, color, nl = false } = {}) {
|
|
|
30
30
|
|
|
31
31
|
// [function]
|
|
32
32
|
|
|
33
|
+
// a warning, marked with ⚠ (OUTPUT.md), on its first line.
|
|
34
|
+
const warned = function (message) {
|
|
35
|
+
return `⚠ ${[message].flat().join("\n").trim()}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// [function]
|
|
39
|
+
|
|
33
40
|
const h = function (message) {
|
|
34
41
|
log(console.log, message, { bold: true });
|
|
35
42
|
};
|
|
@@ -91,13 +98,13 @@ const hwo = function (message) {
|
|
|
91
98
|
// [function]
|
|
92
99
|
|
|
93
100
|
const pw = function (message) {
|
|
94
|
-
log(console.warn, message, { color: "yellow" });
|
|
101
|
+
log(console.warn, warned(message), { color: "yellow" });
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
// [function]
|
|
98
105
|
|
|
99
106
|
const nlpw = function (message) {
|
|
100
|
-
log(console.warn, message, { color: "yellow", nl: true });
|
|
107
|
+
log(console.warn, warned(message), { color: "yellow", nl: true });
|
|
101
108
|
};
|
|
102
109
|
|
|
103
110
|
// [function]
|
package/src/util/md.js
CHANGED
|
@@ -32,7 +32,7 @@ const escape = function (str) {
|
|
|
32
32
|
.replace(/>/gu, ">");
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
// [
|
|
35
|
+
// [define]
|
|
36
36
|
|
|
37
37
|
// there is no video syntax in Markdown, so an image of a video renders as the
|
|
38
38
|
// video the developer would have written: its src and title, and no alt.
|
package/src/util/output.js
CHANGED
|
@@ -59,6 +59,20 @@ const print = function (data, { key, nl, toml } = {}) {
|
|
|
59
59
|
|
|
60
60
|
// [function]
|
|
61
61
|
|
|
62
|
+
// exits with `code` once stdout has written what it holds. A pipe on macOS
|
|
63
|
+
// writes later, so exiting at once would cut a large --json document short;
|
|
64
|
+
// with nothing held, it exits at once, as a caller that goes on expects.
|
|
65
|
+
const exit = function (code) {
|
|
66
|
+
if (!process.stdout.writableLength) {
|
|
67
|
+
process.exit(code);
|
|
68
|
+
}
|
|
69
|
+
process.exitCode = code;
|
|
70
|
+
process.stdout.write("", () => process.exit(code));
|
|
71
|
+
return new Promise(() => {});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// [function]
|
|
75
|
+
|
|
62
76
|
// a command's error path: the error as JSON, or its heading ("Deploy
|
|
63
77
|
// aborted.") and the reason on stderr, then its code and the link to it on
|
|
64
78
|
// the Errors page, once the spinner and progress bar are cleared. Then exits
|
|
@@ -102,7 +116,7 @@ const abort = function (
|
|
|
102
116
|
log.pd(message);
|
|
103
117
|
log.nlpd([phrase.errorCode(shown), chalk.cyan.underline(link)]);
|
|
104
118
|
}
|
|
105
|
-
|
|
119
|
+
return exit(1);
|
|
106
120
|
};
|
|
107
121
|
|
|
108
122
|
// [function]
|
|
@@ -128,7 +142,7 @@ const warn = function (warnings, { json, quiet } = {}) {
|
|
|
128
142
|
if (json || quiet) {
|
|
129
143
|
return;
|
|
130
144
|
}
|
|
131
|
-
warnings.forEach((each) => log.pw(
|
|
145
|
+
warnings.forEach((each) => log.pw(schema.print(each)));
|
|
132
146
|
};
|
|
133
147
|
|
|
134
148
|
// [function]
|
|
@@ -150,6 +164,7 @@ const warning = function (code, message, path = null) {
|
|
|
150
164
|
|
|
151
165
|
module.exports = {
|
|
152
166
|
abort,
|
|
167
|
+
exit,
|
|
153
168
|
launched,
|
|
154
169
|
print,
|
|
155
170
|
printJson,
|
package/src/util/rc.js
CHANGED
|
@@ -75,7 +75,22 @@ const files = function (cmd, argv) {
|
|
|
75
75
|
|
|
76
76
|
// [function]
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// ini parses anything, so a section header with no closing bracket, such as
|
|
79
|
+
// `[validate`, would become a key, and the options under it would be read
|
|
80
|
+
// outside their section. The first one's problem, or null.
|
|
81
|
+
const unclosedSection = function (str) {
|
|
82
|
+
const lines = str.split(/\r?\n/u).map((line) => line.trim());
|
|
83
|
+
const i = lines.findIndex(
|
|
84
|
+
(text) => text.startsWith("[") && !/^\[[^\]]*\]$/u.test(text),
|
|
85
|
+
);
|
|
86
|
+
return i === -1
|
|
87
|
+
? null
|
|
88
|
+
: `Line ${i + 1}: section header isn't closed: ${lines[i]}`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// [function]
|
|
92
|
+
|
|
93
|
+
// a file's sections, INI or JSON, or nothing when it can't be read. A file
|
|
79
94
|
// that doesn't parse is `parse_failed`, naming the file.
|
|
80
95
|
const read = function (file) {
|
|
81
96
|
let str = null;
|
|
@@ -85,6 +100,13 @@ const read = function (file) {
|
|
|
85
100
|
return {};
|
|
86
101
|
}
|
|
87
102
|
if (!/^\s*\{/u.test(str)) {
|
|
103
|
+
const unclosed = unclosedSection(str);
|
|
104
|
+
if (unclosed) {
|
|
105
|
+
throw new ArcError(
|
|
106
|
+
cfgcode.PARSE_FAILED,
|
|
107
|
+
phrase.unableToParse(file, unclosed),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
88
110
|
return ini.parse(str);
|
|
89
111
|
}
|
|
90
112
|
try {
|
package/src/util/s3.js
CHANGED
|
@@ -17,13 +17,32 @@ const ArcError = error.ArcError;
|
|
|
17
17
|
const createReadStream = fs.createReadStream;
|
|
18
18
|
const request = https.request;
|
|
19
19
|
|
|
20
|
+
// [declare]
|
|
21
|
+
|
|
22
|
+
const CHECKSUM = "x-amz-checksum-sha256";
|
|
23
|
+
|
|
24
|
+
// [function]
|
|
25
|
+
|
|
26
|
+
// [function]
|
|
27
|
+
|
|
28
|
+
// whether the upload link signs `header`, which must then be sent.
|
|
29
|
+
const signs = function (url, header) {
|
|
30
|
+
const signed = new URL(url).searchParams.get("X-Amz-SignedHeaders");
|
|
31
|
+
return Boolean(signed?.split(";").includes(header));
|
|
32
|
+
};
|
|
33
|
+
|
|
20
34
|
// [function]
|
|
21
35
|
|
|
22
|
-
|
|
36
|
+
// the zip's SHA-256 is sent when the link signs it, so S3 refuses a zip that
|
|
37
|
+
// isn't the one the upload was asked for.
|
|
38
|
+
const requestOpts = function (url, { sha256, size }) {
|
|
23
39
|
return {
|
|
24
40
|
headers: {
|
|
25
41
|
"Content-Length": size,
|
|
26
42
|
"Content-Type": "application/zip",
|
|
43
|
+
...(signs(url, CHECKSUM)
|
|
44
|
+
? { [CHECKSUM]: Buffer.from(sha256, "hex").toString("base64") }
|
|
45
|
+
: {}),
|
|
27
46
|
},
|
|
28
47
|
method: "PUT",
|
|
29
48
|
};
|
|
@@ -34,9 +53,9 @@ const requestOpts = function (size) {
|
|
|
34
53
|
// puts the file to the upload link, and resolves once S3 accepts it. A
|
|
35
54
|
// request that gets no response rejects as api_unreachable; a file that can't
|
|
36
55
|
// be read rejects with its own error.
|
|
37
|
-
const upload = function (url, { path, size }, onProgress) {
|
|
56
|
+
const upload = function (url, { path, sha256, size }, onProgress) {
|
|
38
57
|
return new Promise((resolve, reject) => {
|
|
39
|
-
const req = request(url, requestOpts(size), (resp) => {
|
|
58
|
+
const req = request(url, requestOpts(url, { sha256, size }), (resp) => {
|
|
40
59
|
resp.resume();
|
|
41
60
|
const status = resp.statusCode;
|
|
42
61
|
if (status >= 200 && status < 300) {
|
package/src/util/schema.js
CHANGED
|
@@ -17,6 +17,14 @@ const ISSUE = cfgcode.ISSUE;
|
|
|
17
17
|
|
|
18
18
|
// [declare]
|
|
19
19
|
|
|
20
|
+
// how a value's kind is named, as TOML writes it.
|
|
21
|
+
const KINDS = {
|
|
22
|
+
array: "a list",
|
|
23
|
+
boolean: "true or false",
|
|
24
|
+
number: "a number",
|
|
25
|
+
object: "a table",
|
|
26
|
+
string: "a string",
|
|
27
|
+
};
|
|
20
28
|
const NUMBERS = new Set(["bigint", "int", "number"]);
|
|
21
29
|
|
|
22
30
|
// [function]
|
|
@@ -65,11 +73,54 @@ const issueCodeOf = function (issue) {
|
|
|
65
73
|
|
|
66
74
|
// [function]
|
|
67
75
|
|
|
76
|
+
// a value's kind, as TOML writes it: "a string", "a list", "a table".
|
|
77
|
+
const kindOf = function (type) {
|
|
78
|
+
return KINDS[type] ?? `a ${type}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// [function]
|
|
82
|
+
|
|
83
|
+
// what zod reports itself (a value of the wrong type, or not one of the
|
|
84
|
+
// options), in arc's words, as `{ message, details }`: "Must be a string, not
|
|
85
|
+
// a number." Null for a message of our own.
|
|
86
|
+
const zodMessage = function (issue) {
|
|
87
|
+
if (issue.code === "invalid_value" && issue.values) {
|
|
88
|
+
return { details: [issue.values.join(", ")], message: "Must be one of:" };
|
|
89
|
+
}
|
|
90
|
+
const given = Array.isArray(issue.input) ? "array" : typeof issue.input;
|
|
91
|
+
if (issue.code === "invalid_type" && typeof issue.input !== "undefined") {
|
|
92
|
+
return {
|
|
93
|
+
details: [],
|
|
94
|
+
message: `Must be ${kindOf(issue.expected)}, not ${kindOf(given)}.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
issue.code === "invalid_union" &&
|
|
99
|
+
issue.errors.every((branch) =>
|
|
100
|
+
branch.every((each) => each.code === "invalid_type"),
|
|
101
|
+
)
|
|
102
|
+
) {
|
|
103
|
+
const expected = [
|
|
104
|
+
...new Set(issue.errors.map((branch) => kindOf(branch[0].expected))),
|
|
105
|
+
];
|
|
106
|
+
return {
|
|
107
|
+
details: [],
|
|
108
|
+
message: `Must be ${expected.join(" or ")}, not ${kindOf(given)}.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// [function]
|
|
115
|
+
|
|
68
116
|
// a zod issue as arc validate reports it: the field's path, the rule's code,
|
|
69
117
|
// the message's first line and its details, and the link to the field's
|
|
70
118
|
// rules, or null for a field the docs leave out.
|
|
71
119
|
const toIssue = function (name, issue) {
|
|
72
|
-
const
|
|
120
|
+
const parsed = schemaMessage.parse(issue.message);
|
|
121
|
+
const { details, message } =
|
|
122
|
+
(/^Invalid (input|option)\b/u.test(parsed.message) && zodMessage(issue)) ||
|
|
123
|
+
parsed;
|
|
73
124
|
return {
|
|
74
125
|
code: issueCodeOf(issue),
|
|
75
126
|
details,
|
|
@@ -133,5 +184,6 @@ module.exports = {
|
|
|
133
184
|
dereference,
|
|
134
185
|
issueCodeOf,
|
|
135
186
|
print,
|
|
187
|
+
toIssue,
|
|
136
188
|
validator,
|
|
137
189
|
};
|