@cloudcrane/app-spec 0.2.0 → 0.4.0
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/README.md +7 -1
- package/dist/src/blueprint.d.ts +1 -0
- package/dist/src/blueprint.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/schema.d.ts +237 -20
- package/dist/src/schema.js +34 -5
- package/dist/src/uses.d.ts +19 -0
- package/dist/src/uses.js +28 -0
- package/package.json +5 -1
- package/schema/app-spec.v1.json +37 -2
package/README.md
CHANGED
|
@@ -19,7 +19,13 @@ if (!spec) for (const p of problems) console.log(`${p.at}: ${p.problem}`);
|
|
|
19
19
|
date functions and a bounded `fold`.
|
|
20
20
|
- `@cloudcrane/app-spec/schema.json`: the spec as JSON Schema (2020-12).
|
|
21
21
|
- Coded components (`components`): code for what the catalog can't draw, with
|
|
22
|
-
the kit it runs with (`game`
|
|
22
|
+
the kit it runs with (`game`, or `page` for a whole HTML page), the inputs
|
|
23
|
+
it's given and the events it sends. A game kit's code is at most 64 KB and
|
|
24
|
+
can't contain `</script>`; a page kit's is an HTML page of up to 1 MB.
|
|
25
|
+
- Device features (`uses`): what the app uses, by CloudCrane's names
|
|
26
|
+
(`camera`, `microphone`, `location`, `motion`, `screen-awake`, `gamepad`,
|
|
27
|
+
`voice`, `contacts`, `nfc`), each once with why. Anything not listed is
|
|
28
|
+
blocked. The camera and microphone need a page-kit component.
|
|
23
29
|
A screen places one with the catalog's `Coded` component, binding each input
|
|
24
30
|
to the data model and each event to an action. Its code is exempt from the
|
|
25
31
|
leak scan's code rules, but not from the others.
|
package/dist/src/blueprint.d.ts
CHANGED
package/dist/src/blueprint.js
CHANGED
|
@@ -54,6 +54,7 @@ export function deriveBlueprint(spec) {
|
|
|
54
54
|
journeys: spec.journeys,
|
|
55
55
|
promises: spec.promises,
|
|
56
56
|
network: spec.network,
|
|
57
|
+
...(spec.uses.length ? { uses: spec.uses } : {}),
|
|
57
58
|
...(spec.module.icon ? { icon: spec.module.icon } : {}),
|
|
58
59
|
...(spec.module.category ? { category: spec.module.category } : {}),
|
|
59
60
|
...(spec.module.tags?.length ? { tags: spec.module.tags } : {}),
|
package/dist/src/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { dates, logicProblems, Rules, fromCel, toCel, MAX_RANGE } from "./logic.
|
|
|
7
7
|
export { leakProblems } from "./leak.js";
|
|
8
8
|
export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
|
|
9
9
|
export { deriveBlueprint, type BlueprintV1 } from "./blueprint.js";
|
|
10
|
+
export { USES, USE_NAMES, NEEDS_ORIGIN, featuresOf, type Use } from "./uses.js";
|
package/dist/src/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { dates, logicProblems, Rules, fromCel, toCel, MAX_RANGE } from "./logic.
|
|
|
9
9
|
export { leakProblems } from "./leak.js";
|
|
10
10
|
export { CLOUDCRANE_CATALOG, CLOUDCRANE_CATALOG_ID, catalogFor, cloudcraneCatalog } from "./catalog.js";
|
|
11
11
|
export { deriveBlueprint } from "./blueprint.js";
|
|
12
|
+
export { USES, USE_NAMES, NEEDS_ORIGIN, featuresOf } from "./uses.js";
|
package/dist/src/schema.d.ts
CHANGED
|
@@ -18,42 +18,57 @@ export declare const Tokens: z.ZodType<TokenNode>;
|
|
|
18
18
|
export declare const CODED_TYPES: readonly ["number", "string", "boolean", "list", "map"];
|
|
19
19
|
/**
|
|
20
20
|
* A coded component: what the catalog can't draw, as the spec's own code, run
|
|
21
|
-
* sandboxed in a frame with a kit (`game`: a loop, a canvas, touch and keys
|
|
21
|
+
* sandboxed in a frame with a kit (`game`: a loop, a canvas, touch and keys;
|
|
22
|
+
* `page`: a whole HTML page, full screen). Either saves through `localStorage`,
|
|
23
|
+
* which the frame's bridge keeps on the page under the spec's data key.
|
|
22
24
|
* It's given only its declared inputs and may only send its declared events;
|
|
23
25
|
* a screen places it with the catalog's Coded component.
|
|
24
26
|
*/
|
|
25
|
-
export declare const CodedComponent: z.ZodEffects<z.ZodObject<{
|
|
27
|
+
export declare const CodedComponent: z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
26
28
|
kind: z.ZodLiteral<"coded">;
|
|
27
|
-
|
|
29
|
+
/** game: a canvas, a loop and touch, the code a script; page: a whole HTML page, drawn full screen (what an app written as HTML becomes). */
|
|
30
|
+
kit: z.ZodEnum<["game", "page"]>;
|
|
28
31
|
inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<["number", "string", "boolean", "list", "map"]>>>;
|
|
29
32
|
events: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodEnum<["number", "string", "boolean", "list", "map"]>>>>;
|
|
30
|
-
code: z.
|
|
33
|
+
code: z.ZodString;
|
|
31
34
|
}, "strict", z.ZodTypeAny, {
|
|
32
35
|
code: string;
|
|
33
36
|
kind: "coded";
|
|
34
|
-
kit: "game";
|
|
37
|
+
kit: "game" | "page";
|
|
35
38
|
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
36
39
|
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
37
40
|
}, {
|
|
38
41
|
code: string;
|
|
39
42
|
kind: "coded";
|
|
40
|
-
kit: "game";
|
|
43
|
+
kit: "game" | "page";
|
|
41
44
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
42
45
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
43
46
|
}>, {
|
|
44
47
|
code: string;
|
|
45
48
|
kind: "coded";
|
|
46
|
-
kit: "game";
|
|
49
|
+
kit: "game" | "page";
|
|
47
50
|
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
48
51
|
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
49
52
|
}, {
|
|
50
53
|
code: string;
|
|
51
54
|
kind: "coded";
|
|
52
|
-
kit: "game";
|
|
55
|
+
kit: "game" | "page";
|
|
56
|
+
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
57
|
+
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
58
|
+
}>, {
|
|
59
|
+
code: string;
|
|
60
|
+
kind: "coded";
|
|
61
|
+
kit: "game" | "page";
|
|
62
|
+
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
63
|
+
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
64
|
+
}, {
|
|
65
|
+
code: string;
|
|
66
|
+
kind: "coded";
|
|
67
|
+
kit: "game" | "page";
|
|
53
68
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
54
69
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
55
70
|
}>;
|
|
56
|
-
export declare const SpecSchema: z.ZodObject<{
|
|
71
|
+
export declare const SpecSchema: z.ZodEffects<z.ZodObject<{
|
|
57
72
|
v: z.ZodLiteral<1>;
|
|
58
73
|
manifest: z.ZodObject<{
|
|
59
74
|
runtime: z.ZodString;
|
|
@@ -251,6 +266,17 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
251
266
|
site: string;
|
|
252
267
|
why: string;
|
|
253
268
|
}>, "many">>;
|
|
269
|
+
/** The device features the app uses, by CloudCrane's names, each once with why; anything not listed is blocked. */
|
|
270
|
+
uses: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
271
|
+
what: z.ZodEnum<["camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc", ...("camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc")[]]>;
|
|
272
|
+
why: z.ZodString;
|
|
273
|
+
}, "strict", z.ZodTypeAny, {
|
|
274
|
+
why: string;
|
|
275
|
+
what: "camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc";
|
|
276
|
+
}, {
|
|
277
|
+
why: string;
|
|
278
|
+
what: "camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc";
|
|
279
|
+
}>, "many">>;
|
|
254
280
|
journeys: z.ZodArray<z.ZodObject<{
|
|
255
281
|
id: z.ZodString;
|
|
256
282
|
title: z.ZodString;
|
|
@@ -295,46 +321,59 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
295
321
|
/** CloudCrane's test steps, as its blueprint tests take them; CloudCrane checks their shape. */
|
|
296
322
|
tests: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">>;
|
|
297
323
|
/** Coded components, by name: the spec's own code for what the catalog can't draw. */
|
|
298
|
-
components: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
|
|
324
|
+
components: z.ZodDefault<z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
299
325
|
kind: z.ZodLiteral<"coded">;
|
|
300
|
-
|
|
326
|
+
/** game: a canvas, a loop and touch, the code a script; page: a whole HTML page, drawn full screen (what an app written as HTML becomes). */
|
|
327
|
+
kit: z.ZodEnum<["game", "page"]>;
|
|
301
328
|
inputs: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<["number", "string", "boolean", "list", "map"]>>>;
|
|
302
329
|
events: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodEnum<["number", "string", "boolean", "list", "map"]>>>>;
|
|
303
|
-
code: z.
|
|
330
|
+
code: z.ZodString;
|
|
304
331
|
}, "strict", z.ZodTypeAny, {
|
|
305
332
|
code: string;
|
|
306
333
|
kind: "coded";
|
|
307
|
-
kit: "game";
|
|
334
|
+
kit: "game" | "page";
|
|
308
335
|
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
309
336
|
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
310
337
|
}, {
|
|
311
338
|
code: string;
|
|
312
339
|
kind: "coded";
|
|
313
|
-
kit: "game";
|
|
340
|
+
kit: "game" | "page";
|
|
314
341
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
315
342
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
316
343
|
}>, {
|
|
317
344
|
code: string;
|
|
318
345
|
kind: "coded";
|
|
319
|
-
kit: "game";
|
|
346
|
+
kit: "game" | "page";
|
|
320
347
|
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
321
348
|
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
322
349
|
}, {
|
|
323
350
|
code: string;
|
|
324
351
|
kind: "coded";
|
|
325
|
-
kit: "game";
|
|
352
|
+
kit: "game" | "page";
|
|
353
|
+
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
354
|
+
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
355
|
+
}>, {
|
|
356
|
+
code: string;
|
|
357
|
+
kind: "coded";
|
|
358
|
+
kit: "game" | "page";
|
|
359
|
+
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
360
|
+
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
361
|
+
}, {
|
|
362
|
+
code: string;
|
|
363
|
+
kind: "coded";
|
|
364
|
+
kit: "game" | "page";
|
|
326
365
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
327
366
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
328
367
|
}>>, Record<string, {
|
|
329
368
|
code: string;
|
|
330
369
|
kind: "coded";
|
|
331
|
-
kit: "game";
|
|
370
|
+
kit: "game" | "page";
|
|
332
371
|
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
333
372
|
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
334
373
|
}>, Record<string, {
|
|
335
374
|
code: string;
|
|
336
375
|
kind: "coded";
|
|
337
|
-
kit: "game";
|
|
376
|
+
kit: "game" | "page";
|
|
338
377
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
339
378
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
340
379
|
}>>>;
|
|
@@ -353,7 +392,177 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
353
392
|
components: Record<string, {
|
|
354
393
|
code: string;
|
|
355
394
|
kind: "coded";
|
|
356
|
-
kit: "game";
|
|
395
|
+
kit: "game" | "page";
|
|
396
|
+
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
397
|
+
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
398
|
+
}>;
|
|
399
|
+
surface: {
|
|
400
|
+
catalog: string;
|
|
401
|
+
messages: Record<string, unknown>[];
|
|
402
|
+
};
|
|
403
|
+
v: 1;
|
|
404
|
+
manifest: {
|
|
405
|
+
runtime: string;
|
|
406
|
+
generator: string;
|
|
407
|
+
source: {
|
|
408
|
+
fingerprint: string;
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
module: {
|
|
412
|
+
name: string;
|
|
413
|
+
id: string;
|
|
414
|
+
purpose: string;
|
|
415
|
+
icon?: string | undefined;
|
|
416
|
+
category?: string | undefined;
|
|
417
|
+
tags?: string[] | undefined;
|
|
418
|
+
};
|
|
419
|
+
tokens: TokenNode;
|
|
420
|
+
data: {
|
|
421
|
+
version: number;
|
|
422
|
+
key: string;
|
|
423
|
+
persist: string;
|
|
424
|
+
schema: Record<string, unknown>;
|
|
425
|
+
views: {
|
|
426
|
+
path: string;
|
|
427
|
+
source: string;
|
|
428
|
+
}[];
|
|
429
|
+
migrations: {
|
|
430
|
+
from: number;
|
|
431
|
+
to: number;
|
|
432
|
+
cel: string;
|
|
433
|
+
}[];
|
|
434
|
+
initial?: unknown;
|
|
435
|
+
};
|
|
436
|
+
logic: {
|
|
437
|
+
consts: Record<string, string | number | boolean>;
|
|
438
|
+
functions: Record<string, {
|
|
439
|
+
params: string[];
|
|
440
|
+
cel: string;
|
|
441
|
+
}>;
|
|
442
|
+
computed: Record<string, string>;
|
|
443
|
+
vectors: {
|
|
444
|
+
from: "recorded" | "hint";
|
|
445
|
+
fn: string;
|
|
446
|
+
in: unknown[];
|
|
447
|
+
out?: unknown;
|
|
448
|
+
}[];
|
|
449
|
+
};
|
|
450
|
+
network: {
|
|
451
|
+
site: string;
|
|
452
|
+
why: string;
|
|
453
|
+
}[];
|
|
454
|
+
uses: {
|
|
455
|
+
why: string;
|
|
456
|
+
what: "camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc";
|
|
457
|
+
}[];
|
|
458
|
+
journeys: {
|
|
459
|
+
title: string;
|
|
460
|
+
id: string;
|
|
461
|
+
steps: {
|
|
462
|
+
text: string;
|
|
463
|
+
id: string;
|
|
464
|
+
}[];
|
|
465
|
+
}[];
|
|
466
|
+
promises: {
|
|
467
|
+
text: string;
|
|
468
|
+
id: string;
|
|
469
|
+
kind?: "data" | undefined;
|
|
470
|
+
}[];
|
|
471
|
+
tests: Record<string, unknown>[];
|
|
472
|
+
mods: {
|
|
473
|
+
open: string[];
|
|
474
|
+
locked: string[];
|
|
475
|
+
};
|
|
476
|
+
}, {
|
|
477
|
+
surface: {
|
|
478
|
+
catalog: string;
|
|
479
|
+
messages: Record<string, unknown>[];
|
|
480
|
+
};
|
|
481
|
+
v: 1;
|
|
482
|
+
manifest: {
|
|
483
|
+
runtime: string;
|
|
484
|
+
generator: string;
|
|
485
|
+
source: {
|
|
486
|
+
fingerprint: string;
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
module: {
|
|
490
|
+
name: string;
|
|
491
|
+
id: string;
|
|
492
|
+
purpose: string;
|
|
493
|
+
icon?: string | undefined;
|
|
494
|
+
category?: string | undefined;
|
|
495
|
+
tags?: string[] | undefined;
|
|
496
|
+
};
|
|
497
|
+
tokens: TokenNode;
|
|
498
|
+
data: {
|
|
499
|
+
version: number;
|
|
500
|
+
key: string;
|
|
501
|
+
persist: string;
|
|
502
|
+
schema: Record<string, unknown>;
|
|
503
|
+
migrations: {
|
|
504
|
+
from: number;
|
|
505
|
+
to: number;
|
|
506
|
+
cel: string;
|
|
507
|
+
}[];
|
|
508
|
+
initial?: unknown;
|
|
509
|
+
views?: {
|
|
510
|
+
path: string;
|
|
511
|
+
source: string;
|
|
512
|
+
}[] | undefined;
|
|
513
|
+
};
|
|
514
|
+
logic: {
|
|
515
|
+
consts?: Record<string, string | number | boolean> | undefined;
|
|
516
|
+
functions?: Record<string, {
|
|
517
|
+
params: string[];
|
|
518
|
+
cel: string;
|
|
519
|
+
}> | undefined;
|
|
520
|
+
computed?: Record<string, string> | undefined;
|
|
521
|
+
vectors?: {
|
|
522
|
+
from: "recorded" | "hint";
|
|
523
|
+
fn: string;
|
|
524
|
+
in: unknown[];
|
|
525
|
+
out?: unknown;
|
|
526
|
+
}[] | undefined;
|
|
527
|
+
};
|
|
528
|
+
journeys: {
|
|
529
|
+
title: string;
|
|
530
|
+
id: string;
|
|
531
|
+
steps: {
|
|
532
|
+
text: string;
|
|
533
|
+
id: string;
|
|
534
|
+
}[];
|
|
535
|
+
}[];
|
|
536
|
+
components?: Record<string, {
|
|
537
|
+
code: string;
|
|
538
|
+
kind: "coded";
|
|
539
|
+
kit: "game" | "page";
|
|
540
|
+
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
541
|
+
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
542
|
+
}> | undefined;
|
|
543
|
+
network?: {
|
|
544
|
+
site: string;
|
|
545
|
+
why: string;
|
|
546
|
+
}[] | undefined;
|
|
547
|
+
uses?: {
|
|
548
|
+
why: string;
|
|
549
|
+
what: "camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc";
|
|
550
|
+
}[] | undefined;
|
|
551
|
+
promises?: {
|
|
552
|
+
text: string;
|
|
553
|
+
id: string;
|
|
554
|
+
kind?: "data" | undefined;
|
|
555
|
+
}[] | undefined;
|
|
556
|
+
tests?: Record<string, unknown>[] | undefined;
|
|
557
|
+
mods?: {
|
|
558
|
+
open: string[];
|
|
559
|
+
locked: string[];
|
|
560
|
+
} | undefined;
|
|
561
|
+
}>, {
|
|
562
|
+
components: Record<string, {
|
|
563
|
+
code: string;
|
|
564
|
+
kind: "coded";
|
|
565
|
+
kit: "game" | "page";
|
|
357
566
|
inputs: Record<string, "string" | "number" | "boolean" | "map" | "list">;
|
|
358
567
|
events: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">>;
|
|
359
568
|
}>;
|
|
@@ -412,6 +621,10 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
412
621
|
site: string;
|
|
413
622
|
why: string;
|
|
414
623
|
}[];
|
|
624
|
+
uses: {
|
|
625
|
+
why: string;
|
|
626
|
+
what: "camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc";
|
|
627
|
+
}[];
|
|
415
628
|
journeys: {
|
|
416
629
|
title: string;
|
|
417
630
|
id: string;
|
|
@@ -493,7 +706,7 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
493
706
|
components?: Record<string, {
|
|
494
707
|
code: string;
|
|
495
708
|
kind: "coded";
|
|
496
|
-
kit: "game";
|
|
709
|
+
kit: "game" | "page";
|
|
497
710
|
inputs?: Record<string, "string" | "number" | "boolean" | "map" | "list"> | undefined;
|
|
498
711
|
events?: Record<string, Record<string, "string" | "number" | "boolean" | "map" | "list">> | undefined;
|
|
499
712
|
}> | undefined;
|
|
@@ -501,6 +714,10 @@ export declare const SpecSchema: z.ZodObject<{
|
|
|
501
714
|
site: string;
|
|
502
715
|
why: string;
|
|
503
716
|
}[] | undefined;
|
|
717
|
+
uses?: {
|
|
718
|
+
why: string;
|
|
719
|
+
what: "camera" | "microphone" | "location" | "motion" | "screen-awake" | "gamepad" | "voice" | "contacts" | "nfc";
|
|
720
|
+
}[] | undefined;
|
|
504
721
|
promises?: {
|
|
505
722
|
text: string;
|
|
506
723
|
id: string;
|
package/dist/src/schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { NEEDS_ORIGIN, USE_NAMES } from "./uses.js";
|
|
2
3
|
// The CloudCrane app spec: an A2UI surface (the screens) in a small envelope for
|
|
3
4
|
// what A2UI doesn't cover. Nothing in it is code from the app it came from:
|
|
4
5
|
// screens are A2UI components, the look is DTCG design tokens, rules are CEL
|
|
@@ -49,23 +50,35 @@ export const CODED_TYPES = ["number", "string", "boolean", "list", "map"];
|
|
|
49
50
|
const CodedType = z.enum(CODED_TYPES);
|
|
50
51
|
/**
|
|
51
52
|
* A coded component: what the catalog can't draw, as the spec's own code, run
|
|
52
|
-
* sandboxed in a frame with a kit (`game`: a loop, a canvas, touch and keys
|
|
53
|
+
* sandboxed in a frame with a kit (`game`: a loop, a canvas, touch and keys;
|
|
54
|
+
* `page`: a whole HTML page, full screen). Either saves through `localStorage`,
|
|
55
|
+
* which the frame's bridge keeps on the page under the spec's data key.
|
|
53
56
|
* It's given only its declared inputs and may only send its declared events;
|
|
54
57
|
* a screen places it with the catalog's Coded component.
|
|
55
58
|
*/
|
|
56
59
|
export const CodedComponent = z
|
|
57
60
|
.object({
|
|
58
61
|
kind: z.literal("coded"),
|
|
59
|
-
|
|
62
|
+
/** game: a canvas, a loop and touch, the code a script; page: a whole HTML page, drawn full screen (what an app written as HTML becomes). */
|
|
63
|
+
kit: z.enum(["game", "page"]),
|
|
60
64
|
inputs: z.record(CelName, CodedType).default({}),
|
|
61
65
|
events: z.record(CelName, z.record(CelName, CodedType)).default({}),
|
|
62
66
|
code: z
|
|
63
67
|
.string()
|
|
64
68
|
.min(1)
|
|
65
|
-
.max(
|
|
66
|
-
.refine((c) => !/<\/script/i.test(c), "can't contain </script>, which would end the frame's script"),
|
|
69
|
+
.max(1024 * 1024),
|
|
67
70
|
})
|
|
68
71
|
.strict()
|
|
72
|
+
.superRefine((c, ctx) => {
|
|
73
|
+
if (c.kit === "game" && /<\/script/i.test(c.code))
|
|
74
|
+
ctx.addIssue({
|
|
75
|
+
code: z.ZodIssueCode.custom,
|
|
76
|
+
path: ["code"],
|
|
77
|
+
message: "can't contain </script>, which would end the frame's script (a page kit's code can)",
|
|
78
|
+
});
|
|
79
|
+
if (c.kit === "game" && c.code.length > 64 * 1024)
|
|
80
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["code"], message: "a game kit's code is at most 64 KB" });
|
|
81
|
+
})
|
|
69
82
|
.refine((c) => Object.keys(c.inputs).length <= 10 && Object.keys(c.events).length <= 10, "at most 10 inputs and 10 events");
|
|
70
83
|
export const SpecSchema = z
|
|
71
84
|
.object({
|
|
@@ -141,6 +154,11 @@ export const SpecSchema = z
|
|
|
141
154
|
.array(z.object({ site: z.string().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/), why: Text(300) }).strict())
|
|
142
155
|
.max(5)
|
|
143
156
|
.default([]),
|
|
157
|
+
/** The device features the app uses, by CloudCrane's names, each once with why; anything not listed is blocked. */
|
|
158
|
+
uses: z
|
|
159
|
+
.array(z.object({ what: z.enum(USE_NAMES), why: Text(300) }).strict())
|
|
160
|
+
.max(USE_NAMES.length)
|
|
161
|
+
.default([]),
|
|
144
162
|
journeys: z
|
|
145
163
|
.array(z
|
|
146
164
|
.object({
|
|
@@ -164,4 +182,15 @@ export const SpecSchema = z
|
|
|
164
182
|
.strict()
|
|
165
183
|
.default({ open: [], locked: [] }),
|
|
166
184
|
})
|
|
167
|
-
.strict()
|
|
185
|
+
.strict()
|
|
186
|
+
.superRefine((s, ctx) => {
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const u of s.uses) {
|
|
189
|
+
if (seen.has(u.what))
|
|
190
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["uses"], message: `${u.what} is listed twice` });
|
|
191
|
+
seen.add(u.what);
|
|
192
|
+
}
|
|
193
|
+
const needsOrigin = s.uses.some((u) => NEEDS_ORIGIN.includes(u.what));
|
|
194
|
+
if (needsOrigin && !Object.values(s.components).some((c) => c.kit === "page"))
|
|
195
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["uses"], message: "the camera and microphone work only in a page-kit component" });
|
|
196
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const USES: {
|
|
2
|
+
readonly camera: readonly ["camera"];
|
|
3
|
+
readonly microphone: readonly ["microphone"];
|
|
4
|
+
readonly location: readonly ["geolocation"];
|
|
5
|
+
readonly motion: readonly ["accelerometer", "gyroscope", "magnetometer"];
|
|
6
|
+
readonly "screen-awake": readonly ["screen-wake-lock"];
|
|
7
|
+
readonly gamepad: readonly ["gamepad"];
|
|
8
|
+
readonly voice: readonly ["microphone"];
|
|
9
|
+
readonly contacts: readonly [];
|
|
10
|
+
readonly nfc: readonly [];
|
|
11
|
+
};
|
|
12
|
+
export type Use = keyof typeof USES;
|
|
13
|
+
export declare const USE_NAMES: [Use, ...Use[]];
|
|
14
|
+
/** What a frame with no origin of its own can't have, whatever it's allowed: the browser gives it no camera or microphone. */
|
|
15
|
+
export declare const NEEDS_ORIGIN: readonly Use[];
|
|
16
|
+
/** The browser features for the uses declared, each once, in a fixed order. */
|
|
17
|
+
export declare function featuresOf(uses: readonly {
|
|
18
|
+
what: string;
|
|
19
|
+
}[]): string[];
|
package/dist/src/uses.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The device features an app can declare, by CloudCrane's names, and the
|
|
2
|
+
// browser's Permissions-Policy features each needs. A coded component's frame
|
|
3
|
+
// is allowed those of the features the spec declares. The contact picker and
|
|
4
|
+
// NFC have no browser feature: CloudCrane's lockdown is their only block.
|
|
5
|
+
export const USES = {
|
|
6
|
+
camera: ["camera"],
|
|
7
|
+
microphone: ["microphone"],
|
|
8
|
+
location: ["geolocation"],
|
|
9
|
+
motion: ["accelerometer", "gyroscope", "magnetometer"],
|
|
10
|
+
"screen-awake": ["screen-wake-lock"],
|
|
11
|
+
gamepad: ["gamepad"],
|
|
12
|
+
voice: ["microphone"],
|
|
13
|
+
contacts: [],
|
|
14
|
+
nfc: [],
|
|
15
|
+
};
|
|
16
|
+
export const USE_NAMES = Object.keys(USES);
|
|
17
|
+
/** What a frame with no origin of its own can't have, whatever it's allowed: the browser gives it no camera or microphone. */
|
|
18
|
+
export const NEEDS_ORIGIN = ["camera", "microphone"];
|
|
19
|
+
/** The browser features for the uses declared, each once, in a fixed order. */
|
|
20
|
+
export function featuresOf(uses) {
|
|
21
|
+
const declared = new Set(uses.map((u) => u.what));
|
|
22
|
+
const out = new Set();
|
|
23
|
+
for (const name of USE_NAMES)
|
|
24
|
+
if (declared.has(name))
|
|
25
|
+
for (const f of USES[name])
|
|
26
|
+
out.add(f);
|
|
27
|
+
return [...out];
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcrane/app-spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The CloudCrane app spec: its schema, the expression language its rules are written in, and the checks that keep partner code out of it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudcrane",
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"types": "./dist/src/blueprint.d.ts",
|
|
47
47
|
"default": "./dist/src/blueprint.js"
|
|
48
48
|
},
|
|
49
|
+
"./uses": {
|
|
50
|
+
"types": "./dist/src/uses.d.ts",
|
|
51
|
+
"default": "./dist/src/uses.js"
|
|
52
|
+
},
|
|
49
53
|
"./schema.json": "./schema/app-spec.v1.json",
|
|
50
54
|
"./package.json": "./package.json"
|
|
51
55
|
},
|
package/schema/app-spec.v1.json
CHANGED
|
@@ -333,6 +333,40 @@
|
|
|
333
333
|
"maxItems": 5,
|
|
334
334
|
"default": []
|
|
335
335
|
},
|
|
336
|
+
"uses": {
|
|
337
|
+
"type": "array",
|
|
338
|
+
"items": {
|
|
339
|
+
"type": "object",
|
|
340
|
+
"properties": {
|
|
341
|
+
"what": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"enum": [
|
|
344
|
+
"camera",
|
|
345
|
+
"microphone",
|
|
346
|
+
"location",
|
|
347
|
+
"motion",
|
|
348
|
+
"screen-awake",
|
|
349
|
+
"gamepad",
|
|
350
|
+
"voice",
|
|
351
|
+
"contacts",
|
|
352
|
+
"nfc"
|
|
353
|
+
]
|
|
354
|
+
},
|
|
355
|
+
"why": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"minLength": 1,
|
|
358
|
+
"maxLength": 300
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
"required": [
|
|
362
|
+
"what",
|
|
363
|
+
"why"
|
|
364
|
+
],
|
|
365
|
+
"additionalProperties": false
|
|
366
|
+
},
|
|
367
|
+
"maxItems": 9,
|
|
368
|
+
"default": []
|
|
369
|
+
},
|
|
336
370
|
"journeys": {
|
|
337
371
|
"type": "array",
|
|
338
372
|
"items": {
|
|
@@ -428,7 +462,8 @@
|
|
|
428
462
|
"kit": {
|
|
429
463
|
"type": "string",
|
|
430
464
|
"enum": [
|
|
431
|
-
"game"
|
|
465
|
+
"game",
|
|
466
|
+
"page"
|
|
432
467
|
]
|
|
433
468
|
},
|
|
434
469
|
"inputs": {
|
|
@@ -474,7 +509,7 @@
|
|
|
474
509
|
"code": {
|
|
475
510
|
"type": "string",
|
|
476
511
|
"minLength": 1,
|
|
477
|
-
"maxLength":
|
|
512
|
+
"maxLength": 1048576
|
|
478
513
|
}
|
|
479
514
|
},
|
|
480
515
|
"required": [
|