@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 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`), the inputs it's given and the events it sends.
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.
@@ -16,6 +16,7 @@ export type BlueprintV1 = {
16
16
  journeys: Spec["journeys"];
17
17
  promises: Spec["promises"];
18
18
  network: Spec["network"];
19
+ uses?: Spec["uses"];
19
20
  icon?: string;
20
21
  category?: string;
21
22
  tags?: string[];
@@ -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 } : {}),
@@ -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";
@@ -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
- kit: z.ZodEnum<["game"]>;
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.ZodEffects<z.ZodString, string, string>;
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
- kit: z.ZodEnum<["game"]>;
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.ZodEffects<z.ZodString, string, string>;
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;
@@ -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
- kit: z.enum(["game"]),
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(64 * 1024)
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[];
@@ -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.2.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
  },
@@ -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": 65536
512
+ "maxLength": 1048576
478
513
  }
479
514
  },
480
515
  "required": [