@seekprotocol/sdk 0.0.0-stage → 0.1.1
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/LICENSE.md +77 -0
- package/README.md +30 -2
- package/assets.d.ts +67 -0
- package/builders.d.ts +58 -0
- package/compile.d.ts +15 -0
- package/games.d.ts +73 -0
- package/index.d.ts +10 -0
- package/index.js +5 -0
- package/log.d.ts +26 -0
- package/package.d.ts +25 -0
- package/package.json +24 -4
- package/sign.d.ts +17 -0
- package/types.d.ts +189 -0
- package/validate.d.ts +61 -0
- package/verify.d.ts +12 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Seek Protocol SDK License
|
|
2
|
+
|
|
3
|
+
**Draft of 10 November 2026. A lawyer has not reviewed this text yet.**
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 Seek Protocol (https://www.seekprotocol.ai/en). All rights
|
|
6
|
+
reserved.
|
|
7
|
+
|
|
8
|
+
This license covers the npm packages `@seekprotocol/sdk` and
|
|
9
|
+
`@seekprotocol/cli` (the "Software"), including the engine the CLI carries.
|
|
10
|
+
The GameKit bridge, `@seekprotocol/game`, has its own MIT license.
|
|
11
|
+
|
|
12
|
+
## 1. What you may do
|
|
13
|
+
|
|
14
|
+
Seek Protocol ("Seek") gives you a free, worldwide, non-exclusive,
|
|
15
|
+
non-transferable license to install and use the Software, on as many computers
|
|
16
|
+
as you like, to build, test, sign and submit quests and minigames for Seek, and
|
|
17
|
+
to run them in the Seek simulator.
|
|
18
|
+
|
|
19
|
+
You may keep the Software in your own projects and repositories, and install it
|
|
20
|
+
in your build and CI systems, as long as it is installed from npm and not
|
|
21
|
+
changed.
|
|
22
|
+
|
|
23
|
+
## 2. What you may not do
|
|
24
|
+
|
|
25
|
+
You may not:
|
|
26
|
+
|
|
27
|
+
- use the Software, or any part of it, to build or run quests, games or a
|
|
28
|
+
platform for anything other than Seek;
|
|
29
|
+
- copy, change, translate or redistribute the Software, other than installing
|
|
30
|
+
it as described above;
|
|
31
|
+
- decompile, de-minify or reverse engineer the Software, or try to recover its
|
|
32
|
+
source code, except where the law gives you that right and it cannot be
|
|
33
|
+
excluded;
|
|
34
|
+
- remove or change this license or Seek's copyright notices;
|
|
35
|
+
- use the Software to get around Store Connect, App Review or any limit Seek
|
|
36
|
+
sets.
|
|
37
|
+
|
|
38
|
+
## 3. What you make
|
|
39
|
+
|
|
40
|
+
What you make with the Software is yours. Submitting a quest or minigame to
|
|
41
|
+
Seek, and what Seek may do with it, falls under the Seek Maker Agreement, which
|
|
42
|
+
you accept in Store Connect.
|
|
43
|
+
|
|
44
|
+
## 4. Updates and changes
|
|
45
|
+
|
|
46
|
+
Seek may publish new versions of the Software and may change this license for
|
|
47
|
+
new versions. A version you installed stays under the license it came with.
|
|
48
|
+
Store Connect may refuse bundles built with versions Seek no longer supports.
|
|
49
|
+
|
|
50
|
+
## 5. No warranty
|
|
51
|
+
|
|
52
|
+
The Software is provided "as is", without warranty of any kind, express or
|
|
53
|
+
implied, including the warranties of merchantability, fitness for a particular
|
|
54
|
+
purpose and non-infringement.
|
|
55
|
+
|
|
56
|
+
## 6. Liability
|
|
57
|
+
|
|
58
|
+
Seek is not liable for indirect loss or lost profit arising from the Software
|
|
59
|
+
or its use. Seek's total liability under this license is limited to EUR 500,
|
|
60
|
+
except where the law does not allow a limit. Where you have also accepted the
|
|
61
|
+
Seek Maker Agreement, its liability section applies instead.
|
|
62
|
+
|
|
63
|
+
## 7. End
|
|
64
|
+
|
|
65
|
+
This license ends automatically when you break it. You then stop using the
|
|
66
|
+
Software and remove it from your systems.
|
|
67
|
+
|
|
68
|
+
## 8. Third-party software
|
|
69
|
+
|
|
70
|
+
The Software includes open-source code from others, under their own licenses.
|
|
71
|
+
They are listed, with their license texts, in `THIRD_PARTY_NOTICES.md` in the
|
|
72
|
+
package.
|
|
73
|
+
|
|
74
|
+
## 9. Law
|
|
75
|
+
|
|
76
|
+
Dutch law applies. Disputes go to the competent court in the Netherlands,
|
|
77
|
+
unless the law of your country gives you a right to go to your own court.
|
package/README.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @seekprotocol/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Write quests for the Seek app in TypeScript. Made by
|
|
4
|
+
[Seek Protocol](https://www.seekprotocol.ai/en).
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { quest, step, zone, reward, t } from '@seekprotocol/sdk';
|
|
8
|
+
|
|
9
|
+
const bridge = zone.place('Erasmusbrug', { lat: 51.9094, lng: 4.4868 });
|
|
10
|
+
|
|
11
|
+
export default quest({
|
|
12
|
+
id: 'harbour-walk',
|
|
13
|
+
title: t('Harbour walk'),
|
|
14
|
+
area: zone.city('Rotterdam', { lat: 51.9225, lng: 4.4792 }, 8),
|
|
15
|
+
capabilities: ['location.markers', 'catch', 'spawns.place'],
|
|
16
|
+
declarations: { places: 'public', effort: 'light', hazards: false, ai: 'none', sponsored: false, gameEvents: false, rights: true },
|
|
17
|
+
steps: [
|
|
18
|
+
step.talk({ id: 'captain', giver: 'harbour-master', at: bridge, title: t('Meet the harbour master') }),
|
|
19
|
+
step.catch({ id: 'cargo', coin: 'SEEK', count: 3, near: bridge, title: t('Catch 3 SEEK near the bridge') }),
|
|
20
|
+
],
|
|
21
|
+
rewards: [reward.xp(300)],
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Test and submit it with the `seek` command from
|
|
26
|
+
[`@seekprotocol/cli`](https://www.npmjs.com/package/@seekprotocol/cli).
|
|
27
|
+
|
|
28
|
+
- Store Connect: https://storeconnect.seekprotocol.ai
|
|
29
|
+
- Seek Protocol: https://www.seekprotocol.ai/en
|
|
30
|
+
|
|
31
|
+
Copyright (c) 2026 Seek Protocol. Licensed under the Seek Protocol SDK License, see `LICENSE.md`.
|
package/assets.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type GameIssue } from "./games.js";
|
|
2
|
+
export declare const ASSET_LIMITS: {
|
|
3
|
+
readonly imageMaxSide: 4096;
|
|
4
|
+
readonly imageAdviseSide: 2048;
|
|
5
|
+
readonly decodedMaxBytes: number;
|
|
6
|
+
readonly decodedAdviseBytes: number;
|
|
7
|
+
readonly startMaxBytes: number;
|
|
8
|
+
readonly startAdviseBytes: number;
|
|
9
|
+
readonly wavMaxBytes: number;
|
|
10
|
+
readonly wavAdviseBytes: number;
|
|
11
|
+
readonly audioMaxSeconds: 600;
|
|
12
|
+
readonly opaquePngAdviseBytes: number;
|
|
13
|
+
readonly gifAdviseBytes: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const COVER_SPEC: {
|
|
16
|
+
readonly maxBytes: number;
|
|
17
|
+
readonly minWidth: 1280;
|
|
18
|
+
readonly minHeight: 720;
|
|
19
|
+
readonly maxWidth: 3840;
|
|
20
|
+
readonly maxHeight: 2160;
|
|
21
|
+
readonly ratio: number;
|
|
22
|
+
readonly ratioTolerance: 0.01;
|
|
23
|
+
readonly types: readonly string[];
|
|
24
|
+
};
|
|
25
|
+
export interface ImageSize {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
alpha?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function imageSize(path: string, bytes: Uint8Array): ImageSize | null;
|
|
31
|
+
export declare function audioSeconds(path: string, bytes: Uint8Array): number | null;
|
|
32
|
+
export declare function startFiles(files: ReadonlyMap<string, Uint8Array>): string[];
|
|
33
|
+
export interface GameAdvice {
|
|
34
|
+
path: string;
|
|
35
|
+
code: string;
|
|
36
|
+
message: string;
|
|
37
|
+
}
|
|
38
|
+
export interface GameAssetReport {
|
|
39
|
+
images: {
|
|
40
|
+
path: string;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
bytes: number;
|
|
44
|
+
decoded: number;
|
|
45
|
+
alpha?: boolean;
|
|
46
|
+
}[];
|
|
47
|
+
sounds: {
|
|
48
|
+
path: string;
|
|
49
|
+
seconds: number | null;
|
|
50
|
+
bytes: number;
|
|
51
|
+
}[];
|
|
52
|
+
decodedBytes: number;
|
|
53
|
+
start: {
|
|
54
|
+
files: string[];
|
|
55
|
+
bytes: number;
|
|
56
|
+
};
|
|
57
|
+
issues: GameIssue[];
|
|
58
|
+
advice: GameAdvice[];
|
|
59
|
+
}
|
|
60
|
+
export declare function analyzeGameAssets(input: Iterable<{
|
|
61
|
+
path: string;
|
|
62
|
+
bytes: Uint8Array;
|
|
63
|
+
}> | ReadonlyMap<string, Uint8Array>): GameAssetReport;
|
|
64
|
+
export declare function coverProblem(bytes: Uint8Array): {
|
|
65
|
+
code: string;
|
|
66
|
+
message: string;
|
|
67
|
+
} | null;
|
package/builders.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Area, ArenaStep, ArriveStep, CatchStep, ChoiceStep, Place, PlayStep, Point, QuestDefinition, Reward, TalkStep, Text, WalkStep } from "./types.js";
|
|
2
|
+
export declare function t(en: string): Text;
|
|
3
|
+
export declare function quest(definition: QuestDefinition): QuestDefinition;
|
|
4
|
+
export declare const zone: {
|
|
5
|
+
anywhere(): Area;
|
|
6
|
+
circle(name: string, center: Point, radiusM: number): Area;
|
|
7
|
+
city(name: string, center: Point, radiusKm?: number): Area;
|
|
8
|
+
place(name: string, at: Point): Place;
|
|
9
|
+
};
|
|
10
|
+
type Common = {
|
|
11
|
+
id: string;
|
|
12
|
+
title: Text;
|
|
13
|
+
story?: Text;
|
|
14
|
+
next?: string | null;
|
|
15
|
+
};
|
|
16
|
+
export declare const step: {
|
|
17
|
+
talk(s: Common & {
|
|
18
|
+
giver: string;
|
|
19
|
+
at?: Place;
|
|
20
|
+
}): TalkStep;
|
|
21
|
+
arrive(s: Common & {
|
|
22
|
+
at: Place;
|
|
23
|
+
radiusM?: number;
|
|
24
|
+
}): ArriveStep;
|
|
25
|
+
catch(s: Common & {
|
|
26
|
+
coin?: string;
|
|
27
|
+
count: number;
|
|
28
|
+
near?: Place;
|
|
29
|
+
withinM?: number;
|
|
30
|
+
}): CatchStep;
|
|
31
|
+
walk(s: Common & {
|
|
32
|
+
steps: number;
|
|
33
|
+
verified?: boolean;
|
|
34
|
+
}): WalkStep;
|
|
35
|
+
arena(s: Common & {
|
|
36
|
+
wins?: number;
|
|
37
|
+
}): ArenaStep;
|
|
38
|
+
choice(s: Common & {
|
|
39
|
+
options: {
|
|
40
|
+
id: string;
|
|
41
|
+
label: Text;
|
|
42
|
+
next: string | null;
|
|
43
|
+
}[];
|
|
44
|
+
}): ChoiceStep;
|
|
45
|
+
play(s: Common & {
|
|
46
|
+
game: string;
|
|
47
|
+
minScore: number;
|
|
48
|
+
maxScore: number;
|
|
49
|
+
}): PlayStep;
|
|
50
|
+
};
|
|
51
|
+
export declare const reward: {
|
|
52
|
+
xp(amount: number): Reward;
|
|
53
|
+
card(card: string): Reward;
|
|
54
|
+
cosmetic(item: string): Reward;
|
|
55
|
+
boost(boost: string, count?: number): Reward;
|
|
56
|
+
pool(coin: string, perPlayer: number, maxPlayers: number): Reward;
|
|
57
|
+
};
|
|
58
|
+
export {};
|
package/compile.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type QuestBundle, type QuestDefinition } from "./types.js";
|
|
2
|
+
import { type Issue } from "./validate.js";
|
|
3
|
+
export declare const VERSION_PATTERN: RegExp;
|
|
4
|
+
export declare class QuestInvalidError extends Error {
|
|
5
|
+
readonly issues: Issue[];
|
|
6
|
+
constructor(issues: Issue[]);
|
|
7
|
+
}
|
|
8
|
+
export interface CompileOptions {
|
|
9
|
+
version: string;
|
|
10
|
+
build: number;
|
|
11
|
+
games?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export declare function compile(q: QuestDefinition, opts: CompileOptions): QuestBundle;
|
|
14
|
+
export declare function canonicalJson(value: unknown): string;
|
|
15
|
+
export declare function bundleHash(bundle: QuestBundle): Promise<string>;
|
package/games.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { GAME_ID_PATTERN } from "./types.js";
|
|
2
|
+
import { type GameAssetReport } from "./assets.js";
|
|
3
|
+
export { GAME_ID_PATTERN };
|
|
4
|
+
export declare const GAME_FORMAT: 1;
|
|
5
|
+
export declare const GAME_APP_ID_PATTERN: RegExp;
|
|
6
|
+
export declare const GAME_HOST_SUFFIX = "play.seekprotocol.ai";
|
|
7
|
+
export declare const GAME_PROXY_ORIGIN = "https://api.play.seekprotocol.ai";
|
|
8
|
+
export declare const GAME_LIMITS: {
|
|
9
|
+
readonly files: 200;
|
|
10
|
+
readonly fileBytes: number;
|
|
11
|
+
readonly totalBytes: number;
|
|
12
|
+
readonly pathLength: 120;
|
|
13
|
+
readonly depth: 5;
|
|
14
|
+
readonly segment: RegExp;
|
|
15
|
+
readonly storageBytes: number;
|
|
16
|
+
};
|
|
17
|
+
export type GameFileKind = "html" | "script" | "style" | "data" | "image" | "audio" | "font";
|
|
18
|
+
export declare const GAME_FILE_TYPES: Record<string, {
|
|
19
|
+
type: string;
|
|
20
|
+
kind: GameFileKind;
|
|
21
|
+
text: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
export declare const GAME_ENTRY = "index.html";
|
|
24
|
+
export interface GameFile {
|
|
25
|
+
path: string;
|
|
26
|
+
size: number;
|
|
27
|
+
sha256: string;
|
|
28
|
+
type: string;
|
|
29
|
+
}
|
|
30
|
+
export interface GameManifest {
|
|
31
|
+
format: typeof GAME_FORMAT;
|
|
32
|
+
id: string;
|
|
33
|
+
entry: typeof GAME_ENTRY;
|
|
34
|
+
files: GameFile[];
|
|
35
|
+
size: number;
|
|
36
|
+
}
|
|
37
|
+
export interface GameIssue {
|
|
38
|
+
path: string;
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
}
|
|
42
|
+
export declare const extensionOf: (path: string) => string;
|
|
43
|
+
export declare const gameFileType: (path: string) => {
|
|
44
|
+
type: string;
|
|
45
|
+
kind: GameFileKind;
|
|
46
|
+
text: boolean;
|
|
47
|
+
} | undefined;
|
|
48
|
+
export declare function gamePathProblem(path: string): GameIssue | null;
|
|
49
|
+
export declare function sniffGameFile(path: string, bytes: Uint8Array): GameIssue | null;
|
|
50
|
+
export declare function validateGameManifest(m: unknown): GameIssue[];
|
|
51
|
+
export declare const gameHash: (m: GameManifest) => Promise<string>;
|
|
52
|
+
export declare function verifyGameFile(f: GameFile, bytes: Uint8Array): Promise<GameIssue | null>;
|
|
53
|
+
export interface PackagedGame {
|
|
54
|
+
manifest: GameManifest;
|
|
55
|
+
hash: string;
|
|
56
|
+
issues: GameIssue[];
|
|
57
|
+
assets: GameAssetReport;
|
|
58
|
+
}
|
|
59
|
+
export declare function packageGame(id: string, files: {
|
|
60
|
+
path: string;
|
|
61
|
+
bytes: Uint8Array;
|
|
62
|
+
}[]): Promise<PackagedGame>;
|
|
63
|
+
export declare function gameAppId(questId: string, gameId: string): Promise<string>;
|
|
64
|
+
export declare const gameOrigin: (appId: string) => string;
|
|
65
|
+
export interface GameFinding {
|
|
66
|
+
path: string;
|
|
67
|
+
line: number;
|
|
68
|
+
code: string;
|
|
69
|
+
severity: "fail" | "flag";
|
|
70
|
+
message: string;
|
|
71
|
+
match: string;
|
|
72
|
+
}
|
|
73
|
+
export declare function lintGameText(path: string, text: string, limit?: number): GameFinding[];
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./builders.js";
|
|
3
|
+
export * from "./validate.js";
|
|
4
|
+
export * from "./compile.js";
|
|
5
|
+
export * from "./sign.js";
|
|
6
|
+
export * from "./log.js";
|
|
7
|
+
export * from "./package.js";
|
|
8
|
+
export * from "./verify.js";
|
|
9
|
+
export * from "./games.js";
|
|
10
|
+
export * from "./assets.js";
|
package/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var F=1,W=1,$=/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/,Q=["location.markers","spawns.place","catch","steps","arena","rewards.cards","rewards.cosmetics","rewards.boosts","rewards.crypto","gamekit"],U={places:["public","private_with_permission"],effort:["light","moderate","hard"],ai:["none","assisted","generated"]};function tt(e){return{en:e}}function nt(e){return e}var st={anywhere(){return{kind:"anywhere"}},circle(e,t,n){return{kind:"circle",name:e,center:t,radiusM:n}},city(e,t,n=10){return{kind:"circle",name:e,center:t,radiusM:Math.round(n*1e3)}},place(e,t){return{name:e,at:t}}},it={talk(e){return{kind:"talk",...e}},arrive(e){return{kind:"arrive",radiusM:50,...e}},catch(e){return{kind:"catch",coin:"any",...e}},walk(e){return{kind:"walk",verified:!1,...e}},arena(e){return{kind:"arena",wins:1,...e}},choice(e){return{kind:"choice",...e}},play(e){return{kind:"play",...e}}},at={xp(e){return{kind:"xp",amount:e}},card(e){return{kind:"card",card:e}},cosmetic(e){return{kind:"cosmetic",item:e}},boost(e,t=1){return{kind:"boost",boost:e,count:t}},pool(e,t,n){return{kind:"pool",coin:e,perPlayer:t,maxPlayers:n}}};var m={steps:{min:1,max:30},idPattern:/^[a-z0-9][a-z0-9-]{2,39}$/,stepIdPattern:/^[a-z0-9][a-z0-9-]{0,39}$/,title:60,summary:160,story:600,areaRadiusM:{min:100,max:5e4},arriveRadiusM:{min:25,max:500},catchCount:{min:1,max:50},catchWithinM:{min:50,max:2e3},walkSteps:{min:100,max:5e4},arenaWins:{min:1,max:20},choiceOptions:{min:2,max:4},xp:{max:5e3},boostCount:{max:3},rewards:{max:6},windowDays:{max:92}},oe={talk:["location.markers"],arrive:["location.markers"],catch:["catch"],walk:["steps"],arena:["arena"],choice:[],play:["gamekit"]};function ce(e,t){let s=(t.lat-e.lat)*Math.PI/180,a=(t.lng-e.lng)*Math.PI/180,o=Math.sin(s/2)**2+Math.cos(e.lat*Math.PI/180)*Math.cos(t.lat*Math.PI/180)*Math.sin(a/2)**2;return 2*6371e3*Math.asin(Math.min(1,Math.sqrt(o)))}var C=e=>!!e&&Number.isFinite(e.lat)&&Number.isFinite(e.lng)&&Math.abs(e.lat)<=85&&Math.abs(e.lng)<=180;function de(e){let t=new Set;for(let n of e.steps){for(let s of oe[n.kind])t.add(s);n.kind==="catch"&&n.near&&t.add("spawns.place")}for(let n of e.rewards)n.kind==="card"&&t.add("rewards.cards"),n.kind==="cosmetic"&&t.add("rewards.cosmetics"),n.kind==="boost"&&t.add("rewards.boosts"),n.kind==="pool"&&t.add("rewards.crypto");return t}function z(e,t){let n=e[t];return n.kind==="choice"?n.options.map(s=>s.next):n.next!==void 0?[n.next]:[e[t+1]?.id??null]}function K(e){let t=[],n=(i,c,l)=>t.push({path:i,code:c,message:l}),s=(i,c,l,f)=>{(!Number.isFinite(c)||c<(l.min??0)||c>l.max)&&n(i,"out_of_range",`${f} must be between ${l.min??0} and ${l.max}.`)},a=(i,c,l,f)=>{if(!c||typeof c.en!="string"||!c.en.trim()){f&&n(i,"text_missing","Text is required.");return}c.en.length>l&&n(i,"text_too_long",`At most ${l} characters (${c.en.length}).`)};m.idPattern.test(e.id??"")||n("id","bad_id","Use 3 to 40 lowercase letters, digits and hyphens."),a("title",e.title,m.title,!0),a("summary",e.summary,m.summary,!1),e.area?.kind==="circle"?(C(e.area.center)||n("area.center","bad_point","Not a valid coordinate."),s("area.radiusM",e.area.radiusM,m.areaRadiusM,"The area radius")):e.area?.kind!=="anywhere"&&n("area","bad_area","Use zone.anywhere(), zone.city() or zone.circle().");let o=(i,c)=>{if(!C(c))return n(i,"bad_point","Not a valid coordinate.");e.area?.kind==="circle"&&C(e.area.center)&&ce(e.area.center,c)>e.area.radiusM&&n(i,"outside_area",`This place is outside the quest area (${e.area.name}).`)};if(e.window){let i=Date.parse(e.window.from),c=Date.parse(e.window.to);!Number.isFinite(i)||!Number.isFinite(c)||c<=i?n("window","bad_window","The window needs a start before its end."):c-i>m.windowDays.max*864e5&&n("window","window_too_long",`A window lasts at most ${m.windowDays.max} days.`)}for(let[i,c]of(e.capabilities??[]).entries())Q.includes(c)||n(`capabilities[${i}]`,"unknown_capability",`Unknown capability "${c}".`);for(let i of de(e))e.capabilities?.includes(i)||n("capabilities","capability_missing",`The quest uses "${i}" but does not declare it.`);let u=e.steps??[];(u.length<m.steps.min||u.length>m.steps.max)&&n("steps","step_count",`A quest has ${m.steps.min} to ${m.steps.max} steps.`);let d=new Set;if(u.forEach((i,c)=>{let l=`steps[${c}]`;switch(m.stepIdPattern.test(i.id??"")||n(`${l}.id`,"bad_id","Use lowercase letters, digits and hyphens."),d.has(i.id)&&n(`${l}.id`,"duplicate_id",`Step id "${i.id}" is used twice.`),d.add(i.id),a(`${l}.title`,i.title,m.title,!0),a(`${l}.story`,i.story,m.story,!1),i.kind){case"talk":i.giver||n(`${l}.giver`,"giver_missing","Name the quest-giver."),i.at?o(`${l}.at`,i.at.at):e.area?.kind!=="circle"&&n(`${l}.at`,"place_missing","A quest playable anywhere needs a place for its giver.");break;case"arrive":o(`${l}.at`,i.at?.at),s(`${l}.radiusM`,i.radiusM,m.arriveRadiusM,"The arrival radius");break;case"catch":s(`${l}.count`,i.count,m.catchCount,"The number of catches"),i.near&&(o(`${l}.near`,i.near.at),s(`${l}.withinM`,i.withinM??500,m.catchWithinM,"The catch radius"));break;case"walk":s(`${l}.steps`,i.steps,m.walkSteps,"The number of steps");break;case"arena":s(`${l}.wins`,i.wins,m.arenaWins,"The number of wins");break;case"choice":{let f=i.options?.length??0;(f<m.choiceOptions.min||f>m.choiceOptions.max)&&n(`${l}.options`,"option_count","A choice has 2 to 4 options.");let h=new Set;i.options?.forEach((g,D)=>{h.has(g.id)&&n(`${l}.options[${D}].id`,"duplicate_id",`Option "${g.id}" is used twice.`),h.add(g.id),a(`${l}.options[${D}].label`,g.label,40,!0)});break}case"play":i.game?$.test(i.game)||n(`${l}.game`,"bad_game_id","A game id is its folder under games/: 3 to 30 lowercase letters, digits and hyphens."):n(`${l}.game`,"game_missing","Name the minigame."),(!(i.maxScore>0)||!(i.minScore>0)||i.minScore>i.maxScore)&&n(`${l}.minScore`,"bad_score","Scores are positive and minScore is at most maxScore.");break;default:n(`${l}.kind`,"unknown_step",`Unknown step kind "${i.kind}".`)}}),u.forEach((i,c)=>{z(u,c).forEach(l=>{l!==null&&!d.has(l)&&n(`steps[${c}].next`,"unknown_next",`No step "${l}".`)})}),u.length&&!t.some(i=>i.code==="unknown_next"||i.code==="duplicate_id")){let i=new Map(u.map((h,g)=>[h.id,g])),c=new Map,l=!1,f=h=>{if(c.get(h)!=="done"){if(c.get(h)==="open"){l=!0;return}c.set(h,"open");for(let g of z(u,i.get(h)))g!==null&&f(g);c.set(h,"done")}};f(u[0].id),l&&n("steps","loop","Steps lead back to an earlier step. A quest has to end."),u.forEach((h,g)=>{c.has(h.id)||n(`steps[${g}]`,"unreachable",`Step "${h.id}" can never be reached.`)})}let r=e.rewards??[];r.length>m.rewards.max&&n("rewards","reward_count",`At most ${m.rewards.max} rewards.`),r.forEach((i,c)=>{let l=`rewards[${c}]`;i.kind==="xp"&&s(`${l}.amount`,i.amount,{min:1,max:m.xp.max},"XP"),i.kind==="boost"&&s(`${l}.count`,i.count,{min:1,max:m.boostCount.max},"The boost count"),i.kind==="pool"&&((!(i.perPlayer>0)||!(i.maxPlayers>0)||!Number.isInteger(i.maxPlayers))&&n(l,"bad_pool","A pool pays a positive amount to a whole number of players."),(e.minAge??18)<18&&n("minAge","pool_needs_adults","A quest that pays coins is for players of 18 and over."))}),r.some(i=>i.kind==="pool")&&e.repeat&&e.repeat!=="never"&&n("repeat","pool_repeats","A quest that pays coins cannot be repeated.");let p=e.declarations;if(!p||typeof p!="object")n("declarations","declarations_missing","Say what the quest asks of players: places, effort, hazards, AI, sponsoring and game events.");else{let i=l=>{U[l].includes(p[l])||n(`declarations.${l}`,"bad_declaration",`One of ${U[l].join(", ")}.`)};i("places"),i("effort"),i("ai");for(let l of["hazards","sponsored","gameEvents"])typeof p[l]!="boolean"&&n(`declarations.${l}`,"bad_declaration","true or false.");p.rights!==!0&&n("declarations.rights","rights_not_declared","Declare rights: true. Only use texts, images, sounds, fonts, characters, logos and code that are yours or that you have a licence for.");let c=(e.steps??[]).reduce((l,f)=>l+(f.kind==="walk"?f.steps:0),0);p.effort==="light"&&c>1e4&&n("declarations.effort","effort_understated",`The walk steps add up to ${c} steps; that is not light.`)}return t}var le=/^(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})$/,T=class extends Error{constructor(n){super(`The quest has ${n.length} problem(s):
|
|
2
|
+
${n.map(s=>` ${s.path}: ${s.message}`).join(`
|
|
3
|
+
`)}`);this.issues=n}};function ue(e,t){if(t===void 0)return[];let n=[];if(!t||typeof t!="object"||Array.isArray(t))return[{path:"games",code:"bad_games",message:"games maps a game id to its hash."}];let s=new Set(e.steps.flatMap(a=>a.kind==="play"?[a.game]:[]));for(let[a,o]of Object.entries(t))$.test(a)||n.push({path:`games.${a}`,code:"bad_game_id",message:"A game id is 3 to 30 lowercase letters, digits and hyphens."}),(typeof o!="string"||!/^[0-9a-f]{64}$/.test(o))&&n.push({path:`games.${a}`,code:"bad_game_hash",message:"A game hash is 64 lowercase hex characters."}),s.has(a)||n.push({path:`games.${a}`,code:"game_unused",message:`No play step uses the game "${a}".`});for(let a of s)a in t||n.push({path:"games",code:"game_missing",message:`The play step's game "${a}" is not in the quest. Put it in games/${a}/.`});return n}function J(e,t){let n=K(e);if(le.test(t.version)||n.push({path:"version",code:"bad_version",message:"Use a version like 1.2.0."}),(!Number.isInteger(t.build)||t.build<1)&&n.push({path:"build",code:"bad_build",message:"The build number is a whole number from 1."}),n.push(...ue(e,t.games)),n.length)throw new T(n);let s={},a=(u,d)=>{if(d)return s[u]=d.en.trim(),u},o=e.steps.map((u,d)=>{let r=`steps.${u.id}`,[p]=z(e.steps,d),{title:i,story:c,next:l,...f}=u,h={...f,title:a(`${r}.title`,i),...c?{story:a(`${r}.story`,c)}:{},next:u.kind==="choice"?null:p??null};return u.kind==="choice"&&(h.options=u.options.map(g=>({id:g.id,label:a(`${r}.options.${g.id}`,g.label),next:g.next}))),h});return{manifest:{format:1,runtime:1,id:e.id,version:t.version,build:t.build,title:a("title",e.title),...e.summary?{summary:a("summary",e.summary)}:{},area:e.area,...e.window?{window:e.window}:{},repeat:e.repeat??"never",minAge:e.minAge??18,declarations:{...e.declarations},capabilities:[...e.capabilities].sort(),rewards:e.rewards,start:e.steps[0].id,...t.games&&Object.keys(t.games).length?{games:{...t.games}}:{}},steps:o,texts:{en:s}}}function M(e){if(e===null||typeof e!="object"){if(typeof e=="number"&&!Number.isFinite(e))throw new Error("Not JSON: a number that is not finite.");return JSON.stringify(e)}return Array.isArray(e)?`[${e.map(M).join(",")}]`:`{${Object.entries(e).filter(([,n])=>n!==void 0).sort(([n],[s])=>n<s?-1:n>s?1:0).map(([n,s])=>`${JSON.stringify(n)}:${M(s)}`).join(",")}}`}var pe=e=>[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join("");async function G(e){let t=new TextEncoder().encode(M(e));return pe(await crypto.subtle.digest("SHA-256",t))}var H=(e,t)=>`seek-bundle-v1:${t}:${e}`,fe=e=>btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),V=e=>{let t=atob(e.replace(/-/g,"+").replace(/_/g,"/")+"=".repeat((4-e.length%4)%4));return Uint8Array.from(t,n=>n.charCodeAt(0))};async function pt(){let e=await crypto.subtle.generateKey({name:"Ed25519"},!0,["sign","verify"]),t=await crypto.subtle.exportKey("jwk",e.publicKey),n=await crypto.subtle.exportKey("jwk",e.privateKey);return{keyId:await me(t),publicJwk:t,privateJwk:n}}async function me(e){let t=await crypto.subtle.digest("SHA-256",V(e.x));return[...new Uint8Array(t)].slice(0,8).map(n=>n.toString(16).padStart(2,"0")).join("")}async function ft(e,t,n,s){let a=await crypto.subtle.importKey("jwk",s,{name:"Ed25519"},!1,["sign"]),o=await crypto.subtle.sign({name:"Ed25519"},a,new TextEncoder().encode(H(e,t)));return{by:t,keyId:n,sig:fe(new Uint8Array(o))}}async function ge(e,t,n){try{let s=await crypto.subtle.importKey("jwk",n,{name:"Ed25519"},!1,["verify"]);return await crypto.subtle.verify({name:"Ed25519"},s,V(t.sig),new TextEncoder().encode(H(e,t.by)))}catch{return!1}}async function mt(e,t,n){return t.by!=="seek"||!Object.hasOwn(n,t.keyId)?!1:ge(e,t,n[t.keyId])}var he="0".repeat(64);async function xe(e){let t=["seek-log-v1",e.seq,e.prev_entry,e.event,e.quest_id,e.version_id,e.hash,e.seek_key_id,e.seek_sig].join(":"),n=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return[...new Uint8Array(n)].map(s=>s.toString(16).padStart(2,"0")).join("")}async function ht(e,t={size:0,head:he}){let{size:n,head:s}=t;for(let a of e){if(a.seq!==n+1)return{ok:!1,seq:a.seq,reason:"bad_sequence"};if(a.prev_entry!==s)return{ok:!1,seq:a.seq,reason:"bad_link"};if(await xe(a)!==a.entry_hash)return{ok:!1,seq:a.seq,reason:"bad_hash"};n=a.seq,s=a.entry_hash}return{ok:!0,size:n,head:s}}function ye(e){let t=e.texts?.en??{},n=o=>o===void 0?void 0:{en:t[o]??""},s=e.manifest,a=e.steps.map(o=>{let{title:u,story:d,next:r,...p}=o,i={...p,title:n(u),...d?{story:n(d)}:{},next:r};return i.kind==="choice"&&o.kind==="choice"&&(i.options=o.options.map(c=>({id:c.id,label:n(c.label),next:c.next}))),i});return{id:s.id,title:n(s.title),...s.summary?{summary:n(s.summary)}:{},area:s.area,...s.window?{window:s.window}:{},repeat:s.repeat,minAge:s.minAge,declarations:s.declarations,capabilities:s.capabilities,steps:a,rewards:s.rewards}}async function X(e,t){let n=(r,p)=>({ok:!1,issues:[{path:"",code:r,message:p}]}),s=e;if(!s||typeof s!="object"||!s.manifest||!Array.isArray(s.steps)||!s.texts?.en)return n("bad_bundle","Not a quest bundle.");if(s.manifest.format!==1)return n("bad_format",`Unknown bundle format ${String(s.manifest.format)}.`);let a,o;try{a=ye(s),o=J(a,{version:s.manifest.version,build:s.manifest.build,...s.manifest.games!==void 0?{games:s.manifest.games}:{}})}catch(r){return r instanceof T?{ok:!1,issues:r.issues}:n("bad_bundle","The bundle could not be read.")}let[u,d]=await Promise.all([G(s),G(o)]);return u!==d?n("not_compiled","The bundle is not what seek build makes. Build it again with the CLI."):t!==void 0&&t!==u?n("hash_mismatch","The bundle does not match its hash."):{ok:!0,hash:u,definition:a}}var we=".seek",L={fileBytes:64*1024*1024,unpackedBytes:128*1024*1024},w=class extends Error{},kt=e=>`${e.manifest.id}-${e.manifest.version}-${e.manifest.build}${we}`;async function Y(e,t){let n=e.getReader(),s=[],a=0;for(;;){let{done:d,value:r}=await n.read();if(d)break;if(a+=r.byteLength,a>t)throw await n.cancel(),new w("This .seek file unpacks to more than a build can be.");s.push(r)}let o=new Uint8Array(a),u=0;for(let d of s)o.set(d,u),u+=d.byteLength;return o}var Z=e=>new Blob([e]).stream();async function At(e){let t=new TextEncoder().encode(JSON.stringify({format:1,bundle:e.bundle,hash:e.hash,signature:e.signature,games:e.games})),n=Z(t).pipeThrough(new CompressionStream("gzip"));return await Y(n,L.fileBytes)}async function _t(e){if(e.byteLength>L.fileBytes)throw new w("This .seek file is too big.");if(e[0]!==31||e[1]!==139)throw new w("This is not a .seek file. Make one with seek build.");let t;try{let o=Z(e).pipeThrough(new DecompressionStream("gzip"));t=JSON.parse(new TextDecoder().decode(await Y(o,L.unpackedBytes)))}catch(o){throw o instanceof w?o:new w("This .seek file is damaged. Build it again.")}if(!t||t.format!==1)throw new w("This .seek file was made by another version of seek. Build it again.");let n=await X(t.bundle,t.hash);if(!n.ok)throw new w(`This .seek file is damaged: ${n.issues[0].message}`);let s=t.bundle.manifest.games??{},a=t.games??{};for(let[o,u]of Object.entries(s))if(!Object.hasOwn(a,o)||a[o].hash!==u)throw new w(`This .seek file is damaged: the game "${o}" is not the one the quest names.`);if(t.signature&&(t.signature.by!=="maker"||typeof t.signature.keyId!="string"||typeof t.signature.sig!="string"))throw new w("This .seek file has a signature that is not a maker's.");return{format:1,bundle:t.bundle,hash:t.hash,signature:t.signature??null,games:a}}var be={imageMaxSide:4096,imageAdviseSide:2048,decodedMaxBytes:96*1024*1024,decodedAdviseBytes:48*1024*1024,startMaxBytes:8*1024*1024,startAdviseBytes:3*1024*1024,wavMaxBytes:2*1024*1024,wavAdviseBytes:256*1024,audioMaxSeconds:600,opaquePngAdviseBytes:300*1024,gifAdviseBytes:1024*1024},ke={maxBytes:1024*1024,minWidth:1280,minHeight:720,maxWidth:3840,maxHeight:2160,ratio:16/9,ratioTolerance:.01,types:["image/png","image/jpeg","image/webp"]},O=(e,t)=>(e[t]<<8|e[t+1])>>>0,I=(e,t)=>(e[t]|e[t+1]<<8)>>>0,q=(e,t)=>(e[t]|e[t+1]<<8|e[t+2]<<16)>>>0,b=(e,t)=>(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0,E=(e,t)=>(e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24)>>>0,k=(e,t,n)=>String.fromCharCode(...e.subarray(t,t+n));function B(e,t,n=0,s=e.length){let a=[...t].map(o=>o.charCodeAt(0));e:for(let o=Math.max(0,n);o<=Math.min(e.length,s)-a.length;o++){for(let u=0;u<a.length;u++)if(e[o+u]!==a[u])continue e;return o}return-1}function Ae(e){if(e.length<33||k(e,12,4)!=="IHDR")return null;let t=e[25],n=B(e,"IDAT",33),s=B(e,"tRNS",33,n===-1?e.length:n);return{width:b(e,16),height:b(e,20),alpha:t===4||t===6||s!==-1}}function _e(e){let t=2;for(;t+9<e.length;){if(e[t]!==255){t++;continue}let n=e[t+1];if(n===255){t++;continue}if(n===216||n===1||n>=208&&n<=215){t+=2;continue}if(n===217||n===218)return null;if(n>=192&&n<=207&&n!==196&&n!==200&&n!==204)return{height:O(e,t+5),width:O(e,t+7)};t+=2+O(e,t+2)}return null}function Se(e){return e.length>=10?{width:I(e,6),height:I(e,8)}:null}function ve(e){if(e.length<30)return null;let t=k(e,12,4);if(t==="VP8 ")return e[23]!==157||e[24]!==1||e[25]!==42?null:{width:I(e,26)&16383,height:I(e,28)&16383};if(t==="VP8L"){if(e[20]!==47)return null;let n=e[21],s=e[22],a=e[23],o=e[24];return{width:1+((s&63)<<8|n),height:1+((o&15)<<10|a<<2|(s&192)>>6)}}return t==="VP8X"?{width:1+q(e,24),height:1+q(e,27)}:null}function $e(e){let t=null,n=Math.min(e.length,256*1024);for(let s=B(e,"ispe",0,n);s!==-1&&!(s+16>e.length);s=B(e,"ispe",s+4,n)){let a={width:b(e,s+8),height:b(e,s+12)};(!t||a.width*a.height>t.width*t.height)&&(t=a)}return t}function ne(e,t){switch(v(e)){case"png":return Ae(t);case"jpg":case"jpeg":return _e(t);case"gif":return Se(t);case"webp":return ve(t);case"avif":return $e(t);default:return null}}function Te(e){let t=0;for(let n=12;n+8<=e.length;){let s=k(e,n,4),a=E(e,n+4);if(s==="fmt "&&n+20<=e.length&&(t=E(e,n+16)),s==="data")return t>0?a/t:null;n+=8+a+a%2}return null}var ee={v1:[0,32,40,48,56,64,80,96,112,128,160,192,224,256,320],v2:[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160]},Me=[44100,48e3,32e3];function Pe(e){let t=0;for(k(e,0,3)==="ID3"&&e.length>10&&(t=10+(e[6]<<21|e[7]<<14|e[8]<<7|e[9]));t+4<e.length&&t<512*1024;t++){if(e[t]!==255||(e[t+1]&224)!==224)continue;let n=e[t+1]>>3&3,s=e[t+1]>>1&3,a=e[t+2]>>2&3,o=e[t+2]>>4;if(n===1||s!==1||a===3||o===0||o===15)continue;let u=n===3,d=Me[a]/(u?1:n===2?2:4),r=(u?ee.v1:ee.v2)[o],p=e[t+3]>>6===3,i=u?1152:576,c=t+4+(u?p?17:32:p?9:17),l=k(e,c,4);return(l==="Xing"||l==="Info")&&c+12<=e.length&&b(e,c+4)&1?b(e,c+8)*i/d:(e.length-t)*8/(r*1e3)}return null}function Ee(e){if(k(e,0,4)!=="OggS"||e.length<28)return null;let t=27+e[26],n=0,s=0;if(k(e,t,7)==="vorbis"?n=E(e,t+12):k(e,t,8)==="OpusHead"&&(n=48e3,s=I(e,t+10)),!n)return null;for(let a=e.length-14;a>=0;a--){if(e[a]!==79||k(e,a,4)!=="OggS")continue;let o=E(e,a+6)+E(e,a+10)*2**32;return Math.max(0,o-s)/n}return null}function Ie(e){let t=B(e,"mvhd");if(t===-1||t+32>e.length)return null;if(e[t+4]===1){let a=b(e,t+24),o=b(e,t+28)*2**32+b(e,t+32);return a?o/a:null}let s=b(e,t+16);return s?b(e,t+20)/s:null}function Be(e,t){switch(v(e)){case"wav":return Te(t);case"mp3":return Pe(t);case"ogg":return Ee(t);case"m4a":return Ie(t);default:return null}}var ze=/<(?!a[\s>])[a-z][^>]*?\s(?:src|href|poster)\s*=\s*["']([^"']+)["']/gi,te=/url\(\s*["']?([^"')]+)["']?\s*\)|@import\s+["']([^"']+)["']/gi,Re=/(?:^|[\s;}])(?:import|export)\s*(?:[\w$*{}\s,]+?\s*from\s*)?["']([^"'\n]+)["']/g;function Ue(e,t){let n=t.trim().split(/[?#]/)[0];if(!n||/^[a-z][a-z0-9+.-]*:/i.test(n)||n.startsWith("//"))return null;let s=n.startsWith("/")?[]:e.split("/").slice(0,-1);for(let a of n.split("/"))if(!(a===""||a==="."))if(a===".."){if(!s.length)return null;s.pop()}else s.push(a);return s.join("/")||null}function Ce(e){if(!e.has(x))return[];let t=new Set([x]),n=[x],s=a=>new TextDecoder().decode(e.get(a));for(;n.length;){let a=n.shift(),o=S(a)?.kind,u=[],d=(r,p)=>{r.lastIndex=0;for(let i=r.exec(p);i;i=r.exec(p))u.push(i[1]??i[2])};if(o==="html"){let r=s(a);d(ze,r),d(te,r)}else if(o==="style")d(te,s(a));else if(o==="script")d(Re,s(a));else continue;for(let r of u){let p=Ue(a,r);p&&e.has(p)&&!t.has(p)&&(t.add(p),n.push(p))}}return[...t].sort()}var _=e=>`${(e/(1024*1024)).toFixed(1)} MB`,P=e=>e<1024*1024?`${Math.round(e/1024)} KB`:_(e);function se(e){let t=e instanceof Map?e:new Map([...e].map(d=>[d.path,d.bytes])),n=be,s={images:[],sounds:[],decodedBytes:0,start:{files:[],bytes:0},issues:[],advice:[]},a=(d,r,p)=>s.issues.push({path:d,code:r,message:p}),o=(d,r,p)=>s.advice.push({path:d,code:r,message:p});for(let[d,r]of[...t].sort(([p],[i])=>p<i?-1:1)){let p=S(d),i=v(d);if(p?.kind==="image"&&i!=="svg"){let c=ne(d,r);if(!c||!c.width||!c.height){i==="avif"?o(d,"image_size_unknown","Seek could not read the size of this AVIF image, so it is not counted in the memory budget. WebP or PNG is safer."):a(d,"image_unreadable","Seek cannot read the size of this image: the file looks broken. Export it again.");continue}let l=c.width*c.height*4;s.images.push({path:d,width:c.width,height:c.height,bytes:r.byteLength,decoded:l,...c.alpha!==void 0?{alpha:c.alpha}:{}}),s.decodedBytes+=l;let f=Math.max(c.width,c.height);f>n.imageMaxSide?a(d,"image_too_large",`${c.width} x ${c.height} pixels: an image is at most ${n.imageMaxSide} pixels on its longest side. Phones cannot keep larger ones in memory. seek optimize scales it down.`):f>n.imageAdviseSide&&o(d,"image_large",`${c.width} x ${c.height} pixels takes ${_(l)} of memory once loaded. ${n.imageAdviseSide} pixels on the longest side is plenty for a phone; seek optimize scales it down.`),i==="png"&&c.alpha===!1&&r.byteLength>n.opaquePngAdviseBytes&&o(d,"opaque_png",`A ${P(r.byteLength)} PNG without transparency is probably a photo. As WebP or JPEG it is often 5 to 10 times smaller: seek optimize --webp.`),i==="gif"&&r.byteLength>n.gifAdviseBytes&&o(d,"heavy_gif",`A ${P(r.byteLength)} GIF. Animated GIFs are heavy to download and to play; an animated WebP or a sprite sheet is far lighter.`)}else if(p?.kind==="audio"){let c=Be(d,r);s.sounds.push({path:d,seconds:c,bytes:r.byteLength}),c!==null&&c>n.audioMaxSeconds&&a(d,"audio_too_long",`This sound lasts ${Math.round(c)} seconds; one sound is at most ${n.audioMaxSeconds/60} minutes.`),i==="wav"&&(r.byteLength>n.wavMaxBytes?a(d,"wav_too_big",`A WAV of ${P(r.byteLength)} is uncompressed sound: at most ${P(n.wavMaxBytes)}. As M4A it is about a tenth of the size: seek optimize --audio.`):r.byteLength>n.wavAdviseBytes&&o(d,"wav_audio",`A WAV is uncompressed sound. As M4A this ${P(r.byteLength)} is about a tenth: seek optimize --audio.`))}else p?.kind==="font"&&(i==="ttf"||i==="otf")&&o(d,"font_format",`A .${i} font: as WOFF2 it is usually 30 to 50% smaller and loads the same everywhere.`)}s.decodedBytes>n.decodedMaxBytes?a("","memory_too_big",`All images together take ${_(s.decodedBytes)} of memory once loaded; a game may use at most ${_(n.decodedMaxBytes)}. Scale the largest ones down (seek optimize) or load them only when needed.`):s.decodedBytes>n.decodedAdviseBytes&&o("","memory_heavy",`All images together take ${_(s.decodedBytes)} of memory once loaded. Older phones start to struggle above ${_(n.decodedAdviseBytes)}.`);let u=Ce(t);return s.start={files:u,bytes:u.reduce((d,r)=>d+(t.get(r)?.byteLength??0),0)},s.start.bytes>n.startMaxBytes?a(x,"start_too_big",`index.html loads ${_(s.start.bytes)} before the game can start; at most ${_(n.startMaxBytes)}. On a phone outside that takes far too long. Load the rest after ready(), with import() or fetch().`):s.start.bytes>n.startAdviseBytes&&o(x,"start_heavy",`index.html loads ${_(s.start.bytes)} before the game can start. Outside, on 4G, every MB is about a second of waiting; keep it under ${_(n.startAdviseBytes)} and load the rest after ready().`),s}function $t(e){let t=ke;if(e.byteLength>t.maxBytes)return{code:"too_big",message:"A cover is at most 1 MB."};let n=e,s=n[0]===137&&n[1]===80?"png":n[0]===255&&n[1]===216?"jpg":k(n,0,4)==="RIFF"&&k(n,8,4)==="WEBP"?"webp":null;if(!s)return{code:"bad_cover",message:"A cover is a PNG, JPEG or WebP image."};let a=ne(`cover.${s}`,e);if(!a)return{code:"bad_cover",message:"Seek cannot read the size of this image: export it again."};let{width:o,height:u}=a;return o<t.minWidth||u<t.minHeight?{code:"cover_too_small",message:`A cover is at least ${t.minWidth} x ${t.minHeight} pixels; this one is ${o} x ${u}.`}:o>t.maxWidth||u>t.maxHeight?{code:"cover_too_large",message:`A cover is at most ${t.maxWidth} x ${t.maxHeight} pixels; this one is ${o} x ${u}.`}:Math.abs(o/u/t.ratio-1)>t.ratioTolerance?{code:"cover_ratio",message:`A cover is 16:9, like 1920 x 1080; this one is ${o} x ${u}.`}:null}var ae=1,It=/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]-[0-9a-f]{16}$/,Ge="play.seekprotocol.ai",ie="https://api.play.seekprotocol.ai",A={files:200,fileBytes:5*1024*1024,totalBytes:15*1024*1024,pathLength:120,depth:5,segment:/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/,storageBytes:64*1024},Le={html:{type:"text/html; charset=utf-8",kind:"html",text:!0},js:{type:"text/javascript; charset=utf-8",kind:"script",text:!0},mjs:{type:"text/javascript; charset=utf-8",kind:"script",text:!0},css:{type:"text/css; charset=utf-8",kind:"style",text:!0},json:{type:"application/json; charset=utf-8",kind:"data",text:!0},svg:{type:"image/svg+xml",kind:"image",text:!0},png:{type:"image/png",kind:"image",text:!1},jpg:{type:"image/jpeg",kind:"image",text:!1},jpeg:{type:"image/jpeg",kind:"image",text:!1},gif:{type:"image/gif",kind:"image",text:!1},webp:{type:"image/webp",kind:"image",text:!1},avif:{type:"image/avif",kind:"image",text:!1},mp3:{type:"audio/mpeg",kind:"audio",text:!1},ogg:{type:"audio/ogg",kind:"audio",text:!1},wav:{type:"audio/wav",kind:"audio",text:!1},m4a:{type:"audio/mp4",kind:"audio",text:!1},woff:{type:"font/woff",kind:"font",text:!1},woff2:{type:"font/woff2",kind:"font",text:!1},ttf:{type:"font/ttf",kind:"font",text:!1},otf:{type:"font/otf",kind:"font",text:!1}},x="index.html",Oe=/^[0-9a-f]{64}$/,je=e=>[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join(""),R=async e=>je(await crypto.subtle.digest("SHA-256",typeof e=="string"?new TextEncoder().encode(e):e)),v=e=>/\.([a-z0-9]+)$/i.exec(e)?.[1]?.toLowerCase()??"",S=e=>Le[v(e)];function N(e){let t=(s,a)=>({path:e,code:s,message:a});if(typeof e!="string"||!e)return t("bad_path","A file needs a path.");if(e.length>A.pathLength)return t("path_too_long",`A path is at most ${A.pathLength} characters.`);let n=e.split("/");return n.length>A.depth?t("path_too_deep",`Folders go at most ${A.depth-1} deep.`):n.every(s=>A.segment.test(s))?S(e)?null:t("file_type",`".${v(e)||"(none)"}" files are not allowed. A game is html, js, css, json, images, audio and fonts.`):t("bad_path",'Use letters, digits, dots, hyphens and underscores; no hidden files, spaces or "..".')}var y=(e,t,n=0)=>t.every((s,a)=>e[n+a]===s),Ne=[["a Windows executable",[77,90]],["an ELF executable",[127,69,76,70]],["a Mach-O executable",[207,250,237,254]],["a Mach-O executable",[206,250,237,254]],["a Mach-O executable",[254,237,250,207]],["a Mach-O or Java binary",[202,254,186,190]],["a script with an interpreter line",[35,33]],["a zip, jar or apk archive",[80,75,3,4]],["WebAssembly",[0,97,115,109]],["a gzip archive",[31,139]],["a PDF",[37,80,68,70]]],De={png:e=>y(e,[137,80,78,71,13,10,26,10]),jpg:e=>y(e,[255,216,255]),jpeg:e=>y(e,[255,216,255]),gif:e=>y(e,[71,73,70,56]),webp:e=>y(e,[82,73,70,70])&&y(e,[87,69,66,80],8),wav:e=>y(e,[82,73,70,70])&&y(e,[87,65,86,69],8),ogg:e=>y(e,[79,103,103,83]),woff:e=>y(e,[119,79,70,70]),woff2:e=>y(e,[119,79,70,50])};function re(e,t){let n=S(e);if(!n)return N(e);let s=v(e);for(let[o,u]of Ne)if((!n.text||u[0]===35)&&y(t,u))return{path:e,code:"executable",message:`This file is ${o}, which a game cannot carry.`};if(n.text){if(t.includes(0))return{path:e,code:"not_text",message:"A text file must not contain NUL bytes."};try{new TextDecoder("utf-8",{fatal:!0}).decode(t)}catch{return{path:e,code:"not_text",message:"A text file must be UTF-8."}}return null}let a=De[s];return a&&!a(t)?{path:e,code:"type_mismatch",message:`This file is not really a .${s} file.`}:null}function Fe(e){let t=[],n=(d,r,p)=>t.push({path:d,code:r,message:p}),s=e;if(!s||typeof s!="object"||Array.isArray(s))return[{path:"",code:"bad_game",message:"Not a game manifest."}];let a=new Set(["format","id","entry","files","size"]);for(let d of Object.keys(s))a.has(d)||n(d,"bad_game",`Unknown field "${d}".`);if(s.format!==ae&&n("format","bad_format",`Unknown game format ${String(s.format)}.`),(typeof s.id!="string"||!$.test(s.id))&&n("id","bad_game_id","A game id is 3 to 30 lowercase letters, digits and hyphens."),s.entry!==x&&n("entry","bad_entry",`A game starts at ${x}.`),!Array.isArray(s.files))return[...t,{path:"files",code:"bad_game",message:"A game lists its files."}];s.files.length>A.files&&n("files","too_many_files",`A game has at most ${A.files} files; this one has ${s.files.length}.`);let o=0,u="";for(let[d,r]of s.files.entries()){let p=`files[${d}]`;if(!r||typeof r!="object"||Object.keys(r).sort().join()!=="path,sha256,size,type"){n(p,"bad_game","A file is { path, size, sha256, type }.");continue}let i=N(r.path);i?n(r.path||p,i.code,i.message):r.type!==S(r.path).type&&n(r.path,"bad_type",`A .${v(r.path)} file is served as ${S(r.path).type}.`),typeof r.path=="string"&&r.path<=u&&n(r.path,"not_sorted","Files are sorted by path, each once."),typeof r.path=="string"&&(u=r.path),!Number.isSafeInteger(r.size)||r.size<0?n(r.path||p,"bad_size","A size is a whole number of bytes."):r.size>A.fileBytes?n(r.path,"file_too_big",`A file is at most ${A.fileBytes} bytes; this one is ${r.size}.`):o+=r.size,(typeof r.sha256!="string"||!Oe.test(r.sha256))&&n(r.path||p,"bad_sha256","sha256 is 64 lowercase hex characters.")}return o>A.totalBytes&&n("files","game_too_big",`A game is at most ${A.totalBytes} bytes; this one is ${o}.`),s.size!==o&&n("size","bad_size","size is the sum of the file sizes."),s.files.some(d=>d?.path===x)||n(x,"entry_missing",`A game needs ${x} at the top of its folder.`),t}var We=e=>R(M(e));async function Bt(e,t){return t.byteLength!==e.size?{path:e.path,code:"size_mismatch",message:`The manifest says ${e.size} bytes; the file has ${t.byteLength}.`}:await R(t)!==e.sha256?{path:e.path,code:"sha256_mismatch",message:"The file does not match its sha256."}:re(e.path,t)}async function zt(e,t){let n=[],s=[...t].sort((r,p)=>r.path<p.path?-1:r.path>p.path?1:0),a=[];for(let r of s){let p=N(r.path)??re(r.path,r.bytes);p&&n.push(p),a.push({path:r.path,size:r.bytes.byteLength,sha256:await R(r.bytes),type:S(r.path)?.type??"application/octet-stream"})}let o={format:ae,id:e,entry:x,files:a,size:a.reduce((r,p)=>r+p.size,0)},u=new Set(n.map(r=>`${r.path}:${r.code}`));for(let r of Fe(o))!u.has(`${r.path}:${r.code}`)&&r.code!=="bad_type"&&n.push(r);let d=se(s.filter(r=>!n.some(p=>p.path===r.path)));return n.push(...d.issues),{manifest:o,hash:await We(o),issues:n,assets:d}}async function Rt(e,t){return`${t}-${(await R(e.toLowerCase())).slice(0,16)}`}var Ut=e=>`https://${e}.${Ge}`,Qe=/\b(?:https?|wss?):\/\/[^\s"'`<>()\\]+/gi,Ke=/^(?:https?:\/\/www\.w3\.org\/|https:\/\/storeconnect\.seekprotocol\.ai(?:$|\/)|http:\/\/(?:localhost|127\.0\.0\.1)(?::\d{1,5})?(?:$|\/))/i,Je=/\s(src|href|action|formaction|poster|data|srcset|background|xlink:href|manifest)\s*=\s*["']?\s*((?:(?:https?|wss?):)?\/\/[^"'\s>]+)/gi,He=/(?:url\(\s*["']?|@import\s+["'])\s*((?:(?:https?):)?\/\/[^"')\s]+)/gi,Ve=[["eval",/\beval\s*\(/g,"eval() runs text as code."],["new_function",/\bnew\s+Function\s*\(/g,"new Function() runs text as code."],["string_timer",/\bset(?:Timeout|Interval)\s*\(\s*["'`]/g,"A timer with a string runs text as code."],["document_write",/\bdocument\.write(?:ln)?\s*\(/g,"document.write() writes markup at run time."],["window_open",/\bwindow\.open\s*\(/g,"Opening windows is not allowed."],["navigation",/\b(?:(?:top|parent|window|document)\.location|location\.href)\s*=(?!=)/g,"A game does not navigate away."],["javascript_url",/javascript\s*:/gi,"javascript: URLs run code."]],Xe=/<[a-z][^>]*?\son[a-z]+\s*=/gi,Ye=/<script\b(?![^>]*\bsrc\s*=)[^>]*>\s*[^<\s]/gi,Ze=/<meta[^>]+http-equiv\s*=\s*["']?refresh/gi,qe=/<base\b/gi,j=e=>e===ie||e.startsWith(`${ie}/`);function Ct(e,t,n=20){let s=S(e);if(!s?.text||s.kind==="data")return[];let a=[],o=[];for(let i=t.indexOf(`
|
|
4
|
+
`);i!==-1;i=t.indexOf(`
|
|
5
|
+
`,i+1))o.push(i);let u=i=>{let c=0,l=o.length;for(;c<l;){let f=c+l>>1;o[f]<i?c=f+1:l=f}return c+1},d=(i,c,l,f,h)=>{a.length<n&&!a.some(g=>g.code===c&&g.line===u(i))&&a.push({path:e,line:u(i),code:c,severity:l,message:f,match:h.slice(0,120)})},r=(i,c)=>{i.lastIndex=0;for(let l=i.exec(t);l;l=i.exec(t))c(l)},p=new Set;if((s.kind==="html"||s.kind==="image")&&(r(Je,i=>{j(i[2])||(p.add(u(i.index)),d(i.index,"remote_url","fail",`"${i[1]}" points outside the game. Everything a game loads ships inside it.`,i[2]))}),r(Xe,i=>d(i.index,"inline_handler","flag","Inline event handlers do not run under the CSP; use addEventListener in a .js file.",i[0])),r(Ye,i=>d(i.index,"inline_script","flag","Inline scripts do not run under the CSP; put the code in a .js file.",i[0])),r(Ze,i=>d(i.index,"meta_refresh","fail","A game does not redirect.",i[0])),r(qe,i=>d(i.index,"base_tag","flag","A <base> tag changes where relative URLs point.",i[0]))),(s.kind==="style"||s.kind==="html")&&r(He,i=>{j(i[1])||(p.add(u(i.index)),d(i.index,"remote_url","fail","CSS that loads from outside the game.",i[1]))}),s.kind==="script"||s.kind==="html")for(let[i,c,l]of Ve)r(c,f=>d(f.index,i,"flag",l,f[0]));return r(Qe,i=>{j(i[0])||Ke.test(i[0])||p.has(u(i.index))||d(i.index,"remote_url_text","flag","A URL outside the game. The CSP blocks it; a reviewer checks why it is there.",i[0])}),a}export{be as ASSET_LIMITS,F as BUNDLE_FORMAT,Q as CAPABILITIES,ke as COVER_SPEC,U as DECLARATION_VALUES,It as GAME_APP_ID_PATTERN,x as GAME_ENTRY,Le as GAME_FILE_TYPES,ae as GAME_FORMAT,Ge as GAME_HOST_SUFFIX,$ as GAME_ID_PATTERN,A as GAME_LIMITS,ie as GAME_PROXY_ORIGIN,m as LIMITS,he as LOG_GENESIS,we as PACKAGE_EXTENSION,L as PACKAGE_LIMITS,w as PackageError,T as QuestInvalidError,W as SDK_RUNTIME,le as VERSION_PATTERN,se as analyzeGameAssets,Be as audioSeconds,G as bundleHash,M as canonicalJson,J as compile,$t as coverProblem,ye as decompile,ce as distanceM,v as extensionOf,Rt as gameAppId,S as gameFileType,We as gameHash,Ut as gameOrigin,N as gamePathProblem,pt as generateSigningKey,ne as imageSize,me as keyIdOf,Ct as lintGameText,xe as logEntryHash,kt as packageFileName,zt as packageGame,nt as quest,_t as readPackage,at as reward,ft as signHash,H as signedMessage,re as sniffGameFile,Ce as startFiles,it as step,z as successors,tt as t,de as usedCapabilities,K as validate,Fe as validateGameManifest,X as verifyBundle,Bt as verifyGameFile,ge as verifyHash,ht as verifyLogChain,mt as verifySeekSignature,At as writePackage,st as zone};
|
package/log.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const LOG_GENESIS: string;
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
seq: number;
|
|
4
|
+
event: "signed" | "resigned";
|
|
5
|
+
quest_id: string;
|
|
6
|
+
version_id: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
seek_key_id: string;
|
|
9
|
+
seek_sig: string;
|
|
10
|
+
prev_entry: string;
|
|
11
|
+
entry_hash: string;
|
|
12
|
+
}
|
|
13
|
+
export type LogCheck = {
|
|
14
|
+
ok: true;
|
|
15
|
+
size: number;
|
|
16
|
+
head: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
seq: number;
|
|
20
|
+
reason: "bad_sequence" | "bad_link" | "bad_hash";
|
|
21
|
+
};
|
|
22
|
+
export declare function logEntryHash(e: Omit<LogEntry, "entry_hash">): Promise<string>;
|
|
23
|
+
export declare function verifyLogChain(entries: LogEntry[], from?: {
|
|
24
|
+
size: number;
|
|
25
|
+
head: string;
|
|
26
|
+
}): Promise<LogCheck>;
|
package/package.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GameManifest } from "./games.js";
|
|
2
|
+
import type { Signature } from "./sign.js";
|
|
3
|
+
import type { QuestBundle } from "./types.js";
|
|
4
|
+
export declare const PACKAGE_EXTENSION = ".seek";
|
|
5
|
+
export declare const PACKAGE_LIMITS: {
|
|
6
|
+
readonly fileBytes: number;
|
|
7
|
+
readonly unpackedBytes: number;
|
|
8
|
+
};
|
|
9
|
+
export interface GameUpload {
|
|
10
|
+
manifest: GameManifest;
|
|
11
|
+
hash: string;
|
|
12
|
+
files: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export interface QuestPackage {
|
|
15
|
+
format: 1;
|
|
16
|
+
bundle: QuestBundle;
|
|
17
|
+
hash: string;
|
|
18
|
+
signature: Signature | null;
|
|
19
|
+
games: Record<string, GameUpload>;
|
|
20
|
+
}
|
|
21
|
+
export declare class PackageError extends Error {
|
|
22
|
+
}
|
|
23
|
+
export declare const packageFileName: (b: QuestBundle) => string;
|
|
24
|
+
export declare function writePackage(p: Omit<QuestPackage, "format">): Promise<Uint8Array>;
|
|
25
|
+
export declare function readPackage(bytes: Uint8Array): Promise<QuestPackage>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seekprotocol/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Write quests for the Seek app.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Seek Protocol",
|
|
7
|
+
"url": "https://www.seekprotocol.ai/en"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://www.seekprotocol.ai/en",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"seek",
|
|
12
|
+
"seekprotocol",
|
|
13
|
+
"quests",
|
|
14
|
+
"games"
|
|
15
|
+
],
|
|
16
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "index.js",
|
|
19
|
+
"types": "index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./index.d.ts",
|
|
23
|
+
"default": "./index.js"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/sign.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Signature {
|
|
2
|
+
by: "maker" | "seek";
|
|
3
|
+
keyId: string;
|
|
4
|
+
sig: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const signedMessage: (hash: string, by: Signature["by"]) => string;
|
|
7
|
+
export type Keyring = Record<string, JsonWebKey>;
|
|
8
|
+
export interface KeyPairJwk {
|
|
9
|
+
keyId: string;
|
|
10
|
+
publicJwk: JsonWebKey;
|
|
11
|
+
privateJwk: JsonWebKey;
|
|
12
|
+
}
|
|
13
|
+
export declare function generateSigningKey(): Promise<KeyPairJwk>;
|
|
14
|
+
export declare function keyIdOf(jwk: JsonWebKey): Promise<string>;
|
|
15
|
+
export declare function signHash(hash: string, by: Signature["by"], keyId: string, privateJwk: JsonWebKey): Promise<Signature>;
|
|
16
|
+
export declare function verifyHash(hash: string, signature: Signature, publicJwk: JsonWebKey): Promise<boolean>;
|
|
17
|
+
export declare function verifySeekSignature(hash: string, signature: Signature, ring: Keyring): Promise<boolean>;
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export declare const BUNDLE_FORMAT: 1;
|
|
2
|
+
export declare const SDK_RUNTIME: 1;
|
|
3
|
+
export declare const GAME_ID_PATTERN: RegExp;
|
|
4
|
+
export interface Text {
|
|
5
|
+
en: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Point {
|
|
8
|
+
lat: number;
|
|
9
|
+
lng: number;
|
|
10
|
+
}
|
|
11
|
+
export type Area = {
|
|
12
|
+
kind: "anywhere";
|
|
13
|
+
} | {
|
|
14
|
+
kind: "circle";
|
|
15
|
+
name: string;
|
|
16
|
+
center: Point;
|
|
17
|
+
radiusM: number;
|
|
18
|
+
};
|
|
19
|
+
export interface Place {
|
|
20
|
+
name: string;
|
|
21
|
+
at: Point;
|
|
22
|
+
}
|
|
23
|
+
export declare const CAPABILITIES: readonly [
|
|
24
|
+
"location.markers",
|
|
25
|
+
"spawns.place",
|
|
26
|
+
"catch",
|
|
27
|
+
"steps",
|
|
28
|
+
"arena",
|
|
29
|
+
"rewards.cards",
|
|
30
|
+
"rewards.cosmetics",
|
|
31
|
+
"rewards.boosts",
|
|
32
|
+
"rewards.crypto",
|
|
33
|
+
"gamekit"
|
|
34
|
+
];
|
|
35
|
+
export type Capability = (typeof CAPABILITIES)[number];
|
|
36
|
+
interface StepBase {
|
|
37
|
+
id: string;
|
|
38
|
+
title: Text;
|
|
39
|
+
story?: Text;
|
|
40
|
+
next?: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface TalkStep extends StepBase {
|
|
43
|
+
kind: "talk";
|
|
44
|
+
giver: string;
|
|
45
|
+
at?: Place;
|
|
46
|
+
}
|
|
47
|
+
export interface ArriveStep extends StepBase {
|
|
48
|
+
kind: "arrive";
|
|
49
|
+
at: Place;
|
|
50
|
+
radiusM: number;
|
|
51
|
+
}
|
|
52
|
+
export interface CatchStep extends StepBase {
|
|
53
|
+
kind: "catch";
|
|
54
|
+
coin: string;
|
|
55
|
+
count: number;
|
|
56
|
+
near?: Place;
|
|
57
|
+
withinM?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface WalkStep extends StepBase {
|
|
60
|
+
kind: "walk";
|
|
61
|
+
steps: number;
|
|
62
|
+
verified: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface ArenaStep extends StepBase {
|
|
65
|
+
kind: "arena";
|
|
66
|
+
wins: number;
|
|
67
|
+
}
|
|
68
|
+
export interface ChoiceStep extends StepBase {
|
|
69
|
+
kind: "choice";
|
|
70
|
+
options: {
|
|
71
|
+
id: string;
|
|
72
|
+
label: Text;
|
|
73
|
+
next: string | null;
|
|
74
|
+
}[];
|
|
75
|
+
}
|
|
76
|
+
export interface PlayStep extends StepBase {
|
|
77
|
+
kind: "play";
|
|
78
|
+
game: string;
|
|
79
|
+
minScore: number;
|
|
80
|
+
maxScore: number;
|
|
81
|
+
}
|
|
82
|
+
export type Step = TalkStep | ArriveStep | CatchStep | WalkStep | ArenaStep | ChoiceStep | PlayStep;
|
|
83
|
+
export type StepKind = Step["kind"];
|
|
84
|
+
export type Reward = {
|
|
85
|
+
kind: "xp";
|
|
86
|
+
amount: number;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "card";
|
|
89
|
+
card: string;
|
|
90
|
+
} | {
|
|
91
|
+
kind: "cosmetic";
|
|
92
|
+
item: string;
|
|
93
|
+
} | {
|
|
94
|
+
kind: "boost";
|
|
95
|
+
boost: string;
|
|
96
|
+
count: number;
|
|
97
|
+
} | {
|
|
98
|
+
kind: "pool";
|
|
99
|
+
coin: string;
|
|
100
|
+
perPlayer: number;
|
|
101
|
+
maxPlayers: number;
|
|
102
|
+
};
|
|
103
|
+
export interface Declarations {
|
|
104
|
+
places: "public" | "private_with_permission";
|
|
105
|
+
effort: "light" | "moderate" | "hard";
|
|
106
|
+
hazards: boolean;
|
|
107
|
+
ai: "none" | "assisted" | "generated";
|
|
108
|
+
sponsored: boolean;
|
|
109
|
+
gameEvents: boolean;
|
|
110
|
+
rights: true;
|
|
111
|
+
}
|
|
112
|
+
export declare const DECLARATION_VALUES: {
|
|
113
|
+
readonly places: readonly [
|
|
114
|
+
"public",
|
|
115
|
+
"private_with_permission"
|
|
116
|
+
];
|
|
117
|
+
readonly effort: readonly [
|
|
118
|
+
"light",
|
|
119
|
+
"moderate",
|
|
120
|
+
"hard"
|
|
121
|
+
];
|
|
122
|
+
readonly ai: readonly [
|
|
123
|
+
"none",
|
|
124
|
+
"assisted",
|
|
125
|
+
"generated"
|
|
126
|
+
];
|
|
127
|
+
};
|
|
128
|
+
export interface QuestDefinition {
|
|
129
|
+
id: string;
|
|
130
|
+
title: Text;
|
|
131
|
+
summary?: Text;
|
|
132
|
+
area: Area;
|
|
133
|
+
window?: {
|
|
134
|
+
from: string;
|
|
135
|
+
to: string;
|
|
136
|
+
};
|
|
137
|
+
repeat?: "never" | "daily" | "weekly";
|
|
138
|
+
capabilities: Capability[];
|
|
139
|
+
steps: Step[];
|
|
140
|
+
rewards: Reward[];
|
|
141
|
+
minAge?: 13 | 16 | 18;
|
|
142
|
+
declarations: Declarations;
|
|
143
|
+
}
|
|
144
|
+
export type TextTable = Record<string, string>;
|
|
145
|
+
type CompileOne<S extends Step> = Omit<S, "title" | "story" | "next" | "options"> & {
|
|
146
|
+
title: string;
|
|
147
|
+
story?: string;
|
|
148
|
+
next: string | null;
|
|
149
|
+
} & (S extends ChoiceStep ? {
|
|
150
|
+
options: {
|
|
151
|
+
id: string;
|
|
152
|
+
label: string;
|
|
153
|
+
next: string | null;
|
|
154
|
+
}[];
|
|
155
|
+
} : unknown);
|
|
156
|
+
export type CompiledStep = {
|
|
157
|
+
[K in StepKind]: CompileOne<Extract<Step, {
|
|
158
|
+
kind: K;
|
|
159
|
+
}>>;
|
|
160
|
+
}[StepKind];
|
|
161
|
+
export interface QuestManifest {
|
|
162
|
+
format: typeof BUNDLE_FORMAT;
|
|
163
|
+
runtime: number;
|
|
164
|
+
id: string;
|
|
165
|
+
version: string;
|
|
166
|
+
build: number;
|
|
167
|
+
title: string;
|
|
168
|
+
summary?: string;
|
|
169
|
+
area: Area;
|
|
170
|
+
window?: {
|
|
171
|
+
from: string;
|
|
172
|
+
to: string;
|
|
173
|
+
};
|
|
174
|
+
repeat: "never" | "daily" | "weekly";
|
|
175
|
+
minAge: 13 | 16 | 18;
|
|
176
|
+
declarations: Declarations;
|
|
177
|
+
capabilities: Capability[];
|
|
178
|
+
rewards: Reward[];
|
|
179
|
+
start: string;
|
|
180
|
+
games?: Record<string, string>;
|
|
181
|
+
}
|
|
182
|
+
export interface QuestBundle {
|
|
183
|
+
manifest: QuestManifest;
|
|
184
|
+
steps: CompiledStep[];
|
|
185
|
+
texts: {
|
|
186
|
+
en: TextTable;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export {};
|
package/validate.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Capability, type Point, type QuestDefinition, type Step } from "./types.js";
|
|
2
|
+
export declare const LIMITS: {
|
|
3
|
+
readonly steps: {
|
|
4
|
+
readonly min: 1;
|
|
5
|
+
readonly max: 30;
|
|
6
|
+
};
|
|
7
|
+
readonly idPattern: RegExp;
|
|
8
|
+
readonly stepIdPattern: RegExp;
|
|
9
|
+
readonly title: 60;
|
|
10
|
+
readonly summary: 160;
|
|
11
|
+
readonly story: 600;
|
|
12
|
+
readonly areaRadiusM: {
|
|
13
|
+
readonly min: 100;
|
|
14
|
+
readonly max: 50000;
|
|
15
|
+
};
|
|
16
|
+
readonly arriveRadiusM: {
|
|
17
|
+
readonly min: 25;
|
|
18
|
+
readonly max: 500;
|
|
19
|
+
};
|
|
20
|
+
readonly catchCount: {
|
|
21
|
+
readonly min: 1;
|
|
22
|
+
readonly max: 50;
|
|
23
|
+
};
|
|
24
|
+
readonly catchWithinM: {
|
|
25
|
+
readonly min: 50;
|
|
26
|
+
readonly max: 2000;
|
|
27
|
+
};
|
|
28
|
+
readonly walkSteps: {
|
|
29
|
+
readonly min: 100;
|
|
30
|
+
readonly max: 50000;
|
|
31
|
+
};
|
|
32
|
+
readonly arenaWins: {
|
|
33
|
+
readonly min: 1;
|
|
34
|
+
readonly max: 20;
|
|
35
|
+
};
|
|
36
|
+
readonly choiceOptions: {
|
|
37
|
+
readonly min: 2;
|
|
38
|
+
readonly max: 4;
|
|
39
|
+
};
|
|
40
|
+
readonly xp: {
|
|
41
|
+
readonly max: 5000;
|
|
42
|
+
};
|
|
43
|
+
readonly boostCount: {
|
|
44
|
+
readonly max: 3;
|
|
45
|
+
};
|
|
46
|
+
readonly rewards: {
|
|
47
|
+
readonly max: 6;
|
|
48
|
+
};
|
|
49
|
+
readonly windowDays: {
|
|
50
|
+
readonly max: 92;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export interface Issue {
|
|
54
|
+
path: string;
|
|
55
|
+
code: string;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function distanceM(a: Point, b: Point): number;
|
|
59
|
+
export declare function usedCapabilities(q: QuestDefinition): Set<Capability>;
|
|
60
|
+
export declare function successors(steps: readonly Step[], index: number): (string | null)[];
|
|
61
|
+
export declare function validate(q: QuestDefinition): Issue[];
|
package/verify.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { QuestBundle, QuestDefinition } from "./types.js";
|
|
2
|
+
import type { Issue } from "./validate.js";
|
|
3
|
+
export declare function decompile(bundle: QuestBundle): QuestDefinition;
|
|
4
|
+
export type BundleCheck = {
|
|
5
|
+
ok: true;
|
|
6
|
+
hash: string;
|
|
7
|
+
definition: QuestDefinition;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
issues: Issue[];
|
|
11
|
+
};
|
|
12
|
+
export declare function verifyBundle(bundle: unknown, expectedHash?: string): Promise<BundleCheck>;
|