@krtuworks/sdk 0.0.0-stage → 1.0.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.
Files changed (56) hide show
  1. package/AGENTS.md +48 -0
  2. package/CHANGELOG.md +29 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +6 -0
  5. package/README.md +24 -2
  6. package/dist/api.d.ts +34 -0
  7. package/dist/api.js +61 -0
  8. package/dist/avatarReports.d.ts +15 -0
  9. package/dist/avatarReports.js +18 -0
  10. package/dist/bugs.d.ts +114 -0
  11. package/dist/bugs.js +276 -0
  12. package/dist/client.d.ts +390 -0
  13. package/dist/client.js +666 -0
  14. package/dist/clock.d.ts +19 -0
  15. package/dist/clock.js +32 -0
  16. package/dist/config.d.ts +85 -0
  17. package/dist/config.js +68 -0
  18. package/dist/deviceKey.d.ts +32 -0
  19. package/dist/deviceKey.js +26 -0
  20. package/dist/entitlements.d.ts +48 -0
  21. package/dist/entitlements.js +113 -0
  22. package/dist/errors.d.ts +21 -0
  23. package/dist/errors.js +32 -0
  24. package/dist/events.d.ts +93 -0
  25. package/dist/events.js +304 -0
  26. package/dist/families.d.ts +94 -0
  27. package/dist/families.js +1 -0
  28. package/dist/files.d.ts +108 -0
  29. package/dist/files.js +215 -0
  30. package/dist/identity.d.ts +129 -0
  31. package/dist/identity.js +437 -0
  32. package/dist/ids.d.ts +13 -0
  33. package/dist/ids.js +61 -0
  34. package/dist/index.d.ts +16 -0
  35. package/dist/index.js +7 -0
  36. package/dist/jwt.d.ts +2 -0
  37. package/dist/jwt.js +8 -0
  38. package/dist/locks.d.ts +6 -0
  39. package/dist/locks.js +20 -0
  40. package/dist/presence.d.ts +45 -0
  41. package/dist/presence.js +97 -0
  42. package/dist/progress.d.ts +108 -0
  43. package/dist/progress.js +347 -0
  44. package/dist/progression.d.ts +39 -0
  45. package/dist/progression.js +81 -0
  46. package/dist/remoteConfig.d.ts +48 -0
  47. package/dist/remoteConfig.js +70 -0
  48. package/dist/sessions.d.ts +45 -0
  49. package/dist/sessions.js +157 -0
  50. package/dist/store.d.ts +18 -0
  51. package/dist/store.js +57 -0
  52. package/dist/transport.d.ts +30 -0
  53. package/dist/transport.js +73 -0
  54. package/dist/validator.d.ts +43 -0
  55. package/dist/validator.js +252 -0
  56. package/package.json +40 -4
@@ -0,0 +1,48 @@
1
+ import type { Api } from './api.js';
2
+ import { type Module, type ResolvedOptions } from './config.js';
3
+ import type { KrtuStore } from './store.js';
4
+ export interface Switch {
5
+ build: string;
6
+ mode: 'TELEMETRY' | 'ALL';
7
+ message: string;
8
+ }
9
+ export interface RemoteConfigValue {
10
+ appId: string;
11
+ environment: string;
12
+ platform: string | null;
13
+ version: string;
14
+ config: Record<string, unknown> & {
15
+ maintenance?: {
16
+ enabled: boolean;
17
+ message?: string | null;
18
+ };
19
+ minVersion?: string | null;
20
+ modules?: Partial<Record<Module, boolean>>;
21
+ };
22
+ switches: Switch[];
23
+ serverTime: string;
24
+ }
25
+ /**
26
+ * The app's remote config, read without a sign-in and revalidated with its ETag. The
27
+ * last good copy is kept in the store, so an offline launch starts with what the owner set last.
28
+ */
29
+ export declare class RemoteConfig {
30
+ private readonly opts;
31
+ private readonly api;
32
+ private readonly store;
33
+ private stored;
34
+ constructor(opts: ResolvedOptions, api: Api, store: KrtuStore);
35
+ private get key();
36
+ load(): Promise<RemoteConfigValue | null>;
37
+ get value(): RemoteConfigValue | null;
38
+ /** Asks the server; `false` when nothing changed. Throws when it cannot (the kept copy stays). */
39
+ refresh(): Promise<boolean>;
40
+ /** A module the owner switched off makes nothing; a switch never set is on. An app that is not a game has no game-only modules. */
41
+ moduleOn(module: Module): boolean;
42
+ modules(): Record<Module, boolean>;
43
+ /** The owner's switch that rules this build, ALL over TELEMETRY. */
44
+ switchForBuild(): Switch | null;
45
+ get<T = unknown>(key: string, fallback?: T): T | undefined;
46
+ }
47
+ /** True when `build` is older than `minVersion` (dotted numbers; a build's `+meta` is ignored). */
48
+ export declare function olderThan(build: string, minVersion: string): boolean;
@@ -0,0 +1,70 @@
1
+ import { GAME_MODULES, MODULES } from './config.js';
2
+ /**
3
+ * The app's remote config, read without a sign-in and revalidated with its ETag. The
4
+ * last good copy is kept in the store, so an offline launch starts with what the owner set last.
5
+ */
6
+ export class RemoteConfig {
7
+ opts;
8
+ api;
9
+ store;
10
+ stored = null;
11
+ constructor(opts, api, store) {
12
+ this.opts = opts;
13
+ this.api = api;
14
+ this.store = store;
15
+ }
16
+ get key() {
17
+ return `krtu:${this.opts.clientId}:config:${this.opts.platform}`;
18
+ }
19
+ async load() {
20
+ this.stored = (await this.store.get(this.key)) ?? null;
21
+ return this.value;
22
+ }
23
+ get value() {
24
+ return this.stored?.value ?? null;
25
+ }
26
+ /** Asks the server; `false` when nothing changed. Throws when it cannot (the kept copy stays). */
27
+ async refresh() {
28
+ const answer = await this.api.open(`/v1/apps/${encodeURIComponent(this.opts.slug)}/config`, {
29
+ // The build rides in the query (no header: a browser's read stays a simple GET) and counts for its release report.
30
+ query: { environment: this.opts.environment, platform: this.opts.platform, build: this.opts.build },
31
+ headers: this.stored?.etag ? { 'if-none-match': this.stored.etag } : {},
32
+ accept: [304],
33
+ });
34
+ if (answer.status === 304)
35
+ return false;
36
+ this.stored = { etag: answer.headers.get('etag'), value: answer.json };
37
+ await this.store.set(this.key, this.stored);
38
+ return true;
39
+ }
40
+ /** A module the owner switched off makes nothing; a switch never set is on. An app that is not a game has no game-only modules. */
41
+ moduleOn(module) {
42
+ if (!this.opts.game && GAME_MODULES.includes(module))
43
+ return false;
44
+ return this.value?.config.modules?.[module] !== false;
45
+ }
46
+ modules() {
47
+ return Object.fromEntries(MODULES.map((m) => [m, this.moduleOn(m)]));
48
+ }
49
+ /** The owner's switch that rules this build, ALL over TELEMETRY. */
50
+ switchForBuild() {
51
+ const mine = (this.value?.switches ?? []).filter((s) => s.build === '*' || s.build === this.opts.build);
52
+ return mine.find((s) => s.mode === 'ALL') ?? mine.find((s) => s.mode === 'TELEMETRY') ?? null;
53
+ }
54
+ get(key, fallback) {
55
+ const config = this.value?.config;
56
+ return config && Object.prototype.hasOwnProperty.call(config, key) ? config[key] : fallback;
57
+ }
58
+ }
59
+ /** True when `build` is older than `minVersion` (dotted numbers; a build's `+meta` is ignored). */
60
+ export function olderThan(build, minVersion) {
61
+ const parts = (v) => v.split('+')[0].split(/[.-]/).map((p) => Number.parseInt(p, 10) || 0);
62
+ const a = parts(build);
63
+ const b = parts(minVersion);
64
+ for (let i = 0; i < Math.max(a.length, b.length); i += 1) {
65
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
66
+ if (d !== 0)
67
+ return d < 0;
68
+ }
69
+ return false;
70
+ }
@@ -0,0 +1,45 @@
1
+ import type { Api } from './api.js';
2
+ import type { ResolvedOptions } from './config.js';
3
+ export interface SessionInfo {
4
+ id: string;
5
+ /** The player the session is for. */
6
+ owner: string;
7
+ startedAt: string;
8
+ }
9
+ /**
10
+ * The play session: opened once per launch and player, kept alive every
11
+ * `heartbeatSeconds` with the time actually in use (foreground only), ended when the app goes away.
12
+ * An open whose answer was lost is sent again with the same `clientSessionId` and gets the same session.
13
+ */
14
+ export declare class SessionKeeper {
15
+ private readonly opts;
16
+ private readonly api;
17
+ private readonly subject;
18
+ /** Told the licence list of a session answer (only an open that sent a platform proof gets one). */
19
+ private readonly onEntitlements;
20
+ private current;
21
+ private clientSessionId;
22
+ private timer;
23
+ private heartbeatSeconds;
24
+ private activeMs;
25
+ private activeSince;
26
+ private opening;
27
+ constructor(opts: ResolvedOptions, api: Api, subject: () => string | null,
28
+ /** Told the licence list of a session answer (only an open that sent a platform proof gets one). */
29
+ onEntitlements?: (items: unknown) => Promise<void>);
30
+ get session(): SessionInfo | null;
31
+ /** Seconds in use in this launch (foreground only). */
32
+ activeSeconds(): number;
33
+ setForeground(foreground: boolean): void;
34
+ /** Opens the player's session if there is none (one at a time). */
35
+ open(): Promise<SessionInfo | null>;
36
+ private doOpen;
37
+ private schedule;
38
+ heartbeat(): Promise<void>;
39
+ /** One whole session where every step must answer: open, a heartbeat, end (the validator's round). */
40
+ round(): Promise<void>;
41
+ /** Ends the session (on pagehide the request outlives the page). */
42
+ end(): Promise<void>;
43
+ /** Forgets the session without telling the server (the player changed: it times out there). */
44
+ close(): void;
45
+ }
@@ -0,0 +1,157 @@
1
+ import { KrtuworksError, isKrtuworksError } from './errors.js';
2
+ import { uuidV4 } from './ids.js';
3
+ /**
4
+ * The play session: opened once per launch and player, kept alive every
5
+ * `heartbeatSeconds` with the time actually in use (foreground only), ended when the app goes away.
6
+ * An open whose answer was lost is sent again with the same `clientSessionId` and gets the same session.
7
+ */
8
+ export class SessionKeeper {
9
+ opts;
10
+ api;
11
+ subject;
12
+ onEntitlements;
13
+ current = null;
14
+ clientSessionId = null;
15
+ timer = null;
16
+ heartbeatSeconds = 60;
17
+ activeMs = 0;
18
+ activeSince = performance.now();
19
+ opening = null;
20
+ constructor(opts, api, subject,
21
+ /** Told the licence list of a session answer (only an open that sent a platform proof gets one). */
22
+ onEntitlements = async () => undefined) {
23
+ this.opts = opts;
24
+ this.api = api;
25
+ this.subject = subject;
26
+ this.onEntitlements = onEntitlements;
27
+ }
28
+ get session() {
29
+ const owner = this.subject();
30
+ return this.current && this.current.owner === owner ? this.current : null;
31
+ }
32
+ /** Seconds in use in this launch (foreground only). */
33
+ activeSeconds() {
34
+ const live = this.activeSince !== null ? performance.now() - this.activeSince : 0;
35
+ return Math.floor((this.activeMs + live) / 1000);
36
+ }
37
+ setForeground(foreground) {
38
+ if (foreground && this.activeSince === null)
39
+ this.activeSince = performance.now();
40
+ if (!foreground && this.activeSince !== null) {
41
+ this.activeMs += performance.now() - this.activeSince;
42
+ this.activeSince = null;
43
+ }
44
+ }
45
+ /** Opens the player's session if there is none (one at a time). */
46
+ async open() {
47
+ if (this.session)
48
+ return this.session;
49
+ this.opening ??= this.doOpen().finally(() => {
50
+ this.opening = null;
51
+ });
52
+ return this.opening;
53
+ }
54
+ async doOpen() {
55
+ const owner = this.subject();
56
+ if (!owner)
57
+ return null;
58
+ if (this.current && this.current.owner !== owner)
59
+ this.close();
60
+ this.clientSessionId ??= uuidV4();
61
+ const device = this.opts.device ?? undefined;
62
+ const answer = await this.api.player('/v1/sessions', {
63
+ method: 'POST',
64
+ json: {
65
+ clientSessionId: this.clientSessionId,
66
+ platform: this.opts.platform,
67
+ build: this.opts.build,
68
+ ...(device ? { device } : {}),
69
+ ...(this.opts.locale ? { locale: this.opts.locale } : {}),
70
+ },
71
+ });
72
+ const session = answer.json?.session;
73
+ if (!session || this.subject() !== owner)
74
+ return null;
75
+ this.current = { id: session.id, owner, startedAt: session.startedAt };
76
+ await this.onEntitlements(answer.json?.entitlements).catch(() => undefined);
77
+ if (typeof answer.json?.heartbeatSeconds === 'number')
78
+ this.heartbeatSeconds = answer.json.heartbeatSeconds;
79
+ this.schedule();
80
+ return this.current;
81
+ }
82
+ schedule() {
83
+ if (this.timer)
84
+ clearTimeout(this.timer);
85
+ this.timer = setTimeout(() => void this.heartbeat().catch(() => undefined), this.heartbeatSeconds * 1000);
86
+ this.timer.unref?.();
87
+ }
88
+ async heartbeat() {
89
+ const session = this.session;
90
+ if (!session)
91
+ return;
92
+ try {
93
+ const answer = await this.api.player(`/v1/sessions/${session.id}/heartbeat`, {
94
+ method: 'POST',
95
+ json: { activeSeconds: this.activeSeconds(), foreground: this.activeSince !== null },
96
+ });
97
+ const next = answer.json?.nextHeartbeatSeconds;
98
+ if (typeof next === 'number' && next > 0)
99
+ this.heartbeatSeconds = next;
100
+ }
101
+ catch (err) {
102
+ if (isKrtuworksError(err) && (err.code === 'session_ended' || err.code === 'session_not_found')) {
103
+ // The server closed it (30 minutes without a heartbeat): a new one, with a new id.
104
+ this.current = null;
105
+ this.clientSessionId = null;
106
+ await this.open().catch(() => undefined);
107
+ return;
108
+ }
109
+ }
110
+ finally {
111
+ if (this.current)
112
+ this.schedule();
113
+ }
114
+ }
115
+ /** One whole session where every step must answer: open, a heartbeat, end (the validator's round). */
116
+ async round() {
117
+ const session = await this.open();
118
+ if (!session)
119
+ throw new KrtuworksError('signed_out', 'No player is signed in.');
120
+ let failure = null;
121
+ try {
122
+ await this.api.player(`/v1/sessions/${session.id}/heartbeat`, {
123
+ method: 'POST',
124
+ json: { activeSeconds: this.activeSeconds(), foreground: this.activeSince !== null },
125
+ });
126
+ }
127
+ catch (err) {
128
+ failure = err;
129
+ }
130
+ this.close();
131
+ // The session is ended even when its heartbeat failed, so the round leaves nothing open.
132
+ const ended = this.api.player(`/v1/sessions/${session.id}/end`, { method: 'POST', json: { activeSeconds: this.activeSeconds() } });
133
+ if (failure) {
134
+ await ended.catch(() => undefined);
135
+ throw failure;
136
+ }
137
+ await ended;
138
+ }
139
+ /** Ends the session (on pagehide the request outlives the page). */
140
+ async end() {
141
+ const session = this.session;
142
+ this.close();
143
+ if (!session)
144
+ return;
145
+ await this.api
146
+ .player(`/v1/sessions/${session.id}/end`, { method: 'POST', json: { activeSeconds: this.activeSeconds() }, keepalive: true })
147
+ .catch(() => undefined);
148
+ }
149
+ /** Forgets the session without telling the server (the player changed: it times out there). */
150
+ close() {
151
+ if (this.timer)
152
+ clearTimeout(this.timer);
153
+ this.timer = null;
154
+ this.current = null;
155
+ this.clientSessionId = null;
156
+ }
157
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Where the SDK keeps what must outlive the page: tokens, the device key, queued events, progress and
3
+ * uploads. Values are structured-cloneable (plain objects, Uint8Array, CryptoKey): the device's private
4
+ * key is a non-extractable CryptoKey and is only ever stored as one, never as bytes.
5
+ */
6
+ export interface KrtuStore {
7
+ get<T = unknown>(key: string): Promise<T | undefined>;
8
+ set(key: string, value: unknown): Promise<void>;
9
+ delete(key: string): Promise<void>;
10
+ /** Every key that starts with `prefix`. */
11
+ keys(prefix: string): Promise<string[]>;
12
+ }
13
+ /** Kept in this process only (tests, servers, a page that must remember nothing). */
14
+ export declare function memoryStore(): KrtuStore;
15
+ /** The browser's IndexedDB: one object store, survives reloads, shared by the origin's tabs. */
16
+ export declare function indexedDbStore(name?: string): KrtuStore;
17
+ /** IndexedDB where there is one, otherwise this process's memory. */
18
+ export declare function defaultStore(): KrtuStore;
package/dist/store.js ADDED
@@ -0,0 +1,57 @@
1
+ /** Kept in this process only (tests, servers, a page that must remember nothing). */
2
+ export function memoryStore() {
3
+ const data = new Map();
4
+ return {
5
+ async get(key) {
6
+ return data.has(key) ? structuredClone(data.get(key)) : undefined;
7
+ },
8
+ async set(key, value) {
9
+ data.set(key, structuredClone(value));
10
+ },
11
+ async delete(key) {
12
+ data.delete(key);
13
+ },
14
+ async keys(prefix) {
15
+ return [...data.keys()].filter((k) => k.startsWith(prefix));
16
+ },
17
+ };
18
+ }
19
+ function request(req) {
20
+ return new Promise((resolve, reject) => {
21
+ req.onsuccess = () => resolve(req.result);
22
+ req.onerror = () => reject(req.error);
23
+ });
24
+ }
25
+ /** The browser's IndexedDB: one object store, survives reloads, shared by the origin's tabs. */
26
+ export function indexedDbStore(name = 'krtuworks-sdk') {
27
+ let db = null;
28
+ const open = () => {
29
+ db ??= new Promise((resolve, reject) => {
30
+ const req = indexedDB.open(name, 1);
31
+ req.onupgradeneeded = () => req.result.createObjectStore('kv');
32
+ req.onsuccess = () => resolve(req.result);
33
+ req.onerror = () => reject(req.error);
34
+ });
35
+ return db;
36
+ };
37
+ const tx = async (mode) => (await open()).transaction('kv', mode).objectStore('kv');
38
+ return {
39
+ async get(key) {
40
+ return (await request((await tx('readonly')).get(key)));
41
+ },
42
+ async set(key, value) {
43
+ await request((await tx('readwrite')).put(value, key));
44
+ },
45
+ async delete(key) {
46
+ await request((await tx('readwrite')).delete(key));
47
+ },
48
+ async keys(prefix) {
49
+ const range = IDBKeyRange.bound(prefix, `${prefix}￿`);
50
+ return (await request((await tx('readonly')).getAllKeys(range))).map(String);
51
+ },
52
+ };
53
+ }
54
+ /** IndexedDB where there is one, otherwise this process's memory. */
55
+ export function defaultStore() {
56
+ return typeof indexedDB === 'undefined' ? memoryStore() : indexedDbStore();
57
+ }
@@ -0,0 +1,30 @@
1
+ import type { Clock } from './clock.js';
2
+ import { KrtuworksError } from './errors.js';
3
+ export interface Answer {
4
+ status: number;
5
+ headers: Headers;
6
+ /** Parsed JSON, or null for an empty or non-JSON body. */
7
+ json: Record<string, unknown> | null;
8
+ /** The raw body, when asked for (file reads). */
9
+ bytes: Uint8Array | null;
10
+ }
11
+ export interface SendInit {
12
+ method: string;
13
+ headers?: Record<string, string>;
14
+ body?: BodyInit | null;
15
+ /** Read the body as bytes instead of JSON. */
16
+ bytes?: boolean;
17
+ /** Let the request outlive the page (the end of a session on pagehide). */
18
+ keepalive?: boolean;
19
+ }
20
+ /**
21
+ * The error an answer carries. The server and the /v1/guest calls answer `{ error: { code, message }, ...more }`;
22
+ * the token endpoint and the device-key challenge answer OAuth's `{ error, error_description, ...more }`.
23
+ */
24
+ export declare function errorOf(answer: Answer): KrtuworksError;
25
+ export declare function send(fetchFn: typeof fetch, clock: Clock, url: string, init: SendInit): Promise<Answer>;
26
+ /** A form body: the token endpoint wants one, and a browser sends it without a preflight. */
27
+ export declare function form(fields: Record<string, string | undefined | null>): {
28
+ body: URLSearchParams;
29
+ headers: Record<string, string>;
30
+ };
@@ -0,0 +1,73 @@
1
+ import { KrtuworksError } from './errors.js';
2
+ function retryAfterOf(headers, json) {
3
+ const fromBody = json?.retryAfter;
4
+ if (typeof fromBody === 'number' && Number.isFinite(fromBody))
5
+ return fromBody;
6
+ const header = headers.get('retry-after');
7
+ if (!header)
8
+ return null;
9
+ const seconds = Number(header);
10
+ if (Number.isFinite(seconds))
11
+ return seconds;
12
+ const at = Date.parse(header);
13
+ return Number.isFinite(at) ? Math.max(0, Math.ceil((at - Date.now()) / 1000)) : null;
14
+ }
15
+ /**
16
+ * The error an answer carries. The server and the /v1/guest calls answer `{ error: { code, message }, ...more }`;
17
+ * the token endpoint and the device-key challenge answer OAuth's `{ error, error_description, ...more }`.
18
+ */
19
+ export function errorOf(answer) {
20
+ const json = answer.json ?? {};
21
+ const { error, error_description: description, ...rest } = json;
22
+ let code = `http_${answer.status}`;
23
+ let message = `The server answered ${answer.status}.`;
24
+ if (error && typeof error === 'object') {
25
+ const e = error;
26
+ if (typeof e.code === 'string')
27
+ code = e.code;
28
+ if (typeof e.message === 'string')
29
+ message = e.message;
30
+ }
31
+ else if (typeof error === 'string') {
32
+ code = error;
33
+ if (typeof description === 'string')
34
+ message = description;
35
+ rest.description = description;
36
+ }
37
+ return new KrtuworksError(code, message, answer.status, retryAfterOf(answer.headers, answer.json), rest);
38
+ }
39
+ export async function send(fetchFn, clock, url, init) {
40
+ let res;
41
+ try {
42
+ res = await fetchFn(url, { method: init.method, headers: init.headers, body: init.body ?? undefined, keepalive: init.keepalive });
43
+ }
44
+ catch (err) {
45
+ throw new KrtuworksError('network_error', `No answer from ${new URL(url).host}: ${err?.message ?? String(err)}`);
46
+ }
47
+ clock.noteServerTime(res.headers.get('date'));
48
+ let json = null;
49
+ let bytes = null;
50
+ const type = res.headers.get('content-type') ?? '';
51
+ try {
52
+ if (init.bytes && res.ok)
53
+ bytes = new Uint8Array(await res.arrayBuffer());
54
+ else if (type.includes('json'))
55
+ json = (await res.json());
56
+ else
57
+ await res.arrayBuffer();
58
+ }
59
+ catch (err) {
60
+ throw new KrtuworksError('network_error', `The answer from ${new URL(url).host} was cut: ${err?.message ?? String(err)}`, res.status);
61
+ }
62
+ if (json && typeof json.serverTime === 'string')
63
+ clock.noteServerTime(json.serverTime);
64
+ return { status: res.status, headers: res.headers, json, bytes };
65
+ }
66
+ /** A form body: the token endpoint wants one, and a browser sends it without a preflight. */
67
+ export function form(fields) {
68
+ const body = new URLSearchParams();
69
+ for (const [k, v] of Object.entries(fields))
70
+ if (v !== undefined && v !== null)
71
+ body.set(k, v);
72
+ return { body, headers: { 'content-type': 'application/x-www-form-urlencoded' } };
73
+ }
@@ -0,0 +1,43 @@
1
+ import { type KrtuworksAppConfig, type KrtuworksOptions } from './config.js';
2
+ /**
3
+ * The Krtuworks validator: one call checks the app's setup on this
4
+ * machine, then runs a TEST round against the servers the way the build's release report counts it
5
+ * (the server's release report): a guest signs in, a session opens, sends a heartbeat and ends, an
6
+ * event batch, a save written and read back, a progress operation, a test bug report, the remote config
7
+ * read. Every call names the build, so this run is what turns its report green on the partner console.
8
+ *
9
+ * The run uses the SDK's own store (the default one unless `store` names another): on this machine the
10
+ * same TEST guest comes back run after run (new guests are limited per address and day). A new guest is
11
+ * an adult that accepts the current terms (`birthDate`, `acceptedTerms` change that). The save goes to the
12
+ * app's first save slot in TEST (`saveSlot` names another) as a new version; older ones stay in its history.
13
+ * A run right after another waits out the slot's write limit (one write per 10 seconds) and saves again.
14
+ */
15
+ export type ValidationStatus = 'PASSED' | 'FAILED' | 'SKIPPED';
16
+ export interface ValidationCheck {
17
+ key: string;
18
+ title: string;
19
+ status: ValidationStatus;
20
+ /** Why it failed or was skipped. */
21
+ detail: string | null;
22
+ }
23
+ export interface ValidationReport {
24
+ /** No check failed. */
25
+ ok: boolean;
26
+ build: string;
27
+ checks: ValidationCheck[];
28
+ }
29
+ export interface ValidateOptions extends Omit<KrtuworksOptions, 'environment' | 'sessions'> {
30
+ /** The slot to write and read back; default: the app's first save slot in TEST. */
31
+ saveSlot?: string;
32
+ /** A new TEST guest's date of birth (default 2000-01-01) and the terms it accepts (default the current ones). */
33
+ birthDate?: string;
34
+ acceptedTerms?: string;
35
+ }
36
+ /** What is wrong with krtuworks-config.json for this kind of client, if anything. */
37
+ export declare function configProblems(config: KrtuworksAppConfig | null | undefined, clientKind: 'web' | 'native'): string[];
38
+ /** What is wrong with the platform and, for a web page, the sign-in return address, if anything. */
39
+ export declare function platformProblems(platform: unknown, clientKind: 'web' | 'native', redirectUri: string | undefined): string[];
40
+ /** A send's verdict: sent, or left this device with no refusal; otherwise why it was not. */
41
+ export declare const sentOrWhy: (sent: number, waiting: number, stoppedBy: string | null | undefined) => string | null;
42
+ /** Checks this app's setup and runs a TEST round against the servers; never throws. */
43
+ export declare function validate(options: ValidateOptions): Promise<ValidationReport>;