arcadible 0.1.26 → 0.1.28

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/package.json CHANGED
@@ -61,5 +61,5 @@
61
61
  "license": "Apache-2.0",
62
62
  "main": "./src/cli/index.js",
63
63
  "name": "arcadible",
64
- "version": "0.1.26"
64
+ "version": "0.1.28"
65
65
  }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ // [export]
4
+
5
+ module.exports = ["unused", "generative"];
package/src/config/art.js CHANGED
@@ -11,6 +11,6 @@ module.exports = [
11
11
  "icon_video.mp4",
12
12
  "poster.png",
13
13
  "poster_video.mp4",
14
- "wordmark.svg",
15
- "wordmark_animated.svg",
14
+ "splash.png",
15
+ "splash_video.mp4",
16
16
  ];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ // [export]
4
+
5
+ module.exports = {
6
+ ".html": "text/html",
7
+ };
@@ -3,25 +3,18 @@
3
3
  // [export]
4
4
 
5
5
  module.exports = {
6
- ALLOWED_ATTR: ["href", "target", "rel", "src", "alt", "title"],
7
- ALLOWED_TAGS: [
8
- "h3",
9
- "h4",
10
- "h5",
11
- "h6",
12
- "hr",
13
- "p",
14
- "ul",
15
- "ol",
16
- "li",
17
- "pre",
18
- "img",
19
- "a",
20
- "code",
21
- "strong",
22
- "b",
23
- "em",
24
- "i",
25
- "br",
26
- ],
6
+ allowed: {
7
+ a: ["href", "alt", "title"],
8
+ blockquote: [],
9
+ em: [],
10
+ h3: [],
11
+ h4: [],
12
+ hr: [],
13
+ img: ["src", "alt", "title"],
14
+ li: [],
15
+ ol: [],
16
+ p: [],
17
+ strong: [],
18
+ ul: [],
19
+ },
27
20
  };
@@ -2,4 +2,4 @@
2
2
 
3
3
  // [export]
4
4
 
5
- module.exports = ["private", "unlisted", "public"];
5
+ module.exports = ["unlisted", "public"];
package/src/phrase.js CHANGED
@@ -34,6 +34,30 @@ const cmdValidate = function () {
34
34
 
35
35
  // [function]
36
36
 
37
+ const docAttrInvalid = function (tag, attr, value) {
38
+ return `Value '${value}' is not valid for the '${attr}' attribute on the '${tag}' tag.`;
39
+ };
40
+
41
+ // [function]
42
+
43
+ const docAttrUnallowed = function (tag, attr) {
44
+ return `Attribute '${attr}' is not allowed on the '${tag}' tag.`;
45
+ };
46
+
47
+ // [function]
48
+
49
+ const docSanitizeFailed = function () {
50
+ return "Document contains invalid elements.";
51
+ };
52
+
53
+ // [function]
54
+
55
+ const docUnsupported = function () {
56
+ return "Document has an unsupported file type.";
57
+ };
58
+
59
+ // [function]
60
+
37
61
  const gameExists = function (idOrHandle) {
38
62
  return `Game exists: ${idOrHandle}`;
39
63
  };
@@ -355,8 +379,10 @@ const videoWidthMin = function (width) {
355
379
  module.exports = {
356
380
  cmdInit,
357
381
  cmdInspect,
358
- cmdLogin,
359
- cmdValidate,
382
+ docAttrInvalid,
383
+ docAttrUnallowed,
384
+ docSanitizeFailed,
385
+ docUnsupported,
360
386
  gameExists,
361
387
  gameVersionExists,
362
388
  gitChangesPresent,
@@ -10,6 +10,7 @@ const fs = require("fs-extra");
10
10
 
11
11
  // [import]
12
12
 
13
+ const docCheck = require("../../util/doc").check;
13
14
  const imageCheck = require("../../util/image").check;
14
15
  const videoCheck = require("../../util/video").check;
15
16
 
@@ -23,6 +24,12 @@ const resolve = path.resolve;
23
24
 
24
25
  // [function]
25
26
 
27
+ const doc = function (root, str, opts) {
28
+ return docCheck(str, opts);
29
+ };
30
+
31
+ // [function]
32
+
26
33
  const exists = function (root, target) {
27
34
  return pathExistsSync(join(root, target));
28
35
  };
@@ -64,4 +71,12 @@ const video = function (root, target, opts) {
64
71
 
65
72
  // [export]
66
73
 
67
- module.exports = { exists, image, isIn, isOut, isRelative, video };
74
+ module.exports = {
75
+ doc,
76
+ exists,
77
+ image,
78
+ isIn,
79
+ isOut,
80
+ isRelative,
81
+ video,
82
+ };
@@ -8,7 +8,9 @@ const z = require("zod");
8
8
 
9
9
  // [import]
10
10
 
11
+ const cfgai = require("../config/ai");
11
12
  const cfgaudience = require("../config/audience");
13
+ const cfgdoc = require("../config/doc");
12
14
  const cfggenre = require("../config/genre");
13
15
  const cfghtml = require("../config/html");
14
16
  const cfgimage = require("../config/image");
@@ -19,20 +21,24 @@ const cfgurl = require("../config/url");
19
21
  const cfgvideo = require("../config/video");
20
22
  const cfgvisibility = require("../config/visibility");
21
23
  const check = require("./check/game");
22
- const dom = require("../util/dom");
24
+ const paths = require("../util/paths");
23
25
  const phrase = require("../phrase");
24
26
  const size = require("../util/size");
25
27
  const strs = require("../util/strs");
28
+ const uris = require("../util/uris");
26
29
 
27
30
  // [use]
28
31
 
32
+ const basefile = paths.basefile;
29
33
  const cleanSemver = semver.clean;
34
+ const docCheck = check.doc;
30
35
  const existsCheck = check.exists;
31
36
  const imageCheck = check.image;
32
37
  const indentLines = strs.indentLines;
33
38
  const isInCheck = check.isIn;
34
39
  const isOutCheck = check.isOut;
35
40
  const isRelativeCheck = check.isRelative;
41
+ const isSafeUri = uris.isSafeUri;
36
42
  const validSemver = semver.valid;
37
43
  const videoCheck = check.video;
38
44
 
@@ -41,6 +47,10 @@ const videoCheck = check.video;
41
47
  const HANDLE = cfgproduct.handle;
42
48
  const MEDIA = `.${HANDLE}`;
43
49
 
50
+ // [declare]
51
+
52
+ const GALLERY = `${MEDIA}/gallery`;
53
+
44
54
  // [function]
45
55
 
46
56
  const reference = function (field) {
@@ -61,15 +71,62 @@ const message = function (field, lines) {
61
71
 
62
72
  // [function]
63
73
 
64
- const zText = function (field, { max, min }) {
74
+ const zSet = function (field, schema, { max, min }) {
75
+ return z
76
+ .array(schema)
77
+ .min(min, message(field, [`Must be at least: ${min} element`]))
78
+ .max(max, message(field, [`Must not exceed: ${max} elements`]))
79
+ .refine(
80
+ (items) => new Set(items).size === items.length,
81
+ message(field, ["Must not contain duplicate elements."]),
82
+ );
83
+ };
84
+
85
+ // [function]
86
+
87
+ const zSetBasefile = function (field, schema, { max, min }) {
88
+ return z
89
+ .array(schema)
90
+ .min(min, message(field, [`Must be at least: ${min} file`]))
91
+ .max(max, message(field, [`Must not exceed: ${max} files`]))
92
+ .refine(
93
+ (items) => new Set(items.map(basefile)).size === items.length,
94
+ message(field, [
95
+ "Must not contain duplicate filenames (ignoring extensions).",
96
+ ]),
97
+ );
98
+ };
99
+
100
+ // [function]
101
+
102
+ const zString = function (field, re, { max, min }) {
65
103
  return z
66
104
  .string()
67
105
  .min(min, message(field, [`Must be at least: ${min} character`]))
68
106
  .max(max, message(field, [`Must not exceed: ${max} characters`]))
69
- .regex(
70
- /^[\p{L}0-9 .,!?'"():;-]+$/u,
71
- message(field, ["Contains invalid characters."]),
72
- );
107
+ .regex(re, message(field, ["Contains invalid characters."]));
108
+ };
109
+
110
+ // [function]
111
+
112
+ const zText = function (field, opts) {
113
+ const re = /^[\p{L}0-9 .,!?'"():;-]+$/u;
114
+ return zString(field, re, opts);
115
+ };
116
+
117
+ // [function]
118
+
119
+ const zHtml = function (field, root, { max, min, ...opts }) {
120
+ const re = /^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}\r\n]*$/u;
121
+ return zString(field, re, { max, min }).superRefine((str, ctx) => {
122
+ const issues = docCheck(root, str, { ...opts, type: cfgdoc[".html"] });
123
+ if (issues.length) {
124
+ ctx.addIssue({
125
+ code: "custom",
126
+ ...message(field, ["Invalid Markdown.", ...issues]),
127
+ });
128
+ }
129
+ });
73
130
  };
74
131
 
75
132
  // [function]
@@ -109,20 +166,6 @@ const zPng = function (field, root, opts) {
109
166
 
110
167
  // [function]
111
168
 
112
- const zSvg = function (field, root, opts) {
113
- return zMedia(field, root).superRefine((file, ctx) => {
114
- const issues = imageCheck(root, file, { ...opts, type: cfgimage[".svg"] });
115
- if (issues.length) {
116
- ctx.addIssue({
117
- code: "custom",
118
- ...message(field, ["Invalid SVG.", ...issues]),
119
- });
120
- }
121
- });
122
- };
123
-
124
- // [function]
125
-
126
169
  const zMp4 = function (field, root, opts) {
127
170
  return zMedia(field, root).superRefine((file, ctx) => {
128
171
  const issues = videoCheck(root, file, {
@@ -162,6 +205,13 @@ const zYouTube = function () {
162
205
 
163
206
  // [function]
164
207
 
208
+ const zAi = function () {
209
+ // const field = "ai";
210
+ return z.optional(z.enum(cfgai));
211
+ };
212
+
213
+ // [function]
214
+
165
215
  const zAudience = function () {
166
216
  // const field = "audience";
167
217
  return z.optional(z.enum(cfgaudience));
@@ -199,7 +249,7 @@ const zBannerVideo = function (root) {
199
249
 
200
250
  const zBio = function () {
201
251
  const field = "bio";
202
- return z.optional(zText(field, { max: 30 * 5, min: 1 }));
252
+ return z.optional(zText(field, { max: 30 * 6, min: 1 }));
203
253
  };
204
254
 
205
255
  // [function]
@@ -232,22 +282,22 @@ const zCoverVideo = function (root) {
232
282
 
233
283
  // [function]
234
284
 
235
- const zDescription = function () {
285
+ const zDescription = function (root) {
236
286
  const field = "description";
237
287
  return z.optional(
238
- z
239
- .string()
240
- .min(1, message(field, ["Must be at least: 1 character"]))
241
- .max(3000, message(field, ["Must not exceed: 3000 characters"]))
242
- .refine(
243
- (str) =>
244
- str ===
245
- dom.sanitize(str, {
246
- ALLOWED_ATTR: cfghtml.ALLOWED_ATTR,
247
- ALLOWED_TAGS: cfghtml.ALLOWED_TAGS,
248
- }),
249
- message(field, ["Contains invalid elements."]),
250
- ),
288
+ zHtml(field, root, {
289
+ allowed: cfghtml.allowed,
290
+ hooks: {
291
+ href: (uri) => isSafeUri(uri),
292
+ src: (src) =>
293
+ isRelativeCheck(root, src) &&
294
+ isOutCheck(root, src) &&
295
+ isInCheck(root, src, GALLERY) &&
296
+ existsCheck(root, src),
297
+ },
298
+ max: 3000,
299
+ min: 1,
300
+ }),
251
301
  );
252
302
  };
253
303
 
@@ -256,48 +306,48 @@ const zDescription = function () {
256
306
  const zGallery = function (root) {
257
307
  const field = "gallery";
258
308
  return z.optional(
259
- z
260
- .array(
261
- zMedia(field, root).superRefine((file, ctx) => {
262
- if (file.endsWith(".png")) {
263
- const issues = imageCheck(root, file, {
264
- maxSize: size.mb(2),
265
- type: cfgimage[".png"],
309
+ zSetBasefile(
310
+ field,
311
+ zMedia(field, root).superRefine((file, ctx) => {
312
+ if (file.endsWith(".png")) {
313
+ const issues = imageCheck(root, file, {
314
+ maxSize: size.mb(2),
315
+ type: cfgimage[".png"],
316
+ });
317
+ if (issues.length) {
318
+ ctx.addIssue({
319
+ code: "custom",
320
+ ...message(field, [`Invalid PNG: ${file}`, ...issues]),
266
321
  });
267
- if (issues.length) {
268
- ctx.addIssue({
269
- code: "custom",
270
- ...message(field, [`Invalid PNG: ${file}`, ...issues]),
271
- });
272
- }
273
- } else if (file.endsWith(".svg")) {
274
- const issues = imageCheck(root, file, {
275
- maxSize: size.mb(1),
276
- type: cfgimage[".svg"],
322
+ }
323
+ } else if (file.endsWith(".svg")) {
324
+ const issues = imageCheck(root, file, {
325
+ maxSize: size.mb(1),
326
+ type: cfgimage[".svg"],
327
+ });
328
+ if (issues.length) {
329
+ ctx.addIssue({
330
+ code: "custom",
331
+ ...message(field, [`Invalid SVG: ${file}`, ...issues]),
277
332
  });
278
- if (issues.length) {
279
- ctx.addIssue({
280
- code: "custom",
281
- ...message(field, [`Invalid SVG: ${file}`, ...issues]),
282
- });
283
- }
284
- } else if (file.endsWith(".mp4")) {
285
- const issues = videoCheck(root, file, {
286
- maxDuration: 30,
287
- maxSize: size.mb(15),
288
- minDuration: 3,
289
- type: cfgvideo[".mp4"],
333
+ }
334
+ } else if (file.endsWith(".mp4")) {
335
+ const issues = videoCheck(root, file, {
336
+ maxDuration: 30,
337
+ maxSize: size.mb(15),
338
+ minDuration: 3,
339
+ type: cfgvideo[".mp4"],
340
+ });
341
+ if (issues.length) {
342
+ ctx.addIssue({
343
+ code: "custom",
344
+ ...message(field, [`Invalid MP4: ${file}`, ...issues]),
290
345
  });
291
- if (issues.length) {
292
- ctx.addIssue({
293
- code: "custom",
294
- ...message(field, [`Invalid MP4: ${file}`, ...issues]),
295
- });
296
- }
297
346
  }
298
- }),
299
- )
300
- .max(12, message(field, ["Must not exceed: 12 elements"])),
347
+ }
348
+ }),
349
+ { max: 8 * 3, min: 1 },
350
+ ),
301
351
  );
302
352
  };
303
353
 
@@ -305,11 +355,7 @@ const zGallery = function (root) {
305
355
 
306
356
  const zGenres = function () {
307
357
  const field = "genres";
308
- return z.optional(
309
- z
310
- .array(z.enum(cfggenre))
311
- .max(3, message(field, ["Must not exceed: 3 elements"])),
312
- );
358
+ return z.optional(zSet(field, z.enum(cfggenre), { max: 3, min: 1 }));
313
359
  };
314
360
 
315
361
  // [function]
@@ -376,11 +422,7 @@ const zId = function () {
376
422
 
377
423
  const zInputs = function () {
378
424
  const field = "inputs";
379
- return z.optional(
380
- z
381
- .array(z.enum(cfginput))
382
- .max(3, message(field, ["Must not exceed: 3 elements"])),
383
- );
425
+ return z.optional(zSet(field, z.enum(cfginput), { max: 3, min: 1 }));
384
426
  };
385
427
 
386
428
  // [function]
@@ -439,6 +481,34 @@ const zShortTitle = function () {
439
481
 
440
482
  // [function]
441
483
 
484
+ const zSplash = function (root) {
485
+ const field = "splash";
486
+ return z.optional(
487
+ zPng(field, root, {
488
+ aspect: "16:9",
489
+ maxSize: size.mb(2),
490
+ minHeight: 576,
491
+ minWidth: 1024,
492
+ }),
493
+ );
494
+ };
495
+
496
+ // [function]
497
+
498
+ const zSplashVideo = function (root) {
499
+ const field = "splash_video";
500
+ return z.optional(
501
+ zMp4(field, root, {
502
+ aspect: "16:9",
503
+ maxSize: size.mb(15),
504
+ minHeight: 576,
505
+ minWidth: 1024,
506
+ }),
507
+ );
508
+ };
509
+
510
+ // [function]
511
+
442
512
  const zTagline = function () {
443
513
  const field = "tagline";
444
514
  return z.optional(zText(field, { max: 30 * 1, min: 1 }));
@@ -496,38 +566,17 @@ const zVisibility = function () {
496
566
 
497
567
  // [function]
498
568
 
499
- const zWordmark = function (root) {
500
- const field = "wordmark";
501
- return z.optional(
502
- zSvg(field, root, {
503
- maxSize: size.kb(512),
504
- }),
505
- );
506
- };
507
-
508
- // [function]
509
-
510
- const zWordmarkAnimated = function (root) {
511
- const field = "wordmark_animated";
512
- return z.optional(
513
- zSvg(field, root, {
514
- maxSize: size.mb(1),
515
- }),
516
- );
517
- };
518
-
519
- // [function]
520
-
521
569
  const main = function (value, { root }) {
522
570
  /* eslint-disable camelcase */
523
571
  return z.object({
572
+ ai: zAi(),
524
573
  audience: zAudience(),
525
574
  banner: zBanner(root),
526
575
  banner_video: zBannerVideo(root),
527
576
  bio: zBio(),
528
577
  cover: zCover(root),
529
578
  cover_video: zCoverVideo(root),
530
- description: zDescription(),
579
+ description: zDescription(root),
531
580
  gallery: zGallery(root),
532
581
  genres: zGenres(),
533
582
  handle: zHandle(),
@@ -540,13 +589,13 @@ const main = function (value, { root }) {
540
589
  poster_video: zPosterVideo(root),
541
590
  price: zPrice(),
542
591
  short_title: zShortTitle(),
592
+ splash: zSplash(root),
593
+ splash_video: zSplashVideo(root),
543
594
  tagline: zTagline(),
544
595
  title: zTitle(),
545
596
  trailer: zTrailer(),
546
597
  version: zVersion(),
547
598
  visibility: zVisibility(),
548
- wordmark: zWordmark(root),
549
- wordmark_animated: zWordmarkAnimated(root),
550
599
  });
551
600
  /* eslint-enable camelcase */
552
601
  };
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ // [import]
4
+
5
+ const cfgdoc = require("../config/doc");
6
+ const dom = require("./dom");
7
+ const phrase = require("../phrase");
8
+
9
+ // [function]
10
+
11
+ const checkAttr = function (allowed, hooks, issues) {
12
+ return (node, event) => {
13
+ const nodeName = node.nodeName.toLowerCase();
14
+ const attrName = event.attrName;
15
+ const attrValue = event.attrValue;
16
+ const isAllowed = allowed[nodeName]?.includes(attrName);
17
+ const isValid = !hooks[attrName] || hooks[attrName](attrValue);
18
+ if (!isAllowed || !isValid) {
19
+ issues.push({ attrName, attrValue, isAllowed, isValid, nodeName });
20
+ event.keepAttr = false;
21
+ }
22
+ };
23
+ };
24
+
25
+ // [function]
26
+
27
+ const checkAllowed = function (str, allowed, hooks, issues) {
28
+ const attr = checkAttr(allowed, hooks, issues);
29
+ return str === dom.sanitize(str, { allowed, hooks: { attr } });
30
+ };
31
+
32
+ // [function]
33
+
34
+ const checkHtml = function (str, { allowed, hooks }) {
35
+ const issues = [];
36
+ if (!checkAllowed(str, allowed, hooks, issues)) {
37
+ if (issues) {
38
+ return issues.map((issue) => {
39
+ const { attrName, attrValue, isAllowed, nodeName } = issue;
40
+ return isAllowed
41
+ ? phrase.docAttrInvalid(nodeName, attrName, attrValue)
42
+ : phrase.docAttrUnallowed(nodeName, attrName);
43
+ });
44
+ }
45
+ return [phrase.docSanitizeFailed()];
46
+ }
47
+ return [];
48
+ };
49
+
50
+ // [function]
51
+
52
+ const check = function (str, { type, ...opts }) {
53
+ const checks = { [cfgdoc[".html"]]: checkHtml };
54
+ const checkDoc = checks[type];
55
+ return checkDoc ? checkDoc(str, opts) : [phrase.docUnsupported()];
56
+ };
57
+
58
+ // [export]
59
+
60
+ module.exports = { check };
package/src/util/dom.js CHANGED
@@ -11,17 +11,36 @@ const JSDOM = jsdom.JSDOM;
11
11
 
12
12
  // [function]
13
13
 
14
- const sanitize = function (str, cfg) {
15
- const dom = dompurify(new JSDOM().window);
16
- if (cfg) {
14
+ const sanitizeCustom = function (dom, str, cfg) {
15
+ const allowed = cfg.allowed;
16
+ const hooks = cfg.hooks;
17
+ if (hooks?.attr) {
18
+ dom.addHook("uponSanitizeAttribute", hooks.attr);
19
+ }
20
+ if (allowed) {
21
+ const attrs = new Set(Object.values(allowed).flatMap((arr) => arr));
22
+ const tags = Object.keys(allowed);
17
23
  return dom.sanitize(str, {
18
- ALLOWED_ATTR: cfg.ALLOWED_ATTR,
19
- ALLOWED_TAGS: cfg.ALLOWED_TAGS,
24
+ ALLOWED_ATTR: attrs,
25
+ ALLOWED_TAGS: tags,
20
26
  });
21
27
  }
22
28
  return dom.sanitize(str);
23
29
  };
24
30
 
31
+ // [function]
32
+
33
+ const sanitizeBasic = function (dom, str) {
34
+ return dom.sanitize(str);
35
+ };
36
+
37
+ // [function]
38
+
39
+ const sanitize = function (str, cfg) {
40
+ const dom = dompurify(new JSDOM().window);
41
+ return cfg ? sanitizeCustom(dom, str, cfg) : sanitizeBasic(dom, str);
42
+ };
43
+
25
44
  // [export]
26
45
 
27
46
  module.exports = { DOM: JSDOM, sanitize };
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ // [import]
4
+
5
+ const path = require("node:path");
6
+
7
+ // [use]
8
+
9
+ const extname = path.extname;
10
+
11
+ // [function]
12
+
13
+ const basefile = function (file) {
14
+ return file.slice(0, -extname(file).length);
15
+ };
16
+
17
+ // [export]
18
+
19
+ module.exports = { basefile };
@@ -4,6 +4,10 @@
4
4
 
5
5
  const chalk = require("chalk");
6
6
 
7
+ // [import]
8
+
9
+ const cfgurl = require("../config/url");
10
+
7
11
  // [function]
8
12
 
9
13
  const dereference = function (issues) {
@@ -12,7 +16,7 @@ const dereference = function (issues) {
12
16
  issue
13
17
  .split("\n")
14
18
  .filter((line) => {
15
- if (/https?:\/\//u.test(line)) {
19
+ if (new RegExp(`${cfgurl.MANIFEST}`, "u").test(line)) {
16
20
  references.add(line.trim());
17
21
  return false;
18
22
  }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ // [function]
4
+
5
+ const isSafeUri = function (uri) {
6
+ return /^(?:(?:https):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/iu.test(uri);
7
+ };
8
+
9
+ // [export]
10
+
11
+ module.exports = { isSafeUri };