@gamerdexapp/gamerdex-sync 0.1.0 → 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/CHANGELOG.md +13 -0
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +65 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +65 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.1] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `ProviderError.detail` carries the message that GitHub sends with a failed request, such as
|
|
15
|
+
"Resource not accessible by integration", so an app can log the reason of a failure.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- A sync that changes nothing no longer writes the meta file, so the repository gets no empty
|
|
20
|
+
commits. The meta file is still written when data is uploaded, when a device syncs for the first
|
|
21
|
+
time, and when the app changes the revisions.
|
|
22
|
+
|
|
10
23
|
## [0.1.0] - 2026-10-17
|
|
11
24
|
|
|
12
25
|
First release.
|
package/dist/index.d.mts
CHANGED
|
@@ -188,7 +188,11 @@ declare class ProviderError extends Error {
|
|
|
188
188
|
readonly kind: ProviderErrorKind;
|
|
189
189
|
readonly status?: number | undefined;
|
|
190
190
|
readonly retryAfterSeconds?: number | undefined;
|
|
191
|
-
|
|
191
|
+
/** The message that the provider sent with the failed request, when it sent one. */
|
|
192
|
+
readonly detail?: string | undefined;
|
|
193
|
+
constructor(kind: ProviderErrorKind, message: string, status?: number | undefined, retryAfterSeconds?: number | undefined,
|
|
194
|
+
/** The message that the provider sent with the failed request, when it sent one. */
|
|
195
|
+
detail?: string | undefined);
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
interface GitHubOptions {
|
package/dist/index.d.ts
CHANGED
|
@@ -188,7 +188,11 @@ declare class ProviderError extends Error {
|
|
|
188
188
|
readonly kind: ProviderErrorKind;
|
|
189
189
|
readonly status?: number | undefined;
|
|
190
190
|
readonly retryAfterSeconds?: number | undefined;
|
|
191
|
-
|
|
191
|
+
/** The message that the provider sent with the failed request, when it sent one. */
|
|
192
|
+
readonly detail?: string | undefined;
|
|
193
|
+
constructor(kind: ProviderErrorKind, message: string, status?: number | undefined, retryAfterSeconds?: number | undefined,
|
|
194
|
+
/** The message that the provider sent with the failed request, when it sent one. */
|
|
195
|
+
detail?: string | undefined);
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
interface GitHubOptions {
|
package/dist/index.js
CHANGED
|
@@ -346,6 +346,15 @@ var decodeBase64 = (base64) => {
|
|
|
346
346
|
return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
|
|
347
347
|
};
|
|
348
348
|
|
|
349
|
+
// src/provider/detail.ts
|
|
350
|
+
var MAX_DETAIL_LENGTH = 300;
|
|
351
|
+
var detailOf = async (response) => {
|
|
352
|
+
const body = await response.json().catch(() => null);
|
|
353
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
354
|
+
const message = body.message;
|
|
355
|
+
return typeof message === "string" ? message.slice(0, MAX_DETAIL_LENGTH) : void 0;
|
|
356
|
+
};
|
|
357
|
+
|
|
349
358
|
// src/provider/types.ts
|
|
350
359
|
var VersionConflictError = class extends Error {
|
|
351
360
|
constructor(path) {
|
|
@@ -356,16 +365,18 @@ var VersionConflictError = class extends Error {
|
|
|
356
365
|
path;
|
|
357
366
|
};
|
|
358
367
|
var ProviderError = class extends Error {
|
|
359
|
-
constructor(kind, message, status, retryAfterSeconds) {
|
|
368
|
+
constructor(kind, message, status, retryAfterSeconds, detail) {
|
|
360
369
|
super(message);
|
|
361
370
|
this.kind = kind;
|
|
362
371
|
this.status = status;
|
|
363
372
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
373
|
+
this.detail = detail;
|
|
364
374
|
this.name = "ProviderError";
|
|
365
375
|
}
|
|
366
376
|
kind;
|
|
367
377
|
status;
|
|
368
378
|
retryAfterSeconds;
|
|
379
|
+
detail;
|
|
369
380
|
};
|
|
370
381
|
|
|
371
382
|
// src/provider/github.ts
|
|
@@ -476,7 +487,10 @@ var GitHubProvider = class {
|
|
|
476
487
|
async assertOk(response) {
|
|
477
488
|
if (response.ok) return;
|
|
478
489
|
const { status } = response;
|
|
479
|
-
|
|
490
|
+
const detail = await detailOf(response);
|
|
491
|
+
if (status === 401) {
|
|
492
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
493
|
+
}
|
|
480
494
|
if (status === 403 || status === 429) {
|
|
481
495
|
const limited = status === 429 || response.headers.get("x-ratelimit-remaining") === "0";
|
|
482
496
|
if (limited) {
|
|
@@ -484,18 +498,34 @@ var GitHubProvider = class {
|
|
|
484
498
|
"rate-limit",
|
|
485
499
|
"GitHub limits the calls.",
|
|
486
500
|
status,
|
|
487
|
-
retryAfterOf(response)
|
|
501
|
+
retryAfterOf(response),
|
|
502
|
+
detail
|
|
488
503
|
);
|
|
489
504
|
}
|
|
490
|
-
throw new ProviderError(
|
|
505
|
+
throw new ProviderError(
|
|
506
|
+
"auth",
|
|
507
|
+
"The token has no permission for the repository.",
|
|
508
|
+
status,
|
|
509
|
+
void 0,
|
|
510
|
+
detail
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
if (status === 404) {
|
|
514
|
+
throw new ProviderError(
|
|
515
|
+
"not-found",
|
|
516
|
+
"The repository was not found.",
|
|
517
|
+
status,
|
|
518
|
+
void 0,
|
|
519
|
+
detail
|
|
520
|
+
);
|
|
491
521
|
}
|
|
492
|
-
if (status
|
|
493
|
-
throw new ProviderError("
|
|
494
|
-
|
|
495
|
-
throw this.invalid(`GitHub answered ${status}.`, status);
|
|
522
|
+
if (status >= 500) {
|
|
523
|
+
throw new ProviderError("server", "GitHub failed.", status, void 0, detail);
|
|
524
|
+
}
|
|
525
|
+
throw this.invalid(`GitHub answered ${status}.`, status, detail);
|
|
496
526
|
}
|
|
497
|
-
invalid(message, status) {
|
|
498
|
-
return new ProviderError("invalid", message, status);
|
|
527
|
+
invalid(message, status, detail) {
|
|
528
|
+
return new ProviderError("invalid", message, status, void 0, detail);
|
|
499
529
|
}
|
|
500
530
|
};
|
|
501
531
|
|
|
@@ -558,11 +588,14 @@ var readMeta = (text, appId) => {
|
|
|
558
588
|
if (!parsed.ok) throw new RemoteDataError(parsed.error);
|
|
559
589
|
return parsed.meta;
|
|
560
590
|
};
|
|
561
|
-
var writeMeta = async (options) => {
|
|
591
|
+
var writeMeta = async (options, uploaded) => {
|
|
562
592
|
const { provider, appId, deviceId, deviceName, updateMeta, now } = options;
|
|
563
593
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
|
|
564
594
|
const current = await provider.read(metaPath(appId));
|
|
565
595
|
const meta = readMeta(current?.content, appId);
|
|
596
|
+
if (current && !uploaded && !updateMeta && Object.hasOwn(meta.devices, deviceId)) {
|
|
597
|
+
return current.version;
|
|
598
|
+
}
|
|
566
599
|
const time = now();
|
|
567
600
|
const updated = withDevice(updateMeta ? updateMeta(meta) : meta, deviceId, {
|
|
568
601
|
name: deviceName,
|
|
@@ -592,7 +625,7 @@ var sync = async (options) => {
|
|
|
592
625
|
const cloud = restoreCloudValues(local, void 0, schema);
|
|
593
626
|
const created = await tryWrite(provider, path, cloud, null);
|
|
594
627
|
if (created === null) continue;
|
|
595
|
-
const metaVersion2 = await writeMeta(options);
|
|
628
|
+
const metaVersion2 = await writeMeta(options, true);
|
|
596
629
|
return done(local, cloud, created, metaVersion2, [], true);
|
|
597
630
|
}
|
|
598
631
|
const remote = parseData(file.content, validate);
|
|
@@ -605,12 +638,12 @@ var sync = async (options) => {
|
|
|
605
638
|
throw new RemoteDataError("The merged data is not valid.");
|
|
606
639
|
}
|
|
607
640
|
if (equal(merged.cloud, remote)) {
|
|
608
|
-
const metaVersion2 = await writeMeta(options);
|
|
641
|
+
const metaVersion2 = await writeMeta(options, false);
|
|
609
642
|
return done(merged.merged, merged.cloud, file.version, metaVersion2, merged.conflicts, false);
|
|
610
643
|
}
|
|
611
644
|
const version = await tryWrite(provider, path, merged.cloud, file.version);
|
|
612
645
|
if (version === null) continue;
|
|
613
|
-
const metaVersion = await writeMeta(options);
|
|
646
|
+
const metaVersion = await writeMeta(options, true);
|
|
614
647
|
return done(merged.merged, merged.cloud, version, metaVersion, merged.conflicts, true);
|
|
615
648
|
}
|
|
616
649
|
throw new SyncBusyError();
|
|
@@ -756,13 +789,26 @@ var getJson = async (send, token, url) => {
|
|
|
756
789
|
} catch {
|
|
757
790
|
throw new ProviderError("network", "GitHub could not be reached.");
|
|
758
791
|
}
|
|
759
|
-
if (response.
|
|
760
|
-
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
const { status } = response;
|
|
794
|
+
const detail = await detailOf(response);
|
|
795
|
+
if (status === 401 || status === 403) {
|
|
796
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
797
|
+
}
|
|
798
|
+
if (status >= 500)
|
|
799
|
+
throw new ProviderError("server", "GitHub failed.", status, void 0, detail);
|
|
800
|
+
throw new ProviderError(
|
|
801
|
+
"invalid",
|
|
802
|
+
"GitHub answer is not understood.",
|
|
803
|
+
status,
|
|
804
|
+
void 0,
|
|
805
|
+
detail
|
|
806
|
+
);
|
|
761
807
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (!isRecord4(body))
|
|
808
|
+
const body = await response.json().catch(() => null);
|
|
809
|
+
if (!isRecord4(body)) {
|
|
765
810
|
throw new ProviderError("invalid", "GitHub answer is not understood.", response.status);
|
|
811
|
+
}
|
|
766
812
|
return body;
|
|
767
813
|
};
|
|
768
814
|
var listInstalledRepositories = async (token, send = (...args) => globalThis.fetch(...args)) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/layout.ts","../src/meta.ts","../src/merge/equal.ts","../src/merge/cloud.ts","../src/merge/merge.ts","../src/merge/resolve.ts","../src/provider/base64.ts","../src/provider/types.ts","../src/provider/github.ts","../src/provider/memory.ts","../src/sync/sync.ts","../src/auth/app.ts","../src/auth/tokens.ts","../src/auth/source.ts","../src/auth/urls.ts","../src/provider/repositories.ts"],"sourcesContent":["export * from './layout';\nexport * from './meta';\nexport * from './merge/equal';\nexport * from './merge/merge';\nexport * from './merge/resolve';\nexport * from './merge/types';\nexport * from './provider/base64';\nexport * from './provider/github';\nexport * from './provider/memory';\nexport * from './provider/types';\nexport * from './sync/sync';\nexport * from './merge/cloud';\nexport * from './auth/app';\nexport * from './auth/source';\nexport * from './auth/tokens';\nexport * from './auth/urls';\nexport * from './provider/repositories';\n","export const ROOT_FOLDER = 'Gamerdex';\nexport const DATA_FILE = 'data.json';\nexport const META_FILE = 'meta.json';\n\nconst APP_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\n/** An app id names a folder, so it is lowercase words joined by single dashes. */\nexport const isAppId = (value: string): boolean => APP_ID.test(value);\n\nconst folderOf = (appId: string): string => {\n if (!isAppId(appId)) throw new RangeError(`Invalid app id: ${appId}`);\n return `${ROOT_FOLDER}/${appId}`;\n};\n\n/** The path of the saved data of an app, such as `Gamerdex/whiteout-survival/data.json`. */\nexport const dataPath = (appId: string): string => `${folderOf(appId)}/${DATA_FILE}`;\n\n/** The path of the bookkeeping file of an app, next to its saved data. */\nexport const metaPath = (appId: string): string => `${folderOf(appId)}/${META_FILE}`;\n","export const META_FORMAT = 1;\n\nexport interface DeviceRecord {\n name: string;\n /** ISO time of the last sync of the device. */\n lastSeenAt: string;\n}\n\n/**\n * The bookkeeping file beside the saved data. It holds no game data and no token. A revision only\n * counts up, so a device with a wrong clock cannot make old data look new.\n */\nexport interface SyncMeta {\n format: typeof META_FORMAT;\n appId: string;\n /** One counter for each chief id. A chief that was removed keeps its counter. */\n revisions: Record<string, number>;\n settingsRevision: number;\n /** Chief ids removed on purpose, with the revision of the removal. */\n removed: Record<string, number>;\n devices: Record<string, DeviceRecord>;\n lastSyncAt: string | null;\n}\n\nexport type ParsedMeta = { ok: true; meta: SyncMeta } | { ok: false; error: string };\n\nexport const emptyMeta = (appId: string): SyncMeta => ({\n format: META_FORMAT,\n appId,\n revisions: {},\n settingsRevision: 0,\n removed: {},\n devices: {},\n lastSyncAt: null,\n});\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isCounter = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 0;\n\nconst isCounterMap = (value: unknown): value is Record<string, number> =>\n isRecord(value) && Object.values(value).every(isCounter);\n\nconst isDevice = (value: unknown): value is DeviceRecord =>\n isRecord(value) && typeof value.name === 'string' && typeof value.lastSeenAt === 'string';\n\n/** Reads the meta file and checks the whole shape. Nothing is returned for a file that fails. */\nexport const parseMeta = (text: string, appId: string): ParsedMeta => {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return { ok: false, error: 'The meta file is not valid JSON.' };\n }\n if (!isRecord(value)) return { ok: false, error: 'The meta file is not an object.' };\n if (value.format !== META_FORMAT) return { ok: false, error: 'The meta format is not known.' };\n if (value.appId !== appId) return { ok: false, error: 'The meta file belongs to another app.' };\n if (!isCounterMap(value.revisions)) return { ok: false, error: 'The revisions are not valid.' };\n if (!isCounter(value.settingsRevision)) {\n return { ok: false, error: 'The settings revision is not valid.' };\n }\n if (!isCounterMap(value.removed)) return { ok: false, error: 'The removed list is not valid.' };\n if (!isRecord(value.devices) || !Object.values(value.devices).every(isDevice)) {\n return { ok: false, error: 'The devices are not valid.' };\n }\n if (value.lastSyncAt !== null && typeof value.lastSyncAt !== 'string') {\n return { ok: false, error: 'The last sync time is not valid.' };\n }\n return { ok: true, meta: value as unknown as SyncMeta };\n};\n\n/** Counts one change of a chief. A chief that returns after a removal leaves the removed list. */\nexport const withChiefRevision = (meta: SyncMeta, chiefId: string): SyncMeta => {\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: (meta.revisions[chiefId] ?? 0) + 1 },\n removed: Object.fromEntries(Object.entries(meta.removed).filter(([id]) => id !== chiefId)),\n };\n};\n\n/** Counts the removal of a chief. The counter keeps rising so an old copy cannot return it. */\nexport const withChiefRemoved = (meta: SyncMeta, chiefId: string): SyncMeta => {\n const revision = (meta.revisions[chiefId] ?? 0) + 1;\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: revision },\n removed: { ...meta.removed, [chiefId]: revision },\n };\n};\n\nexport const withSettingsRevision = (meta: SyncMeta): SyncMeta => ({\n ...meta,\n settingsRevision: meta.settingsRevision + 1,\n});\n\nexport const withDevice = (meta: SyncMeta, deviceId: string, device: DeviceRecord): SyncMeta => ({\n ...meta,\n devices: { ...meta.devices, [deviceId]: device },\n lastSyncAt: device.lastSeenAt,\n});\n","const isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** Deep equality of two JSON values. The order of the keys of an object does not matter. */\nexport const equal = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n if (Array.isArray(a)) {\n return Array.isArray(b) && a.length === b.length && a.every((item, i) => equal(item, b[i]));\n }\n if (isObject(a) && isObject(b)) {\n const keys = Object.keys(a);\n return (\n keys.length === Object.keys(b).length &&\n keys.every((key) => Object.hasOwn(b, key) && equal(a[key], b[key]))\n );\n }\n return false;\n};\n\nexport { isObject };\n","import { isObject } from './equal';\nimport { Schema } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Extract<Schema, { type: 'object' }>, key: string): Schema =>\n (schema.fields && Object.hasOwn(schema.fields, key) ? schema.fields[key] : undefined) ??\n schema.each ?? { type: 'record' };\n\nconst holdsDeviceValue = (schema: Schema): boolean => {\n if (schema.type === 'local') return true;\n if (schema.type === 'keyed') return holdsDeviceValue(schema.item);\n if (schema.type !== 'object') return false;\n return (\n (schema.each !== undefined && holdsDeviceValue(schema.each)) ||\n Object.values(schema.fields ?? {}).some(holdsDeviceValue)\n );\n};\n\n/**\n * Returns the merged data as it must be stored in the cloud: every part with the `local` strategy\n * keeps the value that the cloud already holds, so a value of one device never changes the cloud.\n */\nexport const restoreCloudValues = (merged: unknown, remote: unknown, schema: Schema): unknown => {\n if (schema.type === 'local') return remote;\n if (!holdsDeviceValue(schema)) return merged;\n\n if (schema.type === 'object' && isObject(merged) && (isObject(remote) || remote === undefined)) {\n const cloud = remote ?? {};\n const result: Record<string, unknown> = {};\n for (const key of new Set([...Object.keys(merged), ...Object.keys(cloud)])) {\n const child = childSchema(schema, key);\n const value = restoreCloudValues(own(merged, key), own(cloud, key), child);\n if (value !== undefined) result[key] = value;\n }\n return result;\n }\n\n if (schema.type === 'keyed' && Array.isArray(merged) && Array.isArray(remote)) {\n const remoteById = new Map(\n remote.filter(isObject).map((item) => [item[schema.key], item] as const),\n );\n return merged.map((item) =>\n isObject(item)\n ? restoreCloudValues(item, remoteById.get(item[schema.key]), schema.item)\n : item,\n );\n }\n return merged;\n};\n","import { restoreCloudValues } from './cloud';\nimport { equal, isObject } from './equal';\nimport { Conflict, MergeInput, MergeResult, Path, Schema, Side } from './types';\n\ninterface Context {\n prefer: Side;\n conflicts: Conflict[];\n}\n\n/** Reads an own key only, so an id such as `constructor` never reads from the prototype. */\nconst own = <T>(map: Record<string, T> | undefined, key: string): T | undefined =>\n map !== undefined && Object.hasOwn(map, key) ? map[key] : undefined;\n\nconst record = (\n base: unknown,\n local: unknown,\n remote: unknown,\n path: Path,\n context: Context,\n): unknown => {\n context.conflicts.push({\n path,\n kind: 'both-changed',\n base,\n local,\n remote,\n taken: context.prefer,\n });\n return context.prefer === 'local' ? local : remote;\n};\n\nconst joinLists = (base: unknown[], local: unknown[], remote: unknown[]): unknown[] => {\n const has = (list: unknown[], value: unknown) => list.some((item) => equal(item, value));\n const kept = (value: unknown) => {\n const inBase = has(base, value);\n const inLocal = has(local, value);\n const inRemote = has(remote, value);\n return inBase ? inLocal && inRemote : inLocal || inRemote;\n };\n const result: unknown[] = [];\n for (const value of [...local, ...remote]) {\n if (kept(value) && !has(result, value)) result.push(value);\n }\n return result;\n};\n\nconst entriesOf = (list: unknown[], key: string): Map<string, unknown> | null => {\n const entries = new Map<string, unknown>();\n for (const item of list) {\n if (!isObject(item) || typeof item[key] !== 'string') return null;\n entries.set(item[key], item);\n }\n return entries;\n};\n\nconst mergeEntries = (\n ids: string[],\n pick: (id: string) => [unknown, unknown, unknown],\n schemaOf: (id: string) => Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): Map<string, unknown> => {\n const merged = new Map<string, unknown>();\n for (const id of ids) {\n const [base, local, remote] = pick(id);\n const value = mergeNode(base, local, remote, schemaOf(id), [...path, id], context, deleteWins);\n if (value !== undefined) merged.set(id, value);\n }\n return merged;\n};\n\nconst orderedIds = (local: Iterable<string>, remote: Iterable<string>, base: Iterable<string>) => [\n ...new Set([...local, ...remote, ...base]),\n];\n\nconst mergeNode = (\n base: unknown,\n local: unknown,\n remote: unknown,\n schema: Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): unknown => {\n if (schema.type === 'local') return local;\n if (equal(local, remote)) return local;\n if (equal(local, base)) return remote;\n if (equal(remote, base)) return local;\n\n if (local === undefined || remote === undefined) {\n if (deleteWins) return undefined;\n const taken: Side = local === undefined ? 'remote' : 'local';\n context.conflicts.push({ path, kind: 'delete-edit', base, local, remote, taken });\n return taken === 'local' ? local : remote;\n }\n\n switch (schema.type) {\n case 'max': {\n const l = schema.order(local);\n const r = schema.order(remote);\n if (l === r) return record(base, local, remote, path, context);\n return l > r ? local : remote;\n }\n case 'join':\n if (Array.isArray(local) && Array.isArray(remote)) {\n return joinLists(Array.isArray(base) ? base : [], local, remote);\n }\n return record(base, local, remote, path, context);\n case 'object': {\n if (!isObject(local) || !isObject(remote)) return record(base, local, remote, path, context);\n const baseObject = isObject(base) ? base : {};\n const ids = orderedIds(Object.keys(local), Object.keys(remote), Object.keys(baseObject));\n const merged = mergeEntries(\n ids,\n (id) => [own(baseObject, id), own(local, id), own(remote, id)],\n (id) => own(schema.fields, id) ?? schema.each ?? { type: 'record' },\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return Object.fromEntries(merged);\n }\n case 'keyed': {\n const localEntries = Array.isArray(local) ? entriesOf(local, schema.key) : null;\n const remoteEntries = Array.isArray(remote) ? entriesOf(remote, schema.key) : null;\n const baseEntries = (Array.isArray(base) ? entriesOf(base, schema.key) : null) ?? new Map();\n if (!localEntries || !remoteEntries) return record(base, local, remote, path, context);\n const ids = orderedIds(localEntries.keys(), remoteEntries.keys(), baseEntries.keys());\n const merged = mergeEntries(\n ids,\n (id) => [baseEntries.get(id), localEntries.get(id), remoteEntries.get(id)],\n () => schema.item,\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return [...merged.values()];\n }\n case 'record':\n return record(base, local, remote, path, context);\n }\n};\n\n/**\n * Merges the data of this device and the data of the cloud against the data of the last sync.\n * A part that only one side changed takes that change. A part that both sides changed merges by its\n * schema, and a part that cannot merge is reported in `conflicts` with the preferred side already\n * in `merged`.\n */\nexport const merge = ({\n base,\n local,\n remote,\n schema,\n prefer = 'remote',\n}: MergeInput): MergeResult => {\n const context: Context = { prefer, conflicts: [] };\n const merged = mergeNode(base, local, remote, schema, [], context, false);\n return {\n merged,\n cloud: restoreCloudValues(merged, remote, schema),\n conflicts: context.conflicts,\n };\n};\n","import { isObject } from './equal';\nimport { Conflict, Schema, Side } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Schema, step: string): Schema => {\n if (schema.type === 'object') {\n const named =\n schema.fields && Object.hasOwn(schema.fields, step) ? schema.fields[step] : undefined;\n return named ?? schema.each ?? { type: 'record' };\n }\n return { type: 'record' };\n};\n\nconst setAt = (data: unknown, schema: Schema, path: string[], value: unknown): unknown => {\n if (path.length === 0) return value;\n const [step, ...rest] = path;\n if (schema.type === 'keyed') {\n const list = Array.isArray(data) ? data : [];\n const index = list.findIndex((item) => isObject(item) && item[schema.key] === step);\n const next = setAt(index >= 0 ? list[index] : undefined, schema.item, rest, value);\n if (index < 0) return next === undefined ? list : [...list, next];\n if (next === undefined) return list.filter((_, i) => i !== index);\n return list.map((item, i) => (i === index ? next : item));\n }\n const object = isObject(data) ? data : {};\n const next = setAt(own(object, step), childSchema(schema, step), rest, value);\n if (next === undefined) {\n return Object.fromEntries(Object.entries(object).filter(([key]) => key !== step));\n }\n return { ...object, [step]: next };\n};\n\n/**\n * Puts the value of one side of a conflict into the merged data. The player uses this to pick the\n * side that the merge did not take. A side that holds no value removes the entry.\n */\nexport const resolveConflict = (\n merged: unknown,\n schema: Schema,\n conflict: Conflict,\n side: Side,\n): unknown => setAt(merged, schema, conflict.path, conflict[side]);\n","const CHUNK = 0x8000;\n\n/** Encodes text as base64 from its UTF-8 bytes. It works in the browser and in Node. */\nexport const encodeBase64 = (text: string): string => {\n const bytes = new TextEncoder().encode(text);\n let binary = '';\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n};\n\n/** Decodes base64 to text. Line breaks in the base64 text are ignored. */\nexport const decodeBase64 = (base64: string): string => {\n const binary = atob(base64.replace(/\\s/g, ''));\n return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));\n};\n","/** A file in the cloud. The version changes with every write and settles which write is newer. */\nexport interface RemoteFile {\n content: string;\n version: string;\n}\n\n/**\n * Where the files of an app are kept. A provider reads, writes and removes whole files by path, and\n * refuses a write that was made from an old version of the file. That refusal is what keeps two\n * devices from overwriting each other.\n */\nexport interface SyncProvider {\n /** Returns null when the file does not exist. */\n read(path: string): Promise<RemoteFile | null>;\n /**\n * Writes the file. The expected version is the version that was read, or null to create a file\n * that must not exist yet. Throws `VersionConflictError` when the file changed since.\n */\n write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }>;\n /** Removes the file. Throws `VersionConflictError` when the file changed since. */\n remove(path: string, expectedVersion: string): Promise<void>;\n}\n\n/** The file changed in the cloud after it was read. Read it again and merge. */\nexport class VersionConflictError extends Error {\n constructor(public readonly path: string) {\n super(`The file ${path} changed in the cloud.`);\n this.name = 'VersionConflictError';\n }\n}\n\nexport type ProviderErrorKind =\n /** The token is missing, expired, revoked or has no permission. The player signs in again. */\n | 'auth'\n /** The provider limits the calls for now. Try again after `retryAfterSeconds`. */\n | 'rate-limit'\n /** The repository or folder does not exist, or the token cannot see it. */\n | 'not-found'\n /** The device is offline or the provider cannot be reached. */\n | 'network'\n /** The provider failed. A later try may work. */\n | 'server'\n /** The provider answered with something this package does not understand. */\n | 'invalid';\n\nexport class ProviderError extends Error {\n constructor(\n public readonly kind: ProviderErrorKind,\n message: string,\n public readonly status?: number,\n public readonly retryAfterSeconds?: number,\n ) {\n super(message);\n this.name = 'ProviderError';\n }\n}\n","import { decodeBase64, encodeBase64 } from './base64';\nimport { ProviderError, RemoteFile, SyncProvider, VersionConflictError } from './types';\n\nconst API = 'https://api.github.com';\nconst API_VERSION = '2022-11-28';\n\nexport interface GitHubOptions {\n /** The owner and the name of the repository, such as `chiefpansancolt/gamerdex-data`. */\n repository: string;\n /** Returns the token. It may refresh the token first, so it is called for every request. */\n getToken: () => Promise<string>;\n /** The branch to use. Without it GitHub uses the default branch of the repository. */\n branch?: string;\n /** The message of the commit that a write or a removal makes. */\n commitMessage?: (path: string) => string;\n fetch?: typeof fetch;\n}\n\nexport interface RepositoryCheck {\n private: boolean;\n canWrite: boolean;\n defaultBranch: string;\n}\n\ninterface ContentsResponse {\n sha?: unknown;\n content?: unknown;\n encoding?: unknown;\n}\n\nconst encodePath = (path: string): string => path.split('/').map(encodeURIComponent).join('/');\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst retryAfterOf = (response: Response): number | undefined => {\n const header = response.headers.get('retry-after');\n if (header !== null && Number.isFinite(Number(header))) return Number(header);\n const reset = response.headers.get('x-ratelimit-reset');\n if (reset !== null && Number.isFinite(Number(reset))) {\n return Math.max(0, Number(reset) - Math.floor(Date.now() / 1000));\n }\n return undefined;\n};\n\n/** Keeps the files of an app in a private GitHub repository, through the contents API. */\nexport class GitHubProvider implements SyncProvider {\n private readonly send: typeof fetch;\n\n constructor(private readonly options: GitHubOptions) {\n this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n }\n\n async read(path: string): Promise<RemoteFile | null> {\n const response = await this.request('GET', this.contentsUrl(path));\n if (response.status === 404) return null;\n await this.assertOk(response);\n const body = (await response.json()) as ContentsResponse;\n if (typeof body.sha !== 'string') throw this.invalid('The file has no version.');\n if (body.encoding === 'base64' && typeof body.content === 'string') {\n return { content: decodeBase64(body.content), version: body.sha };\n }\n if (body.encoding === 'none') {\n const raw = await this.request('GET', this.contentsUrl(path), undefined, {\n Accept: 'application/vnd.github.raw+json',\n });\n await this.assertOk(raw);\n return { content: await raw.text(), version: body.sha };\n }\n throw this.invalid('The file content is not in a known encoding.');\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const body: Record<string, unknown> = {\n message: this.message(path),\n content: encodeBase64(content),\n };\n if (expectedVersion !== null) body.sha = expectedVersion;\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('PUT', this.contentsUrl(path), body);\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n const result = (await response.json()) as { content?: { sha?: unknown } };\n const version = result.content?.sha;\n if (typeof version !== 'string') throw this.invalid('The write returned no version.');\n return { version };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const body: Record<string, unknown> = { message: this.message(path), sha: expectedVersion };\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('DELETE', this.contentsUrl(path), body);\n if (response.status === 404) return;\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n }\n\n /**\n * Checks that the repository can be used: it exists, the token can write to it, and it is\n * private. The saved data of a player must never sit in a public repository.\n */\n async checkRepository(): Promise<RepositoryCheck> {\n const response = await this.request('GET', `${API}/repos/${this.options.repository}`);\n await this.assertOk(response);\n const body: unknown = await response.json();\n if (!isRecord(body) || typeof body.private !== 'boolean') {\n throw this.invalid('The repository answer is not understood.');\n }\n const permissions = isRecord(body.permissions) ? body.permissions : {};\n return {\n private: body.private,\n canWrite: permissions.push === true,\n defaultBranch: typeof body.default_branch === 'string' ? body.default_branch : 'main',\n };\n }\n\n private contentsUrl(path: string): string {\n const base = `${API}/repos/${this.options.repository}/contents/${encodePath(path)}`;\n return this.options.branch ? `${base}?ref=${encodeURIComponent(this.options.branch)}` : base;\n }\n\n private message(path: string): string {\n return this.options.commitMessage?.(path) ?? `Gamerdex sync: ${path}`;\n }\n\n private async request(\n method: string,\n url: string,\n body?: Record<string, unknown>,\n headers: Record<string, string> = {},\n ): Promise<Response> {\n const token = await this.options.getToken();\n try {\n return await this.send(url, {\n method,\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': API_VERSION,\n ...(body ? { 'Content-Type': 'application/json' } : {}),\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n }\n\n private async assertOk(response: Response): Promise<void> {\n if (response.ok) return;\n const { status } = response;\n if (status === 401) throw new ProviderError('auth', 'The token is not valid.', status);\n if (status === 403 || status === 429) {\n const limited = status === 429 || response.headers.get('x-ratelimit-remaining') === '0';\n if (limited) {\n throw new ProviderError(\n 'rate-limit',\n 'GitHub limits the calls.',\n status,\n retryAfterOf(response),\n );\n }\n throw new ProviderError('auth', 'The token has no permission for the repository.', status);\n }\n if (status === 404)\n throw new ProviderError('not-found', 'The repository was not found.', status);\n if (status >= 500) throw new ProviderError('server', 'GitHub failed.', status);\n throw this.invalid(`GitHub answered ${status}.`, status);\n }\n\n private invalid(message: string, status?: number): ProviderError {\n return new ProviderError('invalid', message, status);\n }\n}\n","import { RemoteFile, SyncProvider, VersionConflictError } from './types';\n\n/**\n * A provider that keeps the files in memory. It follows the same version rules as a real provider,\n * so the sync flow of an app can be tested without a network.\n */\nexport class MemoryProvider implements SyncProvider {\n private files = new Map<string, { content: string; counter: number }>();\n private counter = 0;\n\n async read(path: string): Promise<RemoteFile | null> {\n const file = this.files.get(path);\n return file ? { content: file.content, version: String(file.counter) } : null;\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const current = this.files.get(path);\n const currentVersion = current ? String(current.counter) : null;\n if (currentVersion !== expectedVersion) throw new VersionConflictError(path);\n this.counter += 1;\n this.files.set(path, { content, counter: this.counter });\n return { version: String(this.counter) };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const current = this.files.get(path);\n if (current && String(current.counter) !== expectedVersion) {\n throw new VersionConflictError(path);\n }\n this.files.delete(path);\n }\n\n /** The paths of the files that exist, in the order they were first written. */\n paths(): string[] {\n return [...this.files.keys()];\n }\n}\n","import { dataPath, metaPath } from '../layout';\nimport { restoreCloudValues } from '../merge/cloud';\nimport { equal } from '../merge/equal';\nimport { merge } from '../merge/merge';\nimport { Conflict, Schema } from '../merge/types';\nimport { emptyMeta, parseMeta, SyncMeta, withDevice } from '../meta';\nimport { SyncProvider, VersionConflictError } from '../provider/types';\n\nconst MAX_ATTEMPTS = 4;\n\n/**\n * What this device remembers about the last sync. The app keeps it in the browser, next to the\n * saved data but outside it, and passes it back on the next sync.\n */\nexport interface SyncState {\n /** The cloud data as this device last saw it. */\n base: unknown;\n dataVersion: string | null;\n metaVersion: string | null;\n}\n\n/** What to do when this device never synced and both the device and the cloud hold data. */\nexport type FirstSyncChoice = 'merge' | 'keep-local' | 'keep-cloud';\n\nexport interface SyncOptions {\n appId: string;\n deviceId: string;\n deviceName: string;\n provider: SyncProvider;\n schema: Schema;\n /** The data of this device. */\n local: unknown;\n /** The state kept after the last sync, or null when this device never synced. */\n state: SyncState | null;\n firstSync?: FirstSyncChoice;\n /** Returns true when the data is whole and safe to store. Called before any write. */\n validate?: (data: unknown) => boolean;\n /** Lets the app change the revisions of the meta file for this sync. */\n updateMeta?: (meta: SyncMeta) => SyncMeta;\n /** The current time as an ISO text. */\n now: () => string;\n}\n\nexport type SyncResult =\n | {\n status: 'synced';\n /** The data to store on this device. */\n data: unknown;\n /** The state to keep for the next sync. */\n state: SyncState;\n conflicts: Conflict[];\n /** True when the cloud changed during this sync. */\n uploaded: boolean;\n }\n | {\n /** The device and the cloud both hold data and the app has not said what to do. */\n status: 'needs-choice';\n local: unknown;\n remote: unknown;\n };\n\n/** The data in the cloud is not whole, so nothing was written. */\nexport class RemoteDataError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RemoteDataError';\n }\n}\n\n/** Other devices kept writing during every try, so the sync stopped. Try again later. */\nexport class SyncBusyError extends Error {\n constructor() {\n super('The cloud kept changing during the sync.');\n this.name = 'SyncBusyError';\n }\n}\n\nconst parseData = (text: string, validate: SyncOptions['validate']): unknown => {\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n throw new RemoteDataError('The synced data is not valid JSON.');\n }\n if (validate && !validate(data)) throw new RemoteDataError('The synced data is not valid.');\n return data;\n};\n\nconst readMeta = (text: string | undefined, appId: string): SyncMeta => {\n if (text === undefined) return emptyMeta(appId);\n const parsed = parseMeta(text, appId);\n if (!parsed.ok) throw new RemoteDataError(parsed.error);\n return parsed.meta;\n};\n\nconst writeMeta = async (options: SyncOptions): Promise<string> => {\n const { provider, appId, deviceId, deviceName, updateMeta, now } = options;\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const current = await provider.read(metaPath(appId));\n const meta = readMeta(current?.content, appId);\n const time = now();\n const updated = withDevice(updateMeta ? updateMeta(meta) : meta, deviceId, {\n name: deviceName,\n lastSeenAt: time,\n });\n try {\n const written = await provider.write(\n metaPath(appId),\n JSON.stringify({ ...updated, lastSyncAt: time }, null, 2),\n current?.version ?? null,\n );\n return written.version;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n throw new SyncBusyError();\n};\n\n/**\n * Brings this device and the cloud to the same data. It reads the cloud, merges the three copies,\n * writes the cloud copy when it changed, and returns the data to store on this device with the\n * state to keep. It never changes the device by itself, and it does not write when the cloud data\n * or the merged data fail `validate`.\n *\n * Throws `ProviderError` when the provider fails, `RemoteDataError` when the cloud data is not\n * whole, and `SyncBusyError` when other devices keep writing.\n */\nexport const sync = async (options: SyncOptions): Promise<SyncResult> => {\n const { appId, provider, schema, local, state, firstSync, validate } = options;\n const path = dataPath(appId);\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const file = await provider.read(path);\n\n if (file === null) {\n if (validate && !validate(local))\n throw new RemoteDataError('The data of this device is not valid.');\n const cloud = restoreCloudValues(local, undefined, schema);\n const created = await tryWrite(provider, path, cloud, null);\n if (created === null) continue;\n const metaVersion = await writeMeta(options);\n return done(local, cloud, created, metaVersion, [], true);\n }\n\n const remote = parseData(file.content, validate);\n const differs = state === null && !equal(restoreCloudValues(local, remote, schema), remote);\n const choice = differs ? firstSync : undefined;\n if (differs && choice === undefined) return { status: 'needs-choice', local, remote };\n\n const base = state === null ? undefined : state.base;\n const merged = mergeFor(choice, { base, local, remote, schema });\n if (validate && !validate(merged.merged)) {\n throw new RemoteDataError('The merged data is not valid.');\n }\n\n if (equal(merged.cloud, remote)) {\n const metaVersion = await writeMeta(options);\n return done(merged.merged, merged.cloud, file.version, metaVersion, merged.conflicts, false);\n }\n\n const version = await tryWrite(provider, path, merged.cloud, file.version);\n if (version === null) continue;\n const metaVersion = await writeMeta(options);\n return done(merged.merged, merged.cloud, version, metaVersion, merged.conflicts, true);\n }\n throw new SyncBusyError();\n};\n\nconst mergeFor = (\n choice: FirstSyncChoice | undefined,\n input: { base: unknown; local: unknown; remote: unknown; schema: Schema },\n) => {\n if (choice === 'keep-local') {\n const cloud = restoreCloudValues(input.local, input.remote, input.schema);\n return { merged: input.local, cloud, conflicts: [] };\n }\n if (choice === 'keep-cloud') {\n const merged = restoreCloudValues(input.remote, input.local, input.schema);\n return { merged, cloud: input.remote, conflicts: [] };\n }\n return merge(input);\n};\n\nconst tryWrite = async (\n provider: SyncProvider,\n path: string,\n data: unknown,\n version: string | null,\n): Promise<string | null> => {\n try {\n return (await provider.write(path, JSON.stringify(data, null, 2), version)).version;\n } catch (error) {\n if (error instanceof VersionConflictError) return null;\n throw error;\n }\n};\n\nconst done = (\n data: unknown,\n cloud: unknown,\n dataVersion: string,\n metaVersion: string,\n conflicts: Conflict[],\n uploaded: boolean,\n): SyncResult => ({\n status: 'synced',\n data,\n state: { base: cloud, dataVersion, metaVersion },\n conflicts,\n uploaded,\n});\n","/** The public values of the Gamerdex Sync GitHub App. The client secret is never kept here. */\nexport const GITHUB_APP = {\n slug: 'gamerdex-sync',\n clientId: 'Iv23liwzjj3x8O2K5ALA',\n} as const;\n","/** The tokens of a signed in player. Times are milliseconds since 1970. */\nexport interface StoredTokens {\n accessToken: string;\n accessExpiresAt: number;\n refreshToken: string;\n refreshExpiresAt: number;\n}\n\n/** The answer of the token function, with lifetimes in seconds. */\nexport interface TokenResponse {\n accessToken: string;\n expiresIn: number;\n refreshToken: string;\n refreshExpiresIn: number;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isSeconds = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0;\n\nexport const parseTokenResponse = (body: unknown): TokenResponse | null => {\n if (!isRecord(body)) return null;\n const { accessToken, expiresIn, refreshToken, refreshExpiresIn } = body;\n if (typeof accessToken !== 'string' || accessToken === '') return null;\n if (typeof refreshToken !== 'string' || refreshToken === '') return null;\n if (!isSeconds(expiresIn) || !isSeconds(refreshExpiresIn)) return null;\n return { accessToken, expiresIn, refreshToken, refreshExpiresIn };\n};\n\nexport const toStoredTokens = (response: TokenResponse, nowMs: number): StoredTokens => ({\n accessToken: response.accessToken,\n accessExpiresAt: nowMs + response.expiresIn * 1000,\n refreshToken: response.refreshToken,\n refreshExpiresAt: nowMs + response.refreshExpiresIn * 1000,\n});\n","import { ProviderError } from '../provider/types';\nimport { parseTokenResponse, StoredTokens, toStoredTokens } from './tokens';\n\nconst RENEW_BEFORE_MS = 60_000;\n\nexport interface TokenSourceOptions {\n /** The address of the token function, without a trailing slash. */\n endpoint: string;\n /** Reads the tokens that this device saved, or null when the player is signed out. */\n load: () => StoredTokens | null;\n /** Saves the tokens, or removes them when null. */\n save: (tokens: StoredTokens | null) => void;\n now: () => number;\n fetch?: typeof fetch;\n}\n\n/**\n * Keeps a player signed in. `getToken` returns a token that is valid for at least a minute and\n * renews it first when needed. It is meant to be the `getToken` of `GitHubProvider`.\n */\nexport const createTokenSource = (options: TokenSourceOptions) => {\n const send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n let renewing: Promise<StoredTokens> | null = null;\n\n const post = async (path: string, body: Record<string, string>): Promise<StoredTokens> => {\n let response: Response;\n try {\n response = await send(`${options.endpoint}/${path}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n } catch {\n throw new ProviderError('network', 'The sign in service could not be reached.');\n }\n if (response.status >= 500) throw new ProviderError('server', 'The sign in service failed.');\n const parsed = response.ok ? parseTokenResponse(await response.json().catch(() => null)) : null;\n if (parsed === null)\n throw new ProviderError('auth', 'The sign in was refused.', response.status);\n const tokens = toStoredTokens(parsed, options.now());\n options.save(tokens);\n return tokens;\n };\n\n const renew = (tokens: StoredTokens): Promise<StoredTokens> => {\n if (tokens.refreshExpiresAt <= options.now()) {\n options.save(null);\n throw new ProviderError('auth', 'The sign in expired.');\n }\n renewing ??= post('refresh', { refreshToken: tokens.refreshToken })\n .catch((error: unknown) => {\n if (error instanceof ProviderError && error.kind === 'auth') options.save(null);\n throw error;\n })\n .finally(() => {\n renewing = null;\n });\n return renewing;\n };\n\n return {\n /** Finishes a sign in with the code from the callback page. */\n signIn: (code: string, redirectUri: string): Promise<StoredTokens> =>\n post('token', { code, redirectUri }),\n signOut: (): void => options.save(null),\n isSignedIn: (): boolean => options.load() !== null,\n getToken: async (): Promise<string> => {\n const tokens = options.load();\n if (tokens === null) throw new ProviderError('auth', 'The player is not signed in.');\n if (tokens.accessExpiresAt - RENEW_BEFORE_MS > options.now()) return tokens.accessToken;\n return (await renew(tokens)).accessToken;\n },\n };\n};\n","import { GITHUB_APP } from './app';\n\n/** A random value that ties a sign-in answer to the sign-in that this device started. */\nexport const newState = (): string => crypto.randomUUID();\n\nexport const authorizeUrl = (redirectUri: string, state: string): string => {\n const query = new URLSearchParams({\n client_id: GITHUB_APP.clientId,\n redirect_uri: redirectUri,\n state,\n });\n return `https://github.com/login/oauth/authorize?${query}`;\n};\n\n/** The page where the player picks the repository that the app may use. */\nexport const installUrl = (state: string): string =>\n `https://github.com/apps/${GITHUB_APP.slug}/installations/new?${new URLSearchParams({ state })}`;\n\nexport type CallbackResult =\n { ok: true; code: string } | { ok: false; reason: 'denied' | 'state' | 'missing' };\n\n/**\n * Reads the query of the sign-in callback page. The state must equal the state that was saved when\n * the sign-in started, or the answer is refused.\n */\nexport const readCallback = (search: string, expectedState: string | null): CallbackResult => {\n const query = new URLSearchParams(search);\n if (query.get('error') !== null) return { ok: false, reason: 'denied' };\n const code = query.get('code');\n if (code === null || code === '') return { ok: false, reason: 'missing' };\n if (expectedState === null || query.get('state') !== expectedState) {\n return { ok: false, reason: 'state' };\n }\n return { ok: true, code };\n};\n","import { ProviderError } from './types';\n\nexport interface InstalledRepository {\n /** The owner and the name, such as `chiefpansancolt/gamerdex-data`. */\n fullName: string;\n private: boolean;\n}\n\nconst API = 'https://api.github.com';\nconst PAGE_SIZE = 100;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst getJson = async (\n send: typeof fetch,\n token: string,\n url: string,\n): Promise<Record<string, unknown>> => {\n let response: Response;\n try {\n response = await send(url, {\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n if (response.status === 401 || response.status === 403) {\n throw new ProviderError('auth', 'The token is not valid.', response.status);\n }\n if (response.status >= 500) throw new ProviderError('server', 'GitHub failed.', response.status);\n const body: unknown = response.ok ? await response.json().catch(() => null) : null;\n if (!isRecord(body))\n throw new ProviderError('invalid', 'GitHub answer is not understood.', response.status);\n return body;\n};\n\n/**\n * Lists the repositories that the player gave to the GitHub App, for the first 100 installations\n * and the first 100 repositories of each. The player picks the repository for the synced data.\n */\nexport const listInstalledRepositories = async (\n token: string,\n send: typeof fetch = (...args) => globalThis.fetch(...args),\n): Promise<InstalledRepository[]> => {\n const installations = await getJson(\n send,\n token,\n `${API}/user/installations?per_page=${PAGE_SIZE}`,\n );\n const ids = (Array.isArray(installations.installations) ? installations.installations : [])\n .filter(isRecord)\n .map((installation) => installation.id)\n .filter((id): id is number => typeof id === 'number');\n\n const repositories: InstalledRepository[] = [];\n for (const id of ids) {\n const page = await getJson(\n send,\n token,\n `${API}/user/installations/${id}/repositories?per_page=${PAGE_SIZE}`,\n );\n for (const item of Array.isArray(page.repositories) ? page.repositories : []) {\n if (\n isRecord(item) &&\n typeof item.full_name === 'string' &&\n typeof item.private === 'boolean'\n ) {\n repositories.push({ fullName: item.full_name, private: item.private });\n }\n }\n }\n return repositories;\n};\n"],"mappings":"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","names":["own","own","childSchema","next","isRecord","metaVersion","isRecord","API","isRecord"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/layout.ts","../src/meta.ts","../src/merge/equal.ts","../src/merge/cloud.ts","../src/merge/merge.ts","../src/merge/resolve.ts","../src/provider/base64.ts","../src/provider/detail.ts","../src/provider/types.ts","../src/provider/github.ts","../src/provider/memory.ts","../src/sync/sync.ts","../src/auth/app.ts","../src/auth/tokens.ts","../src/auth/source.ts","../src/auth/urls.ts","../src/provider/repositories.ts"],"sourcesContent":["export * from './layout';\nexport * from './meta';\nexport * from './merge/equal';\nexport * from './merge/merge';\nexport * from './merge/resolve';\nexport * from './merge/types';\nexport * from './provider/base64';\nexport * from './provider/github';\nexport * from './provider/memory';\nexport * from './provider/types';\nexport * from './sync/sync';\nexport * from './merge/cloud';\nexport * from './auth/app';\nexport * from './auth/source';\nexport * from './auth/tokens';\nexport * from './auth/urls';\nexport * from './provider/repositories';\n","export const ROOT_FOLDER = 'Gamerdex';\nexport const DATA_FILE = 'data.json';\nexport const META_FILE = 'meta.json';\n\nconst APP_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\n/** An app id names a folder, so it is lowercase words joined by single dashes. */\nexport const isAppId = (value: string): boolean => APP_ID.test(value);\n\nconst folderOf = (appId: string): string => {\n if (!isAppId(appId)) throw new RangeError(`Invalid app id: ${appId}`);\n return `${ROOT_FOLDER}/${appId}`;\n};\n\n/** The path of the saved data of an app, such as `Gamerdex/whiteout-survival/data.json`. */\nexport const dataPath = (appId: string): string => `${folderOf(appId)}/${DATA_FILE}`;\n\n/** The path of the bookkeeping file of an app, next to its saved data. */\nexport const metaPath = (appId: string): string => `${folderOf(appId)}/${META_FILE}`;\n","export const META_FORMAT = 1;\n\nexport interface DeviceRecord {\n name: string;\n /** ISO time of the last sync of the device. */\n lastSeenAt: string;\n}\n\n/**\n * The bookkeeping file beside the saved data. It holds no game data and no token. A revision only\n * counts up, so a device with a wrong clock cannot make old data look new.\n */\nexport interface SyncMeta {\n format: typeof META_FORMAT;\n appId: string;\n /** One counter for each chief id. A chief that was removed keeps its counter. */\n revisions: Record<string, number>;\n settingsRevision: number;\n /** Chief ids removed on purpose, with the revision of the removal. */\n removed: Record<string, number>;\n devices: Record<string, DeviceRecord>;\n lastSyncAt: string | null;\n}\n\nexport type ParsedMeta = { ok: true; meta: SyncMeta } | { ok: false; error: string };\n\nexport const emptyMeta = (appId: string): SyncMeta => ({\n format: META_FORMAT,\n appId,\n revisions: {},\n settingsRevision: 0,\n removed: {},\n devices: {},\n lastSyncAt: null,\n});\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isCounter = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 0;\n\nconst isCounterMap = (value: unknown): value is Record<string, number> =>\n isRecord(value) && Object.values(value).every(isCounter);\n\nconst isDevice = (value: unknown): value is DeviceRecord =>\n isRecord(value) && typeof value.name === 'string' && typeof value.lastSeenAt === 'string';\n\n/** Reads the meta file and checks the whole shape. Nothing is returned for a file that fails. */\nexport const parseMeta = (text: string, appId: string): ParsedMeta => {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return { ok: false, error: 'The meta file is not valid JSON.' };\n }\n if (!isRecord(value)) return { ok: false, error: 'The meta file is not an object.' };\n if (value.format !== META_FORMAT) return { ok: false, error: 'The meta format is not known.' };\n if (value.appId !== appId) return { ok: false, error: 'The meta file belongs to another app.' };\n if (!isCounterMap(value.revisions)) return { ok: false, error: 'The revisions are not valid.' };\n if (!isCounter(value.settingsRevision)) {\n return { ok: false, error: 'The settings revision is not valid.' };\n }\n if (!isCounterMap(value.removed)) return { ok: false, error: 'The removed list is not valid.' };\n if (!isRecord(value.devices) || !Object.values(value.devices).every(isDevice)) {\n return { ok: false, error: 'The devices are not valid.' };\n }\n if (value.lastSyncAt !== null && typeof value.lastSyncAt !== 'string') {\n return { ok: false, error: 'The last sync time is not valid.' };\n }\n return { ok: true, meta: value as unknown as SyncMeta };\n};\n\n/** Counts one change of a chief. A chief that returns after a removal leaves the removed list. */\nexport const withChiefRevision = (meta: SyncMeta, chiefId: string): SyncMeta => {\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: (meta.revisions[chiefId] ?? 0) + 1 },\n removed: Object.fromEntries(Object.entries(meta.removed).filter(([id]) => id !== chiefId)),\n };\n};\n\n/** Counts the removal of a chief. The counter keeps rising so an old copy cannot return it. */\nexport const withChiefRemoved = (meta: SyncMeta, chiefId: string): SyncMeta => {\n const revision = (meta.revisions[chiefId] ?? 0) + 1;\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: revision },\n removed: { ...meta.removed, [chiefId]: revision },\n };\n};\n\nexport const withSettingsRevision = (meta: SyncMeta): SyncMeta => ({\n ...meta,\n settingsRevision: meta.settingsRevision + 1,\n});\n\nexport const withDevice = (meta: SyncMeta, deviceId: string, device: DeviceRecord): SyncMeta => ({\n ...meta,\n devices: { ...meta.devices, [deviceId]: device },\n lastSyncAt: device.lastSeenAt,\n});\n","const isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** Deep equality of two JSON values. The order of the keys of an object does not matter. */\nexport const equal = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n if (Array.isArray(a)) {\n return Array.isArray(b) && a.length === b.length && a.every((item, i) => equal(item, b[i]));\n }\n if (isObject(a) && isObject(b)) {\n const keys = Object.keys(a);\n return (\n keys.length === Object.keys(b).length &&\n keys.every((key) => Object.hasOwn(b, key) && equal(a[key], b[key]))\n );\n }\n return false;\n};\n\nexport { isObject };\n","import { isObject } from './equal';\nimport { Schema } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Extract<Schema, { type: 'object' }>, key: string): Schema =>\n (schema.fields && Object.hasOwn(schema.fields, key) ? schema.fields[key] : undefined) ??\n schema.each ?? { type: 'record' };\n\nconst holdsDeviceValue = (schema: Schema): boolean => {\n if (schema.type === 'local') return true;\n if (schema.type === 'keyed') return holdsDeviceValue(schema.item);\n if (schema.type !== 'object') return false;\n return (\n (schema.each !== undefined && holdsDeviceValue(schema.each)) ||\n Object.values(schema.fields ?? {}).some(holdsDeviceValue)\n );\n};\n\n/**\n * Returns the merged data as it must be stored in the cloud: every part with the `local` strategy\n * keeps the value that the cloud already holds, so a value of one device never changes the cloud.\n */\nexport const restoreCloudValues = (merged: unknown, remote: unknown, schema: Schema): unknown => {\n if (schema.type === 'local') return remote;\n if (!holdsDeviceValue(schema)) return merged;\n\n if (schema.type === 'object' && isObject(merged) && (isObject(remote) || remote === undefined)) {\n const cloud = remote ?? {};\n const result: Record<string, unknown> = {};\n for (const key of new Set([...Object.keys(merged), ...Object.keys(cloud)])) {\n const child = childSchema(schema, key);\n const value = restoreCloudValues(own(merged, key), own(cloud, key), child);\n if (value !== undefined) result[key] = value;\n }\n return result;\n }\n\n if (schema.type === 'keyed' && Array.isArray(merged) && Array.isArray(remote)) {\n const remoteById = new Map(\n remote.filter(isObject).map((item) => [item[schema.key], item] as const),\n );\n return merged.map((item) =>\n isObject(item)\n ? restoreCloudValues(item, remoteById.get(item[schema.key]), schema.item)\n : item,\n );\n }\n return merged;\n};\n","import { restoreCloudValues } from './cloud';\nimport { equal, isObject } from './equal';\nimport { Conflict, MergeInput, MergeResult, Path, Schema, Side } from './types';\n\ninterface Context {\n prefer: Side;\n conflicts: Conflict[];\n}\n\n/** Reads an own key only, so an id such as `constructor` never reads from the prototype. */\nconst own = <T>(map: Record<string, T> | undefined, key: string): T | undefined =>\n map !== undefined && Object.hasOwn(map, key) ? map[key] : undefined;\n\nconst record = (\n base: unknown,\n local: unknown,\n remote: unknown,\n path: Path,\n context: Context,\n): unknown => {\n context.conflicts.push({\n path,\n kind: 'both-changed',\n base,\n local,\n remote,\n taken: context.prefer,\n });\n return context.prefer === 'local' ? local : remote;\n};\n\nconst joinLists = (base: unknown[], local: unknown[], remote: unknown[]): unknown[] => {\n const has = (list: unknown[], value: unknown) => list.some((item) => equal(item, value));\n const kept = (value: unknown) => {\n const inBase = has(base, value);\n const inLocal = has(local, value);\n const inRemote = has(remote, value);\n return inBase ? inLocal && inRemote : inLocal || inRemote;\n };\n const result: unknown[] = [];\n for (const value of [...local, ...remote]) {\n if (kept(value) && !has(result, value)) result.push(value);\n }\n return result;\n};\n\nconst entriesOf = (list: unknown[], key: string): Map<string, unknown> | null => {\n const entries = new Map<string, unknown>();\n for (const item of list) {\n if (!isObject(item) || typeof item[key] !== 'string') return null;\n entries.set(item[key], item);\n }\n return entries;\n};\n\nconst mergeEntries = (\n ids: string[],\n pick: (id: string) => [unknown, unknown, unknown],\n schemaOf: (id: string) => Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): Map<string, unknown> => {\n const merged = new Map<string, unknown>();\n for (const id of ids) {\n const [base, local, remote] = pick(id);\n const value = mergeNode(base, local, remote, schemaOf(id), [...path, id], context, deleteWins);\n if (value !== undefined) merged.set(id, value);\n }\n return merged;\n};\n\nconst orderedIds = (local: Iterable<string>, remote: Iterable<string>, base: Iterable<string>) => [\n ...new Set([...local, ...remote, ...base]),\n];\n\nconst mergeNode = (\n base: unknown,\n local: unknown,\n remote: unknown,\n schema: Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): unknown => {\n if (schema.type === 'local') return local;\n if (equal(local, remote)) return local;\n if (equal(local, base)) return remote;\n if (equal(remote, base)) return local;\n\n if (local === undefined || remote === undefined) {\n if (deleteWins) return undefined;\n const taken: Side = local === undefined ? 'remote' : 'local';\n context.conflicts.push({ path, kind: 'delete-edit', base, local, remote, taken });\n return taken === 'local' ? local : remote;\n }\n\n switch (schema.type) {\n case 'max': {\n const l = schema.order(local);\n const r = schema.order(remote);\n if (l === r) return record(base, local, remote, path, context);\n return l > r ? local : remote;\n }\n case 'join':\n if (Array.isArray(local) && Array.isArray(remote)) {\n return joinLists(Array.isArray(base) ? base : [], local, remote);\n }\n return record(base, local, remote, path, context);\n case 'object': {\n if (!isObject(local) || !isObject(remote)) return record(base, local, remote, path, context);\n const baseObject = isObject(base) ? base : {};\n const ids = orderedIds(Object.keys(local), Object.keys(remote), Object.keys(baseObject));\n const merged = mergeEntries(\n ids,\n (id) => [own(baseObject, id), own(local, id), own(remote, id)],\n (id) => own(schema.fields, id) ?? schema.each ?? { type: 'record' },\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return Object.fromEntries(merged);\n }\n case 'keyed': {\n const localEntries = Array.isArray(local) ? entriesOf(local, schema.key) : null;\n const remoteEntries = Array.isArray(remote) ? entriesOf(remote, schema.key) : null;\n const baseEntries = (Array.isArray(base) ? entriesOf(base, schema.key) : null) ?? new Map();\n if (!localEntries || !remoteEntries) return record(base, local, remote, path, context);\n const ids = orderedIds(localEntries.keys(), remoteEntries.keys(), baseEntries.keys());\n const merged = mergeEntries(\n ids,\n (id) => [baseEntries.get(id), localEntries.get(id), remoteEntries.get(id)],\n () => schema.item,\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return [...merged.values()];\n }\n case 'record':\n return record(base, local, remote, path, context);\n }\n};\n\n/**\n * Merges the data of this device and the data of the cloud against the data of the last sync.\n * A part that only one side changed takes that change. A part that both sides changed merges by its\n * schema, and a part that cannot merge is reported in `conflicts` with the preferred side already\n * in `merged`.\n */\nexport const merge = ({\n base,\n local,\n remote,\n schema,\n prefer = 'remote',\n}: MergeInput): MergeResult => {\n const context: Context = { prefer, conflicts: [] };\n const merged = mergeNode(base, local, remote, schema, [], context, false);\n return {\n merged,\n cloud: restoreCloudValues(merged, remote, schema),\n conflicts: context.conflicts,\n };\n};\n","import { isObject } from './equal';\nimport { Conflict, Schema, Side } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Schema, step: string): Schema => {\n if (schema.type === 'object') {\n const named =\n schema.fields && Object.hasOwn(schema.fields, step) ? schema.fields[step] : undefined;\n return named ?? schema.each ?? { type: 'record' };\n }\n return { type: 'record' };\n};\n\nconst setAt = (data: unknown, schema: Schema, path: string[], value: unknown): unknown => {\n if (path.length === 0) return value;\n const [step, ...rest] = path;\n if (schema.type === 'keyed') {\n const list = Array.isArray(data) ? data : [];\n const index = list.findIndex((item) => isObject(item) && item[schema.key] === step);\n const next = setAt(index >= 0 ? list[index] : undefined, schema.item, rest, value);\n if (index < 0) return next === undefined ? list : [...list, next];\n if (next === undefined) return list.filter((_, i) => i !== index);\n return list.map((item, i) => (i === index ? next : item));\n }\n const object = isObject(data) ? data : {};\n const next = setAt(own(object, step), childSchema(schema, step), rest, value);\n if (next === undefined) {\n return Object.fromEntries(Object.entries(object).filter(([key]) => key !== step));\n }\n return { ...object, [step]: next };\n};\n\n/**\n * Puts the value of one side of a conflict into the merged data. The player uses this to pick the\n * side that the merge did not take. A side that holds no value removes the entry.\n */\nexport const resolveConflict = (\n merged: unknown,\n schema: Schema,\n conflict: Conflict,\n side: Side,\n): unknown => setAt(merged, schema, conflict.path, conflict[side]);\n","const CHUNK = 0x8000;\n\n/** Encodes text as base64 from its UTF-8 bytes. It works in the browser and in Node. */\nexport const encodeBase64 = (text: string): string => {\n const bytes = new TextEncoder().encode(text);\n let binary = '';\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n};\n\n/** Decodes base64 to text. Line breaks in the base64 text are ignored. */\nexport const decodeBase64 = (base64: string): string => {\n const binary = atob(base64.replace(/\\s/g, ''));\n return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));\n};\n","const MAX_DETAIL_LENGTH = 300;\n\n/** Reads the `message` that GitHub sends with a failed request, for the logs and the player. */\nexport const detailOf = async (response: Response): Promise<string | undefined> => {\n const body: unknown = await response.json().catch(() => null);\n if (typeof body !== 'object' || body === null) return undefined;\n const message = (body as { message?: unknown }).message;\n return typeof message === 'string' ? message.slice(0, MAX_DETAIL_LENGTH) : undefined;\n};\n","/** A file in the cloud. The version changes with every write and settles which write is newer. */\nexport interface RemoteFile {\n content: string;\n version: string;\n}\n\n/**\n * Where the files of an app are kept. A provider reads, writes and removes whole files by path, and\n * refuses a write that was made from an old version of the file. That refusal is what keeps two\n * devices from overwriting each other.\n */\nexport interface SyncProvider {\n /** Returns null when the file does not exist. */\n read(path: string): Promise<RemoteFile | null>;\n /**\n * Writes the file. The expected version is the version that was read, or null to create a file\n * that must not exist yet. Throws `VersionConflictError` when the file changed since.\n */\n write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }>;\n /** Removes the file. Throws `VersionConflictError` when the file changed since. */\n remove(path: string, expectedVersion: string): Promise<void>;\n}\n\n/** The file changed in the cloud after it was read. Read it again and merge. */\nexport class VersionConflictError extends Error {\n constructor(public readonly path: string) {\n super(`The file ${path} changed in the cloud.`);\n this.name = 'VersionConflictError';\n }\n}\n\nexport type ProviderErrorKind =\n /** The token is missing, expired, revoked or has no permission. The player signs in again. */\n | 'auth'\n /** The provider limits the calls for now. Try again after `retryAfterSeconds`. */\n | 'rate-limit'\n /** The repository or folder does not exist, or the token cannot see it. */\n | 'not-found'\n /** The device is offline or the provider cannot be reached. */\n | 'network'\n /** The provider failed. A later try may work. */\n | 'server'\n /** The provider answered with something this package does not understand. */\n | 'invalid';\n\nexport class ProviderError extends Error {\n constructor(\n public readonly kind: ProviderErrorKind,\n message: string,\n public readonly status?: number,\n public readonly retryAfterSeconds?: number,\n /** The message that the provider sent with the failed request, when it sent one. */\n public readonly detail?: string,\n ) {\n super(message);\n this.name = 'ProviderError';\n }\n}\n","import { decodeBase64, encodeBase64 } from './base64';\nimport { detailOf } from './detail';\nimport { ProviderError, RemoteFile, SyncProvider, VersionConflictError } from './types';\n\nconst API = 'https://api.github.com';\nconst API_VERSION = '2022-11-28';\n\nexport interface GitHubOptions {\n /** The owner and the name of the repository, such as `chiefpansancolt/gamerdex-data`. */\n repository: string;\n /** Returns the token. It may refresh the token first, so it is called for every request. */\n getToken: () => Promise<string>;\n /** The branch to use. Without it GitHub uses the default branch of the repository. */\n branch?: string;\n /** The message of the commit that a write or a removal makes. */\n commitMessage?: (path: string) => string;\n fetch?: typeof fetch;\n}\n\nexport interface RepositoryCheck {\n private: boolean;\n canWrite: boolean;\n defaultBranch: string;\n}\n\ninterface ContentsResponse {\n sha?: unknown;\n content?: unknown;\n encoding?: unknown;\n}\n\nconst encodePath = (path: string): string => path.split('/').map(encodeURIComponent).join('/');\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst retryAfterOf = (response: Response): number | undefined => {\n const header = response.headers.get('retry-after');\n if (header !== null && Number.isFinite(Number(header))) return Number(header);\n const reset = response.headers.get('x-ratelimit-reset');\n if (reset !== null && Number.isFinite(Number(reset))) {\n return Math.max(0, Number(reset) - Math.floor(Date.now() / 1000));\n }\n return undefined;\n};\n\n/** Keeps the files of an app in a private GitHub repository, through the contents API. */\nexport class GitHubProvider implements SyncProvider {\n private readonly send: typeof fetch;\n\n constructor(private readonly options: GitHubOptions) {\n this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n }\n\n async read(path: string): Promise<RemoteFile | null> {\n const response = await this.request('GET', this.contentsUrl(path));\n if (response.status === 404) return null;\n await this.assertOk(response);\n const body = (await response.json()) as ContentsResponse;\n if (typeof body.sha !== 'string') throw this.invalid('The file has no version.');\n if (body.encoding === 'base64' && typeof body.content === 'string') {\n return { content: decodeBase64(body.content), version: body.sha };\n }\n if (body.encoding === 'none') {\n const raw = await this.request('GET', this.contentsUrl(path), undefined, {\n Accept: 'application/vnd.github.raw+json',\n });\n await this.assertOk(raw);\n return { content: await raw.text(), version: body.sha };\n }\n throw this.invalid('The file content is not in a known encoding.');\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const body: Record<string, unknown> = {\n message: this.message(path),\n content: encodeBase64(content),\n };\n if (expectedVersion !== null) body.sha = expectedVersion;\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('PUT', this.contentsUrl(path), body);\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n const result = (await response.json()) as { content?: { sha?: unknown } };\n const version = result.content?.sha;\n if (typeof version !== 'string') throw this.invalid('The write returned no version.');\n return { version };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const body: Record<string, unknown> = { message: this.message(path), sha: expectedVersion };\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('DELETE', this.contentsUrl(path), body);\n if (response.status === 404) return;\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n }\n\n /**\n * Checks that the repository can be used: it exists, the token can write to it, and it is\n * private. The saved data of a player must never sit in a public repository.\n */\n async checkRepository(): Promise<RepositoryCheck> {\n const response = await this.request('GET', `${API}/repos/${this.options.repository}`);\n await this.assertOk(response);\n const body: unknown = await response.json();\n if (!isRecord(body) || typeof body.private !== 'boolean') {\n throw this.invalid('The repository answer is not understood.');\n }\n const permissions = isRecord(body.permissions) ? body.permissions : {};\n return {\n private: body.private,\n canWrite: permissions.push === true,\n defaultBranch: typeof body.default_branch === 'string' ? body.default_branch : 'main',\n };\n }\n\n private contentsUrl(path: string): string {\n const base = `${API}/repos/${this.options.repository}/contents/${encodePath(path)}`;\n return this.options.branch ? `${base}?ref=${encodeURIComponent(this.options.branch)}` : base;\n }\n\n private message(path: string): string {\n return this.options.commitMessage?.(path) ?? `Gamerdex sync: ${path}`;\n }\n\n private async request(\n method: string,\n url: string,\n body?: Record<string, unknown>,\n headers: Record<string, string> = {},\n ): Promise<Response> {\n const token = await this.options.getToken();\n try {\n return await this.send(url, {\n method,\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': API_VERSION,\n ...(body ? { 'Content-Type': 'application/json' } : {}),\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n }\n\n private async assertOk(response: Response): Promise<void> {\n if (response.ok) return;\n const { status } = response;\n const detail = await detailOf(response);\n if (status === 401) {\n throw new ProviderError('auth', 'The token is not valid.', status, undefined, detail);\n }\n if (status === 403 || status === 429) {\n const limited = status === 429 || response.headers.get('x-ratelimit-remaining') === '0';\n if (limited) {\n throw new ProviderError(\n 'rate-limit',\n 'GitHub limits the calls.',\n status,\n retryAfterOf(response),\n detail,\n );\n }\n throw new ProviderError(\n 'auth',\n 'The token has no permission for the repository.',\n status,\n undefined,\n detail,\n );\n }\n if (status === 404) {\n throw new ProviderError(\n 'not-found',\n 'The repository was not found.',\n status,\n undefined,\n detail,\n );\n }\n if (status >= 500) {\n throw new ProviderError('server', 'GitHub failed.', status, undefined, detail);\n }\n throw this.invalid(`GitHub answered ${status}.`, status, detail);\n }\n\n private invalid(message: string, status?: number, detail?: string): ProviderError {\n return new ProviderError('invalid', message, status, undefined, detail);\n }\n}\n","import { RemoteFile, SyncProvider, VersionConflictError } from './types';\n\n/**\n * A provider that keeps the files in memory. It follows the same version rules as a real provider,\n * so the sync flow of an app can be tested without a network.\n */\nexport class MemoryProvider implements SyncProvider {\n private files = new Map<string, { content: string; counter: number }>();\n private counter = 0;\n\n async read(path: string): Promise<RemoteFile | null> {\n const file = this.files.get(path);\n return file ? { content: file.content, version: String(file.counter) } : null;\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const current = this.files.get(path);\n const currentVersion = current ? String(current.counter) : null;\n if (currentVersion !== expectedVersion) throw new VersionConflictError(path);\n this.counter += 1;\n this.files.set(path, { content, counter: this.counter });\n return { version: String(this.counter) };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const current = this.files.get(path);\n if (current && String(current.counter) !== expectedVersion) {\n throw new VersionConflictError(path);\n }\n this.files.delete(path);\n }\n\n /** The paths of the files that exist, in the order they were first written. */\n paths(): string[] {\n return [...this.files.keys()];\n }\n}\n","import { dataPath, metaPath } from '../layout';\nimport { restoreCloudValues } from '../merge/cloud';\nimport { equal } from '../merge/equal';\nimport { merge } from '../merge/merge';\nimport { Conflict, Schema } from '../merge/types';\nimport { emptyMeta, parseMeta, SyncMeta, withDevice } from '../meta';\nimport { SyncProvider, VersionConflictError } from '../provider/types';\n\nconst MAX_ATTEMPTS = 4;\n\n/**\n * What this device remembers about the last sync. The app keeps it in the browser, next to the\n * saved data but outside it, and passes it back on the next sync.\n */\nexport interface SyncState {\n /** The cloud data as this device last saw it. */\n base: unknown;\n dataVersion: string | null;\n metaVersion: string | null;\n}\n\n/** What to do when this device never synced and both the device and the cloud hold data. */\nexport type FirstSyncChoice = 'merge' | 'keep-local' | 'keep-cloud';\n\nexport interface SyncOptions {\n appId: string;\n deviceId: string;\n deviceName: string;\n provider: SyncProvider;\n schema: Schema;\n /** The data of this device. */\n local: unknown;\n /** The state kept after the last sync, or null when this device never synced. */\n state: SyncState | null;\n firstSync?: FirstSyncChoice;\n /** Returns true when the data is whole and safe to store. Called before any write. */\n validate?: (data: unknown) => boolean;\n /** Lets the app change the revisions of the meta file for this sync. */\n updateMeta?: (meta: SyncMeta) => SyncMeta;\n /** The current time as an ISO text. */\n now: () => string;\n}\n\nexport type SyncResult =\n | {\n status: 'synced';\n /** The data to store on this device. */\n data: unknown;\n /** The state to keep for the next sync. */\n state: SyncState;\n conflicts: Conflict[];\n /** True when the cloud changed during this sync. */\n uploaded: boolean;\n }\n | {\n /** The device and the cloud both hold data and the app has not said what to do. */\n status: 'needs-choice';\n local: unknown;\n remote: unknown;\n };\n\n/** The data in the cloud is not whole, so nothing was written. */\nexport class RemoteDataError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RemoteDataError';\n }\n}\n\n/** Other devices kept writing during every try, so the sync stopped. Try again later. */\nexport class SyncBusyError extends Error {\n constructor() {\n super('The cloud kept changing during the sync.');\n this.name = 'SyncBusyError';\n }\n}\n\nconst parseData = (text: string, validate: SyncOptions['validate']): unknown => {\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n throw new RemoteDataError('The synced data is not valid JSON.');\n }\n if (validate && !validate(data)) throw new RemoteDataError('The synced data is not valid.');\n return data;\n};\n\nconst readMeta = (text: string | undefined, appId: string): SyncMeta => {\n if (text === undefined) return emptyMeta(appId);\n const parsed = parseMeta(text, appId);\n if (!parsed.ok) throw new RemoteDataError(parsed.error);\n return parsed.meta;\n};\n\n/**\n * Records this device and the time of the sync in the meta file. A sync that changed nothing writes\n * no file when this device is already recorded, so the repository gets no empty commits.\n */\nconst writeMeta = async (options: SyncOptions, uploaded: boolean): Promise<string> => {\n const { provider, appId, deviceId, deviceName, updateMeta, now } = options;\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const current = await provider.read(metaPath(appId));\n const meta = readMeta(current?.content, appId);\n if (current && !uploaded && !updateMeta && Object.hasOwn(meta.devices, deviceId)) {\n return current.version;\n }\n const time = now();\n const updated = withDevice(updateMeta ? updateMeta(meta) : meta, deviceId, {\n name: deviceName,\n lastSeenAt: time,\n });\n try {\n const written = await provider.write(\n metaPath(appId),\n JSON.stringify({ ...updated, lastSyncAt: time }, null, 2),\n current?.version ?? null,\n );\n return written.version;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n throw new SyncBusyError();\n};\n\n/**\n * Brings this device and the cloud to the same data. It reads the cloud, merges the three copies,\n * writes the cloud copy when it changed, and returns the data to store on this device with the\n * state to keep. It never changes the device by itself, and it does not write when the cloud data\n * or the merged data fail `validate`.\n *\n * Throws `ProviderError` when the provider fails, `RemoteDataError` when the cloud data is not\n * whole, and `SyncBusyError` when other devices keep writing.\n */\nexport const sync = async (options: SyncOptions): Promise<SyncResult> => {\n const { appId, provider, schema, local, state, firstSync, validate } = options;\n const path = dataPath(appId);\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const file = await provider.read(path);\n\n if (file === null) {\n if (validate && !validate(local))\n throw new RemoteDataError('The data of this device is not valid.');\n const cloud = restoreCloudValues(local, undefined, schema);\n const created = await tryWrite(provider, path, cloud, null);\n if (created === null) continue;\n const metaVersion = await writeMeta(options, true);\n return done(local, cloud, created, metaVersion, [], true);\n }\n\n const remote = parseData(file.content, validate);\n const differs = state === null && !equal(restoreCloudValues(local, remote, schema), remote);\n const choice = differs ? firstSync : undefined;\n if (differs && choice === undefined) return { status: 'needs-choice', local, remote };\n\n const base = state === null ? undefined : state.base;\n const merged = mergeFor(choice, { base, local, remote, schema });\n if (validate && !validate(merged.merged)) {\n throw new RemoteDataError('The merged data is not valid.');\n }\n\n if (equal(merged.cloud, remote)) {\n const metaVersion = await writeMeta(options, false);\n return done(merged.merged, merged.cloud, file.version, metaVersion, merged.conflicts, false);\n }\n\n const version = await tryWrite(provider, path, merged.cloud, file.version);\n if (version === null) continue;\n const metaVersion = await writeMeta(options, true);\n return done(merged.merged, merged.cloud, version, metaVersion, merged.conflicts, true);\n }\n throw new SyncBusyError();\n};\n\nconst mergeFor = (\n choice: FirstSyncChoice | undefined,\n input: { base: unknown; local: unknown; remote: unknown; schema: Schema },\n) => {\n if (choice === 'keep-local') {\n const cloud = restoreCloudValues(input.local, input.remote, input.schema);\n return { merged: input.local, cloud, conflicts: [] };\n }\n if (choice === 'keep-cloud') {\n const merged = restoreCloudValues(input.remote, input.local, input.schema);\n return { merged, cloud: input.remote, conflicts: [] };\n }\n return merge(input);\n};\n\nconst tryWrite = async (\n provider: SyncProvider,\n path: string,\n data: unknown,\n version: string | null,\n): Promise<string | null> => {\n try {\n return (await provider.write(path, JSON.stringify(data, null, 2), version)).version;\n } catch (error) {\n if (error instanceof VersionConflictError) return null;\n throw error;\n }\n};\n\nconst done = (\n data: unknown,\n cloud: unknown,\n dataVersion: string,\n metaVersion: string,\n conflicts: Conflict[],\n uploaded: boolean,\n): SyncResult => ({\n status: 'synced',\n data,\n state: { base: cloud, dataVersion, metaVersion },\n conflicts,\n uploaded,\n});\n","/** The public values of the Gamerdex Sync GitHub App. The client secret is never kept here. */\nexport const GITHUB_APP = {\n slug: 'gamerdex-sync',\n clientId: 'Iv23liwzjj3x8O2K5ALA',\n} as const;\n","/** The tokens of a signed in player. Times are milliseconds since 1970. */\nexport interface StoredTokens {\n accessToken: string;\n accessExpiresAt: number;\n refreshToken: string;\n refreshExpiresAt: number;\n}\n\n/** The answer of the token function, with lifetimes in seconds. */\nexport interface TokenResponse {\n accessToken: string;\n expiresIn: number;\n refreshToken: string;\n refreshExpiresIn: number;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isSeconds = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0;\n\nexport const parseTokenResponse = (body: unknown): TokenResponse | null => {\n if (!isRecord(body)) return null;\n const { accessToken, expiresIn, refreshToken, refreshExpiresIn } = body;\n if (typeof accessToken !== 'string' || accessToken === '') return null;\n if (typeof refreshToken !== 'string' || refreshToken === '') return null;\n if (!isSeconds(expiresIn) || !isSeconds(refreshExpiresIn)) return null;\n return { accessToken, expiresIn, refreshToken, refreshExpiresIn };\n};\n\nexport const toStoredTokens = (response: TokenResponse, nowMs: number): StoredTokens => ({\n accessToken: response.accessToken,\n accessExpiresAt: nowMs + response.expiresIn * 1000,\n refreshToken: response.refreshToken,\n refreshExpiresAt: nowMs + response.refreshExpiresIn * 1000,\n});\n","import { ProviderError } from '../provider/types';\nimport { parseTokenResponse, StoredTokens, toStoredTokens } from './tokens';\n\nconst RENEW_BEFORE_MS = 60_000;\n\nexport interface TokenSourceOptions {\n /** The address of the token function, without a trailing slash. */\n endpoint: string;\n /** Reads the tokens that this device saved, or null when the player is signed out. */\n load: () => StoredTokens | null;\n /** Saves the tokens, or removes them when null. */\n save: (tokens: StoredTokens | null) => void;\n now: () => number;\n fetch?: typeof fetch;\n}\n\n/**\n * Keeps a player signed in. `getToken` returns a token that is valid for at least a minute and\n * renews it first when needed. It is meant to be the `getToken` of `GitHubProvider`.\n */\nexport const createTokenSource = (options: TokenSourceOptions) => {\n const send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n let renewing: Promise<StoredTokens> | null = null;\n\n const post = async (path: string, body: Record<string, string>): Promise<StoredTokens> => {\n let response: Response;\n try {\n response = await send(`${options.endpoint}/${path}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n } catch {\n throw new ProviderError('network', 'The sign in service could not be reached.');\n }\n if (response.status >= 500) throw new ProviderError('server', 'The sign in service failed.');\n const parsed = response.ok ? parseTokenResponse(await response.json().catch(() => null)) : null;\n if (parsed === null)\n throw new ProviderError('auth', 'The sign in was refused.', response.status);\n const tokens = toStoredTokens(parsed, options.now());\n options.save(tokens);\n return tokens;\n };\n\n const renew = (tokens: StoredTokens): Promise<StoredTokens> => {\n if (tokens.refreshExpiresAt <= options.now()) {\n options.save(null);\n throw new ProviderError('auth', 'The sign in expired.');\n }\n renewing ??= post('refresh', { refreshToken: tokens.refreshToken })\n .catch((error: unknown) => {\n if (error instanceof ProviderError && error.kind === 'auth') options.save(null);\n throw error;\n })\n .finally(() => {\n renewing = null;\n });\n return renewing;\n };\n\n return {\n /** Finishes a sign in with the code from the callback page. */\n signIn: (code: string, redirectUri: string): Promise<StoredTokens> =>\n post('token', { code, redirectUri }),\n signOut: (): void => options.save(null),\n isSignedIn: (): boolean => options.load() !== null,\n getToken: async (): Promise<string> => {\n const tokens = options.load();\n if (tokens === null) throw new ProviderError('auth', 'The player is not signed in.');\n if (tokens.accessExpiresAt - RENEW_BEFORE_MS > options.now()) return tokens.accessToken;\n return (await renew(tokens)).accessToken;\n },\n };\n};\n","import { GITHUB_APP } from './app';\n\n/** A random value that ties a sign-in answer to the sign-in that this device started. */\nexport const newState = (): string => crypto.randomUUID();\n\nexport const authorizeUrl = (redirectUri: string, state: string): string => {\n const query = new URLSearchParams({\n client_id: GITHUB_APP.clientId,\n redirect_uri: redirectUri,\n state,\n });\n return `https://github.com/login/oauth/authorize?${query}`;\n};\n\n/** The page where the player picks the repository that the app may use. */\nexport const installUrl = (state: string): string =>\n `https://github.com/apps/${GITHUB_APP.slug}/installations/new?${new URLSearchParams({ state })}`;\n\nexport type CallbackResult =\n { ok: true; code: string } | { ok: false; reason: 'denied' | 'state' | 'missing' };\n\n/**\n * Reads the query of the sign-in callback page. The state must equal the state that was saved when\n * the sign-in started, or the answer is refused.\n */\nexport const readCallback = (search: string, expectedState: string | null): CallbackResult => {\n const query = new URLSearchParams(search);\n if (query.get('error') !== null) return { ok: false, reason: 'denied' };\n const code = query.get('code');\n if (code === null || code === '') return { ok: false, reason: 'missing' };\n if (expectedState === null || query.get('state') !== expectedState) {\n return { ok: false, reason: 'state' };\n }\n return { ok: true, code };\n};\n","import { detailOf } from './detail';\nimport { ProviderError } from './types';\n\nexport interface InstalledRepository {\n /** The owner and the name, such as `chiefpansancolt/gamerdex-data`. */\n fullName: string;\n private: boolean;\n}\n\nconst API = 'https://api.github.com';\nconst PAGE_SIZE = 100;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst getJson = async (\n send: typeof fetch,\n token: string,\n url: string,\n): Promise<Record<string, unknown>> => {\n let response: Response;\n try {\n response = await send(url, {\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n if (!response.ok) {\n const { status } = response;\n const detail = await detailOf(response);\n if (status === 401 || status === 403) {\n throw new ProviderError('auth', 'The token is not valid.', status, undefined, detail);\n }\n if (status >= 500)\n throw new ProviderError('server', 'GitHub failed.', status, undefined, detail);\n throw new ProviderError(\n 'invalid',\n 'GitHub answer is not understood.',\n status,\n undefined,\n detail,\n );\n }\n const body: unknown = await response.json().catch(() => null);\n if (!isRecord(body)) {\n throw new ProviderError('invalid', 'GitHub answer is not understood.', response.status);\n }\n return body;\n};\n\n/**\n * Lists the repositories that the player gave to the GitHub App, for the first 100 installations\n * and the first 100 repositories of each. The player picks the repository for the synced data.\n */\nexport const listInstalledRepositories = async (\n token: string,\n send: typeof fetch = (...args) => globalThis.fetch(...args),\n): Promise<InstalledRepository[]> => {\n const installations = await getJson(\n send,\n token,\n `${API}/user/installations?per_page=${PAGE_SIZE}`,\n );\n const ids = (Array.isArray(installations.installations) ? installations.installations : [])\n .filter(isRecord)\n .map((installation) => installation.id)\n .filter((id): id is number => typeof id === 'number');\n\n const repositories: InstalledRepository[] = [];\n for (const id of ids) {\n const page = await getJson(\n send,\n token,\n `${API}/user/installations/${id}/repositories?per_page=${PAGE_SIZE}`,\n );\n for (const item of Array.isArray(page.repositories) ? page.repositories : []) {\n if (\n isRecord(item) &&\n typeof item.full_name === 'string' &&\n typeof item.private === 'boolean'\n ) {\n repositories.push({ fullName: item.full_name, private: item.private });\n }\n }\n }\n return repositories;\n};\n"],"mappings":"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","names":["own","own","childSchema","next","isRecord","metaVersion","isRecord","API","isRecord"]}
|
package/dist/index.mjs
CHANGED
|
@@ -285,6 +285,15 @@ var decodeBase64 = (base64) => {
|
|
|
285
285
|
return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
+
// src/provider/detail.ts
|
|
289
|
+
var MAX_DETAIL_LENGTH = 300;
|
|
290
|
+
var detailOf = async (response) => {
|
|
291
|
+
const body = await response.json().catch(() => null);
|
|
292
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
293
|
+
const message = body.message;
|
|
294
|
+
return typeof message === "string" ? message.slice(0, MAX_DETAIL_LENGTH) : void 0;
|
|
295
|
+
};
|
|
296
|
+
|
|
288
297
|
// src/provider/types.ts
|
|
289
298
|
var VersionConflictError = class extends Error {
|
|
290
299
|
constructor(path) {
|
|
@@ -295,16 +304,18 @@ var VersionConflictError = class extends Error {
|
|
|
295
304
|
path;
|
|
296
305
|
};
|
|
297
306
|
var ProviderError = class extends Error {
|
|
298
|
-
constructor(kind, message, status, retryAfterSeconds) {
|
|
307
|
+
constructor(kind, message, status, retryAfterSeconds, detail) {
|
|
299
308
|
super(message);
|
|
300
309
|
this.kind = kind;
|
|
301
310
|
this.status = status;
|
|
302
311
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
312
|
+
this.detail = detail;
|
|
303
313
|
this.name = "ProviderError";
|
|
304
314
|
}
|
|
305
315
|
kind;
|
|
306
316
|
status;
|
|
307
317
|
retryAfterSeconds;
|
|
318
|
+
detail;
|
|
308
319
|
};
|
|
309
320
|
|
|
310
321
|
// src/provider/github.ts
|
|
@@ -415,7 +426,10 @@ var GitHubProvider = class {
|
|
|
415
426
|
async assertOk(response) {
|
|
416
427
|
if (response.ok) return;
|
|
417
428
|
const { status } = response;
|
|
418
|
-
|
|
429
|
+
const detail = await detailOf(response);
|
|
430
|
+
if (status === 401) {
|
|
431
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
432
|
+
}
|
|
419
433
|
if (status === 403 || status === 429) {
|
|
420
434
|
const limited = status === 429 || response.headers.get("x-ratelimit-remaining") === "0";
|
|
421
435
|
if (limited) {
|
|
@@ -423,18 +437,34 @@ var GitHubProvider = class {
|
|
|
423
437
|
"rate-limit",
|
|
424
438
|
"GitHub limits the calls.",
|
|
425
439
|
status,
|
|
426
|
-
retryAfterOf(response)
|
|
440
|
+
retryAfterOf(response),
|
|
441
|
+
detail
|
|
427
442
|
);
|
|
428
443
|
}
|
|
429
|
-
throw new ProviderError(
|
|
444
|
+
throw new ProviderError(
|
|
445
|
+
"auth",
|
|
446
|
+
"The token has no permission for the repository.",
|
|
447
|
+
status,
|
|
448
|
+
void 0,
|
|
449
|
+
detail
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (status === 404) {
|
|
453
|
+
throw new ProviderError(
|
|
454
|
+
"not-found",
|
|
455
|
+
"The repository was not found.",
|
|
456
|
+
status,
|
|
457
|
+
void 0,
|
|
458
|
+
detail
|
|
459
|
+
);
|
|
430
460
|
}
|
|
431
|
-
if (status
|
|
432
|
-
throw new ProviderError("
|
|
433
|
-
|
|
434
|
-
throw this.invalid(`GitHub answered ${status}.`, status);
|
|
461
|
+
if (status >= 500) {
|
|
462
|
+
throw new ProviderError("server", "GitHub failed.", status, void 0, detail);
|
|
463
|
+
}
|
|
464
|
+
throw this.invalid(`GitHub answered ${status}.`, status, detail);
|
|
435
465
|
}
|
|
436
|
-
invalid(message, status) {
|
|
437
|
-
return new ProviderError("invalid", message, status);
|
|
466
|
+
invalid(message, status, detail) {
|
|
467
|
+
return new ProviderError("invalid", message, status, void 0, detail);
|
|
438
468
|
}
|
|
439
469
|
};
|
|
440
470
|
|
|
@@ -497,11 +527,14 @@ var readMeta = (text, appId) => {
|
|
|
497
527
|
if (!parsed.ok) throw new RemoteDataError(parsed.error);
|
|
498
528
|
return parsed.meta;
|
|
499
529
|
};
|
|
500
|
-
var writeMeta = async (options) => {
|
|
530
|
+
var writeMeta = async (options, uploaded) => {
|
|
501
531
|
const { provider, appId, deviceId, deviceName, updateMeta, now } = options;
|
|
502
532
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
|
|
503
533
|
const current = await provider.read(metaPath(appId));
|
|
504
534
|
const meta = readMeta(current?.content, appId);
|
|
535
|
+
if (current && !uploaded && !updateMeta && Object.hasOwn(meta.devices, deviceId)) {
|
|
536
|
+
return current.version;
|
|
537
|
+
}
|
|
505
538
|
const time = now();
|
|
506
539
|
const updated = withDevice(updateMeta ? updateMeta(meta) : meta, deviceId, {
|
|
507
540
|
name: deviceName,
|
|
@@ -531,7 +564,7 @@ var sync = async (options) => {
|
|
|
531
564
|
const cloud = restoreCloudValues(local, void 0, schema);
|
|
532
565
|
const created = await tryWrite(provider, path, cloud, null);
|
|
533
566
|
if (created === null) continue;
|
|
534
|
-
const metaVersion2 = await writeMeta(options);
|
|
567
|
+
const metaVersion2 = await writeMeta(options, true);
|
|
535
568
|
return done(local, cloud, created, metaVersion2, [], true);
|
|
536
569
|
}
|
|
537
570
|
const remote = parseData(file.content, validate);
|
|
@@ -544,12 +577,12 @@ var sync = async (options) => {
|
|
|
544
577
|
throw new RemoteDataError("The merged data is not valid.");
|
|
545
578
|
}
|
|
546
579
|
if (equal(merged.cloud, remote)) {
|
|
547
|
-
const metaVersion2 = await writeMeta(options);
|
|
580
|
+
const metaVersion2 = await writeMeta(options, false);
|
|
548
581
|
return done(merged.merged, merged.cloud, file.version, metaVersion2, merged.conflicts, false);
|
|
549
582
|
}
|
|
550
583
|
const version = await tryWrite(provider, path, merged.cloud, file.version);
|
|
551
584
|
if (version === null) continue;
|
|
552
|
-
const metaVersion = await writeMeta(options);
|
|
585
|
+
const metaVersion = await writeMeta(options, true);
|
|
553
586
|
return done(merged.merged, merged.cloud, version, metaVersion, merged.conflicts, true);
|
|
554
587
|
}
|
|
555
588
|
throw new SyncBusyError();
|
|
@@ -695,13 +728,26 @@ var getJson = async (send, token, url) => {
|
|
|
695
728
|
} catch {
|
|
696
729
|
throw new ProviderError("network", "GitHub could not be reached.");
|
|
697
730
|
}
|
|
698
|
-
if (response.
|
|
699
|
-
|
|
731
|
+
if (!response.ok) {
|
|
732
|
+
const { status } = response;
|
|
733
|
+
const detail = await detailOf(response);
|
|
734
|
+
if (status === 401 || status === 403) {
|
|
735
|
+
throw new ProviderError("auth", "The token is not valid.", status, void 0, detail);
|
|
736
|
+
}
|
|
737
|
+
if (status >= 500)
|
|
738
|
+
throw new ProviderError("server", "GitHub failed.", status, void 0, detail);
|
|
739
|
+
throw new ProviderError(
|
|
740
|
+
"invalid",
|
|
741
|
+
"GitHub answer is not understood.",
|
|
742
|
+
status,
|
|
743
|
+
void 0,
|
|
744
|
+
detail
|
|
745
|
+
);
|
|
700
746
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (!isRecord4(body))
|
|
747
|
+
const body = await response.json().catch(() => null);
|
|
748
|
+
if (!isRecord4(body)) {
|
|
704
749
|
throw new ProviderError("invalid", "GitHub answer is not understood.", response.status);
|
|
750
|
+
}
|
|
705
751
|
return body;
|
|
706
752
|
};
|
|
707
753
|
var listInstalledRepositories = async (token, send = (...args) => globalThis.fetch(...args)) => {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/layout.ts","../src/meta.ts","../src/merge/equal.ts","../src/merge/cloud.ts","../src/merge/merge.ts","../src/merge/resolve.ts","../src/provider/base64.ts","../src/provider/types.ts","../src/provider/github.ts","../src/provider/memory.ts","../src/sync/sync.ts","../src/auth/app.ts","../src/auth/tokens.ts","../src/auth/source.ts","../src/auth/urls.ts","../src/provider/repositories.ts"],"sourcesContent":["export const ROOT_FOLDER = 'Gamerdex';\nexport const DATA_FILE = 'data.json';\nexport const META_FILE = 'meta.json';\n\nconst APP_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\n/** An app id names a folder, so it is lowercase words joined by single dashes. */\nexport const isAppId = (value: string): boolean => APP_ID.test(value);\n\nconst folderOf = (appId: string): string => {\n if (!isAppId(appId)) throw new RangeError(`Invalid app id: ${appId}`);\n return `${ROOT_FOLDER}/${appId}`;\n};\n\n/** The path of the saved data of an app, such as `Gamerdex/whiteout-survival/data.json`. */\nexport const dataPath = (appId: string): string => `${folderOf(appId)}/${DATA_FILE}`;\n\n/** The path of the bookkeeping file of an app, next to its saved data. */\nexport const metaPath = (appId: string): string => `${folderOf(appId)}/${META_FILE}`;\n","export const META_FORMAT = 1;\n\nexport interface DeviceRecord {\n name: string;\n /** ISO time of the last sync of the device. */\n lastSeenAt: string;\n}\n\n/**\n * The bookkeeping file beside the saved data. It holds no game data and no token. A revision only\n * counts up, so a device with a wrong clock cannot make old data look new.\n */\nexport interface SyncMeta {\n format: typeof META_FORMAT;\n appId: string;\n /** One counter for each chief id. A chief that was removed keeps its counter. */\n revisions: Record<string, number>;\n settingsRevision: number;\n /** Chief ids removed on purpose, with the revision of the removal. */\n removed: Record<string, number>;\n devices: Record<string, DeviceRecord>;\n lastSyncAt: string | null;\n}\n\nexport type ParsedMeta = { ok: true; meta: SyncMeta } | { ok: false; error: string };\n\nexport const emptyMeta = (appId: string): SyncMeta => ({\n format: META_FORMAT,\n appId,\n revisions: {},\n settingsRevision: 0,\n removed: {},\n devices: {},\n lastSyncAt: null,\n});\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isCounter = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 0;\n\nconst isCounterMap = (value: unknown): value is Record<string, number> =>\n isRecord(value) && Object.values(value).every(isCounter);\n\nconst isDevice = (value: unknown): value is DeviceRecord =>\n isRecord(value) && typeof value.name === 'string' && typeof value.lastSeenAt === 'string';\n\n/** Reads the meta file and checks the whole shape. Nothing is returned for a file that fails. */\nexport const parseMeta = (text: string, appId: string): ParsedMeta => {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return { ok: false, error: 'The meta file is not valid JSON.' };\n }\n if (!isRecord(value)) return { ok: false, error: 'The meta file is not an object.' };\n if (value.format !== META_FORMAT) return { ok: false, error: 'The meta format is not known.' };\n if (value.appId !== appId) return { ok: false, error: 'The meta file belongs to another app.' };\n if (!isCounterMap(value.revisions)) return { ok: false, error: 'The revisions are not valid.' };\n if (!isCounter(value.settingsRevision)) {\n return { ok: false, error: 'The settings revision is not valid.' };\n }\n if (!isCounterMap(value.removed)) return { ok: false, error: 'The removed list is not valid.' };\n if (!isRecord(value.devices) || !Object.values(value.devices).every(isDevice)) {\n return { ok: false, error: 'The devices are not valid.' };\n }\n if (value.lastSyncAt !== null && typeof value.lastSyncAt !== 'string') {\n return { ok: false, error: 'The last sync time is not valid.' };\n }\n return { ok: true, meta: value as unknown as SyncMeta };\n};\n\n/** Counts one change of a chief. A chief that returns after a removal leaves the removed list. */\nexport const withChiefRevision = (meta: SyncMeta, chiefId: string): SyncMeta => {\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: (meta.revisions[chiefId] ?? 0) + 1 },\n removed: Object.fromEntries(Object.entries(meta.removed).filter(([id]) => id !== chiefId)),\n };\n};\n\n/** Counts the removal of a chief. The counter keeps rising so an old copy cannot return it. */\nexport const withChiefRemoved = (meta: SyncMeta, chiefId: string): SyncMeta => {\n const revision = (meta.revisions[chiefId] ?? 0) + 1;\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: revision },\n removed: { ...meta.removed, [chiefId]: revision },\n };\n};\n\nexport const withSettingsRevision = (meta: SyncMeta): SyncMeta => ({\n ...meta,\n settingsRevision: meta.settingsRevision + 1,\n});\n\nexport const withDevice = (meta: SyncMeta, deviceId: string, device: DeviceRecord): SyncMeta => ({\n ...meta,\n devices: { ...meta.devices, [deviceId]: device },\n lastSyncAt: device.lastSeenAt,\n});\n","const isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** Deep equality of two JSON values. The order of the keys of an object does not matter. */\nexport const equal = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n if (Array.isArray(a)) {\n return Array.isArray(b) && a.length === b.length && a.every((item, i) => equal(item, b[i]));\n }\n if (isObject(a) && isObject(b)) {\n const keys = Object.keys(a);\n return (\n keys.length === Object.keys(b).length &&\n keys.every((key) => Object.hasOwn(b, key) && equal(a[key], b[key]))\n );\n }\n return false;\n};\n\nexport { isObject };\n","import { isObject } from './equal';\nimport { Schema } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Extract<Schema, { type: 'object' }>, key: string): Schema =>\n (schema.fields && Object.hasOwn(schema.fields, key) ? schema.fields[key] : undefined) ??\n schema.each ?? { type: 'record' };\n\nconst holdsDeviceValue = (schema: Schema): boolean => {\n if (schema.type === 'local') return true;\n if (schema.type === 'keyed') return holdsDeviceValue(schema.item);\n if (schema.type !== 'object') return false;\n return (\n (schema.each !== undefined && holdsDeviceValue(schema.each)) ||\n Object.values(schema.fields ?? {}).some(holdsDeviceValue)\n );\n};\n\n/**\n * Returns the merged data as it must be stored in the cloud: every part with the `local` strategy\n * keeps the value that the cloud already holds, so a value of one device never changes the cloud.\n */\nexport const restoreCloudValues = (merged: unknown, remote: unknown, schema: Schema): unknown => {\n if (schema.type === 'local') return remote;\n if (!holdsDeviceValue(schema)) return merged;\n\n if (schema.type === 'object' && isObject(merged) && (isObject(remote) || remote === undefined)) {\n const cloud = remote ?? {};\n const result: Record<string, unknown> = {};\n for (const key of new Set([...Object.keys(merged), ...Object.keys(cloud)])) {\n const child = childSchema(schema, key);\n const value = restoreCloudValues(own(merged, key), own(cloud, key), child);\n if (value !== undefined) result[key] = value;\n }\n return result;\n }\n\n if (schema.type === 'keyed' && Array.isArray(merged) && Array.isArray(remote)) {\n const remoteById = new Map(\n remote.filter(isObject).map((item) => [item[schema.key], item] as const),\n );\n return merged.map((item) =>\n isObject(item)\n ? restoreCloudValues(item, remoteById.get(item[schema.key]), schema.item)\n : item,\n );\n }\n return merged;\n};\n","import { restoreCloudValues } from './cloud';\nimport { equal, isObject } from './equal';\nimport { Conflict, MergeInput, MergeResult, Path, Schema, Side } from './types';\n\ninterface Context {\n prefer: Side;\n conflicts: Conflict[];\n}\n\n/** Reads an own key only, so an id such as `constructor` never reads from the prototype. */\nconst own = <T>(map: Record<string, T> | undefined, key: string): T | undefined =>\n map !== undefined && Object.hasOwn(map, key) ? map[key] : undefined;\n\nconst record = (\n base: unknown,\n local: unknown,\n remote: unknown,\n path: Path,\n context: Context,\n): unknown => {\n context.conflicts.push({\n path,\n kind: 'both-changed',\n base,\n local,\n remote,\n taken: context.prefer,\n });\n return context.prefer === 'local' ? local : remote;\n};\n\nconst joinLists = (base: unknown[], local: unknown[], remote: unknown[]): unknown[] => {\n const has = (list: unknown[], value: unknown) => list.some((item) => equal(item, value));\n const kept = (value: unknown) => {\n const inBase = has(base, value);\n const inLocal = has(local, value);\n const inRemote = has(remote, value);\n return inBase ? inLocal && inRemote : inLocal || inRemote;\n };\n const result: unknown[] = [];\n for (const value of [...local, ...remote]) {\n if (kept(value) && !has(result, value)) result.push(value);\n }\n return result;\n};\n\nconst entriesOf = (list: unknown[], key: string): Map<string, unknown> | null => {\n const entries = new Map<string, unknown>();\n for (const item of list) {\n if (!isObject(item) || typeof item[key] !== 'string') return null;\n entries.set(item[key], item);\n }\n return entries;\n};\n\nconst mergeEntries = (\n ids: string[],\n pick: (id: string) => [unknown, unknown, unknown],\n schemaOf: (id: string) => Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): Map<string, unknown> => {\n const merged = new Map<string, unknown>();\n for (const id of ids) {\n const [base, local, remote] = pick(id);\n const value = mergeNode(base, local, remote, schemaOf(id), [...path, id], context, deleteWins);\n if (value !== undefined) merged.set(id, value);\n }\n return merged;\n};\n\nconst orderedIds = (local: Iterable<string>, remote: Iterable<string>, base: Iterable<string>) => [\n ...new Set([...local, ...remote, ...base]),\n];\n\nconst mergeNode = (\n base: unknown,\n local: unknown,\n remote: unknown,\n schema: Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): unknown => {\n if (schema.type === 'local') return local;\n if (equal(local, remote)) return local;\n if (equal(local, base)) return remote;\n if (equal(remote, base)) return local;\n\n if (local === undefined || remote === undefined) {\n if (deleteWins) return undefined;\n const taken: Side = local === undefined ? 'remote' : 'local';\n context.conflicts.push({ path, kind: 'delete-edit', base, local, remote, taken });\n return taken === 'local' ? local : remote;\n }\n\n switch (schema.type) {\n case 'max': {\n const l = schema.order(local);\n const r = schema.order(remote);\n if (l === r) return record(base, local, remote, path, context);\n return l > r ? local : remote;\n }\n case 'join':\n if (Array.isArray(local) && Array.isArray(remote)) {\n return joinLists(Array.isArray(base) ? base : [], local, remote);\n }\n return record(base, local, remote, path, context);\n case 'object': {\n if (!isObject(local) || !isObject(remote)) return record(base, local, remote, path, context);\n const baseObject = isObject(base) ? base : {};\n const ids = orderedIds(Object.keys(local), Object.keys(remote), Object.keys(baseObject));\n const merged = mergeEntries(\n ids,\n (id) => [own(baseObject, id), own(local, id), own(remote, id)],\n (id) => own(schema.fields, id) ?? schema.each ?? { type: 'record' },\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return Object.fromEntries(merged);\n }\n case 'keyed': {\n const localEntries = Array.isArray(local) ? entriesOf(local, schema.key) : null;\n const remoteEntries = Array.isArray(remote) ? entriesOf(remote, schema.key) : null;\n const baseEntries = (Array.isArray(base) ? entriesOf(base, schema.key) : null) ?? new Map();\n if (!localEntries || !remoteEntries) return record(base, local, remote, path, context);\n const ids = orderedIds(localEntries.keys(), remoteEntries.keys(), baseEntries.keys());\n const merged = mergeEntries(\n ids,\n (id) => [baseEntries.get(id), localEntries.get(id), remoteEntries.get(id)],\n () => schema.item,\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return [...merged.values()];\n }\n case 'record':\n return record(base, local, remote, path, context);\n }\n};\n\n/**\n * Merges the data of this device and the data of the cloud against the data of the last sync.\n * A part that only one side changed takes that change. A part that both sides changed merges by its\n * schema, and a part that cannot merge is reported in `conflicts` with the preferred side already\n * in `merged`.\n */\nexport const merge = ({\n base,\n local,\n remote,\n schema,\n prefer = 'remote',\n}: MergeInput): MergeResult => {\n const context: Context = { prefer, conflicts: [] };\n const merged = mergeNode(base, local, remote, schema, [], context, false);\n return {\n merged,\n cloud: restoreCloudValues(merged, remote, schema),\n conflicts: context.conflicts,\n };\n};\n","import { isObject } from './equal';\nimport { Conflict, Schema, Side } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Schema, step: string): Schema => {\n if (schema.type === 'object') {\n const named =\n schema.fields && Object.hasOwn(schema.fields, step) ? schema.fields[step] : undefined;\n return named ?? schema.each ?? { type: 'record' };\n }\n return { type: 'record' };\n};\n\nconst setAt = (data: unknown, schema: Schema, path: string[], value: unknown): unknown => {\n if (path.length === 0) return value;\n const [step, ...rest] = path;\n if (schema.type === 'keyed') {\n const list = Array.isArray(data) ? data : [];\n const index = list.findIndex((item) => isObject(item) && item[schema.key] === step);\n const next = setAt(index >= 0 ? list[index] : undefined, schema.item, rest, value);\n if (index < 0) return next === undefined ? list : [...list, next];\n if (next === undefined) return list.filter((_, i) => i !== index);\n return list.map((item, i) => (i === index ? next : item));\n }\n const object = isObject(data) ? data : {};\n const next = setAt(own(object, step), childSchema(schema, step), rest, value);\n if (next === undefined) {\n return Object.fromEntries(Object.entries(object).filter(([key]) => key !== step));\n }\n return { ...object, [step]: next };\n};\n\n/**\n * Puts the value of one side of a conflict into the merged data. The player uses this to pick the\n * side that the merge did not take. A side that holds no value removes the entry.\n */\nexport const resolveConflict = (\n merged: unknown,\n schema: Schema,\n conflict: Conflict,\n side: Side,\n): unknown => setAt(merged, schema, conflict.path, conflict[side]);\n","const CHUNK = 0x8000;\n\n/** Encodes text as base64 from its UTF-8 bytes. It works in the browser and in Node. */\nexport const encodeBase64 = (text: string): string => {\n const bytes = new TextEncoder().encode(text);\n let binary = '';\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n};\n\n/** Decodes base64 to text. Line breaks in the base64 text are ignored. */\nexport const decodeBase64 = (base64: string): string => {\n const binary = atob(base64.replace(/\\s/g, ''));\n return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));\n};\n","/** A file in the cloud. The version changes with every write and settles which write is newer. */\nexport interface RemoteFile {\n content: string;\n version: string;\n}\n\n/**\n * Where the files of an app are kept. A provider reads, writes and removes whole files by path, and\n * refuses a write that was made from an old version of the file. That refusal is what keeps two\n * devices from overwriting each other.\n */\nexport interface SyncProvider {\n /** Returns null when the file does not exist. */\n read(path: string): Promise<RemoteFile | null>;\n /**\n * Writes the file. The expected version is the version that was read, or null to create a file\n * that must not exist yet. Throws `VersionConflictError` when the file changed since.\n */\n write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }>;\n /** Removes the file. Throws `VersionConflictError` when the file changed since. */\n remove(path: string, expectedVersion: string): Promise<void>;\n}\n\n/** The file changed in the cloud after it was read. Read it again and merge. */\nexport class VersionConflictError extends Error {\n constructor(public readonly path: string) {\n super(`The file ${path} changed in the cloud.`);\n this.name = 'VersionConflictError';\n }\n}\n\nexport type ProviderErrorKind =\n /** The token is missing, expired, revoked or has no permission. The player signs in again. */\n | 'auth'\n /** The provider limits the calls for now. Try again after `retryAfterSeconds`. */\n | 'rate-limit'\n /** The repository or folder does not exist, or the token cannot see it. */\n | 'not-found'\n /** The device is offline or the provider cannot be reached. */\n | 'network'\n /** The provider failed. A later try may work. */\n | 'server'\n /** The provider answered with something this package does not understand. */\n | 'invalid';\n\nexport class ProviderError extends Error {\n constructor(\n public readonly kind: ProviderErrorKind,\n message: string,\n public readonly status?: number,\n public readonly retryAfterSeconds?: number,\n ) {\n super(message);\n this.name = 'ProviderError';\n }\n}\n","import { decodeBase64, encodeBase64 } from './base64';\nimport { ProviderError, RemoteFile, SyncProvider, VersionConflictError } from './types';\n\nconst API = 'https://api.github.com';\nconst API_VERSION = '2022-11-28';\n\nexport interface GitHubOptions {\n /** The owner and the name of the repository, such as `chiefpansancolt/gamerdex-data`. */\n repository: string;\n /** Returns the token. It may refresh the token first, so it is called for every request. */\n getToken: () => Promise<string>;\n /** The branch to use. Without it GitHub uses the default branch of the repository. */\n branch?: string;\n /** The message of the commit that a write or a removal makes. */\n commitMessage?: (path: string) => string;\n fetch?: typeof fetch;\n}\n\nexport interface RepositoryCheck {\n private: boolean;\n canWrite: boolean;\n defaultBranch: string;\n}\n\ninterface ContentsResponse {\n sha?: unknown;\n content?: unknown;\n encoding?: unknown;\n}\n\nconst encodePath = (path: string): string => path.split('/').map(encodeURIComponent).join('/');\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst retryAfterOf = (response: Response): number | undefined => {\n const header = response.headers.get('retry-after');\n if (header !== null && Number.isFinite(Number(header))) return Number(header);\n const reset = response.headers.get('x-ratelimit-reset');\n if (reset !== null && Number.isFinite(Number(reset))) {\n return Math.max(0, Number(reset) - Math.floor(Date.now() / 1000));\n }\n return undefined;\n};\n\n/** Keeps the files of an app in a private GitHub repository, through the contents API. */\nexport class GitHubProvider implements SyncProvider {\n private readonly send: typeof fetch;\n\n constructor(private readonly options: GitHubOptions) {\n this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n }\n\n async read(path: string): Promise<RemoteFile | null> {\n const response = await this.request('GET', this.contentsUrl(path));\n if (response.status === 404) return null;\n await this.assertOk(response);\n const body = (await response.json()) as ContentsResponse;\n if (typeof body.sha !== 'string') throw this.invalid('The file has no version.');\n if (body.encoding === 'base64' && typeof body.content === 'string') {\n return { content: decodeBase64(body.content), version: body.sha };\n }\n if (body.encoding === 'none') {\n const raw = await this.request('GET', this.contentsUrl(path), undefined, {\n Accept: 'application/vnd.github.raw+json',\n });\n await this.assertOk(raw);\n return { content: await raw.text(), version: body.sha };\n }\n throw this.invalid('The file content is not in a known encoding.');\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const body: Record<string, unknown> = {\n message: this.message(path),\n content: encodeBase64(content),\n };\n if (expectedVersion !== null) body.sha = expectedVersion;\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('PUT', this.contentsUrl(path), body);\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n const result = (await response.json()) as { content?: { sha?: unknown } };\n const version = result.content?.sha;\n if (typeof version !== 'string') throw this.invalid('The write returned no version.');\n return { version };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const body: Record<string, unknown> = { message: this.message(path), sha: expectedVersion };\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('DELETE', this.contentsUrl(path), body);\n if (response.status === 404) return;\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n }\n\n /**\n * Checks that the repository can be used: it exists, the token can write to it, and it is\n * private. The saved data of a player must never sit in a public repository.\n */\n async checkRepository(): Promise<RepositoryCheck> {\n const response = await this.request('GET', `${API}/repos/${this.options.repository}`);\n await this.assertOk(response);\n const body: unknown = await response.json();\n if (!isRecord(body) || typeof body.private !== 'boolean') {\n throw this.invalid('The repository answer is not understood.');\n }\n const permissions = isRecord(body.permissions) ? body.permissions : {};\n return {\n private: body.private,\n canWrite: permissions.push === true,\n defaultBranch: typeof body.default_branch === 'string' ? body.default_branch : 'main',\n };\n }\n\n private contentsUrl(path: string): string {\n const base = `${API}/repos/${this.options.repository}/contents/${encodePath(path)}`;\n return this.options.branch ? `${base}?ref=${encodeURIComponent(this.options.branch)}` : base;\n }\n\n private message(path: string): string {\n return this.options.commitMessage?.(path) ?? `Gamerdex sync: ${path}`;\n }\n\n private async request(\n method: string,\n url: string,\n body?: Record<string, unknown>,\n headers: Record<string, string> = {},\n ): Promise<Response> {\n const token = await this.options.getToken();\n try {\n return await this.send(url, {\n method,\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': API_VERSION,\n ...(body ? { 'Content-Type': 'application/json' } : {}),\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n }\n\n private async assertOk(response: Response): Promise<void> {\n if (response.ok) return;\n const { status } = response;\n if (status === 401) throw new ProviderError('auth', 'The token is not valid.', status);\n if (status === 403 || status === 429) {\n const limited = status === 429 || response.headers.get('x-ratelimit-remaining') === '0';\n if (limited) {\n throw new ProviderError(\n 'rate-limit',\n 'GitHub limits the calls.',\n status,\n retryAfterOf(response),\n );\n }\n throw new ProviderError('auth', 'The token has no permission for the repository.', status);\n }\n if (status === 404)\n throw new ProviderError('not-found', 'The repository was not found.', status);\n if (status >= 500) throw new ProviderError('server', 'GitHub failed.', status);\n throw this.invalid(`GitHub answered ${status}.`, status);\n }\n\n private invalid(message: string, status?: number): ProviderError {\n return new ProviderError('invalid', message, status);\n }\n}\n","import { RemoteFile, SyncProvider, VersionConflictError } from './types';\n\n/**\n * A provider that keeps the files in memory. It follows the same version rules as a real provider,\n * so the sync flow of an app can be tested without a network.\n */\nexport class MemoryProvider implements SyncProvider {\n private files = new Map<string, { content: string; counter: number }>();\n private counter = 0;\n\n async read(path: string): Promise<RemoteFile | null> {\n const file = this.files.get(path);\n return file ? { content: file.content, version: String(file.counter) } : null;\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const current = this.files.get(path);\n const currentVersion = current ? String(current.counter) : null;\n if (currentVersion !== expectedVersion) throw new VersionConflictError(path);\n this.counter += 1;\n this.files.set(path, { content, counter: this.counter });\n return { version: String(this.counter) };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const current = this.files.get(path);\n if (current && String(current.counter) !== expectedVersion) {\n throw new VersionConflictError(path);\n }\n this.files.delete(path);\n }\n\n /** The paths of the files that exist, in the order they were first written. */\n paths(): string[] {\n return [...this.files.keys()];\n }\n}\n","import { dataPath, metaPath } from '../layout';\nimport { restoreCloudValues } from '../merge/cloud';\nimport { equal } from '../merge/equal';\nimport { merge } from '../merge/merge';\nimport { Conflict, Schema } from '../merge/types';\nimport { emptyMeta, parseMeta, SyncMeta, withDevice } from '../meta';\nimport { SyncProvider, VersionConflictError } from '../provider/types';\n\nconst MAX_ATTEMPTS = 4;\n\n/**\n * What this device remembers about the last sync. The app keeps it in the browser, next to the\n * saved data but outside it, and passes it back on the next sync.\n */\nexport interface SyncState {\n /** The cloud data as this device last saw it. */\n base: unknown;\n dataVersion: string | null;\n metaVersion: string | null;\n}\n\n/** What to do when this device never synced and both the device and the cloud hold data. */\nexport type FirstSyncChoice = 'merge' | 'keep-local' | 'keep-cloud';\n\nexport interface SyncOptions {\n appId: string;\n deviceId: string;\n deviceName: string;\n provider: SyncProvider;\n schema: Schema;\n /** The data of this device. */\n local: unknown;\n /** The state kept after the last sync, or null when this device never synced. */\n state: SyncState | null;\n firstSync?: FirstSyncChoice;\n /** Returns true when the data is whole and safe to store. Called before any write. */\n validate?: (data: unknown) => boolean;\n /** Lets the app change the revisions of the meta file for this sync. */\n updateMeta?: (meta: SyncMeta) => SyncMeta;\n /** The current time as an ISO text. */\n now: () => string;\n}\n\nexport type SyncResult =\n | {\n status: 'synced';\n /** The data to store on this device. */\n data: unknown;\n /** The state to keep for the next sync. */\n state: SyncState;\n conflicts: Conflict[];\n /** True when the cloud changed during this sync. */\n uploaded: boolean;\n }\n | {\n /** The device and the cloud both hold data and the app has not said what to do. */\n status: 'needs-choice';\n local: unknown;\n remote: unknown;\n };\n\n/** The data in the cloud is not whole, so nothing was written. */\nexport class RemoteDataError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RemoteDataError';\n }\n}\n\n/** Other devices kept writing during every try, so the sync stopped. Try again later. */\nexport class SyncBusyError extends Error {\n constructor() {\n super('The cloud kept changing during the sync.');\n this.name = 'SyncBusyError';\n }\n}\n\nconst parseData = (text: string, validate: SyncOptions['validate']): unknown => {\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n throw new RemoteDataError('The synced data is not valid JSON.');\n }\n if (validate && !validate(data)) throw new RemoteDataError('The synced data is not valid.');\n return data;\n};\n\nconst readMeta = (text: string | undefined, appId: string): SyncMeta => {\n if (text === undefined) return emptyMeta(appId);\n const parsed = parseMeta(text, appId);\n if (!parsed.ok) throw new RemoteDataError(parsed.error);\n return parsed.meta;\n};\n\nconst writeMeta = async (options: SyncOptions): Promise<string> => {\n const { provider, appId, deviceId, deviceName, updateMeta, now } = options;\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const current = await provider.read(metaPath(appId));\n const meta = readMeta(current?.content, appId);\n const time = now();\n const updated = withDevice(updateMeta ? updateMeta(meta) : meta, deviceId, {\n name: deviceName,\n lastSeenAt: time,\n });\n try {\n const written = await provider.write(\n metaPath(appId),\n JSON.stringify({ ...updated, lastSyncAt: time }, null, 2),\n current?.version ?? null,\n );\n return written.version;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n throw new SyncBusyError();\n};\n\n/**\n * Brings this device and the cloud to the same data. It reads the cloud, merges the three copies,\n * writes the cloud copy when it changed, and returns the data to store on this device with the\n * state to keep. It never changes the device by itself, and it does not write when the cloud data\n * or the merged data fail `validate`.\n *\n * Throws `ProviderError` when the provider fails, `RemoteDataError` when the cloud data is not\n * whole, and `SyncBusyError` when other devices keep writing.\n */\nexport const sync = async (options: SyncOptions): Promise<SyncResult> => {\n const { appId, provider, schema, local, state, firstSync, validate } = options;\n const path = dataPath(appId);\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const file = await provider.read(path);\n\n if (file === null) {\n if (validate && !validate(local))\n throw new RemoteDataError('The data of this device is not valid.');\n const cloud = restoreCloudValues(local, undefined, schema);\n const created = await tryWrite(provider, path, cloud, null);\n if (created === null) continue;\n const metaVersion = await writeMeta(options);\n return done(local, cloud, created, metaVersion, [], true);\n }\n\n const remote = parseData(file.content, validate);\n const differs = state === null && !equal(restoreCloudValues(local, remote, schema), remote);\n const choice = differs ? firstSync : undefined;\n if (differs && choice === undefined) return { status: 'needs-choice', local, remote };\n\n const base = state === null ? undefined : state.base;\n const merged = mergeFor(choice, { base, local, remote, schema });\n if (validate && !validate(merged.merged)) {\n throw new RemoteDataError('The merged data is not valid.');\n }\n\n if (equal(merged.cloud, remote)) {\n const metaVersion = await writeMeta(options);\n return done(merged.merged, merged.cloud, file.version, metaVersion, merged.conflicts, false);\n }\n\n const version = await tryWrite(provider, path, merged.cloud, file.version);\n if (version === null) continue;\n const metaVersion = await writeMeta(options);\n return done(merged.merged, merged.cloud, version, metaVersion, merged.conflicts, true);\n }\n throw new SyncBusyError();\n};\n\nconst mergeFor = (\n choice: FirstSyncChoice | undefined,\n input: { base: unknown; local: unknown; remote: unknown; schema: Schema },\n) => {\n if (choice === 'keep-local') {\n const cloud = restoreCloudValues(input.local, input.remote, input.schema);\n return { merged: input.local, cloud, conflicts: [] };\n }\n if (choice === 'keep-cloud') {\n const merged = restoreCloudValues(input.remote, input.local, input.schema);\n return { merged, cloud: input.remote, conflicts: [] };\n }\n return merge(input);\n};\n\nconst tryWrite = async (\n provider: SyncProvider,\n path: string,\n data: unknown,\n version: string | null,\n): Promise<string | null> => {\n try {\n return (await provider.write(path, JSON.stringify(data, null, 2), version)).version;\n } catch (error) {\n if (error instanceof VersionConflictError) return null;\n throw error;\n }\n};\n\nconst done = (\n data: unknown,\n cloud: unknown,\n dataVersion: string,\n metaVersion: string,\n conflicts: Conflict[],\n uploaded: boolean,\n): SyncResult => ({\n status: 'synced',\n data,\n state: { base: cloud, dataVersion, metaVersion },\n conflicts,\n uploaded,\n});\n","/** The public values of the Gamerdex Sync GitHub App. The client secret is never kept here. */\nexport const GITHUB_APP = {\n slug: 'gamerdex-sync',\n clientId: 'Iv23liwzjj3x8O2K5ALA',\n} as const;\n","/** The tokens of a signed in player. Times are milliseconds since 1970. */\nexport interface StoredTokens {\n accessToken: string;\n accessExpiresAt: number;\n refreshToken: string;\n refreshExpiresAt: number;\n}\n\n/** The answer of the token function, with lifetimes in seconds. */\nexport interface TokenResponse {\n accessToken: string;\n expiresIn: number;\n refreshToken: string;\n refreshExpiresIn: number;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isSeconds = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0;\n\nexport const parseTokenResponse = (body: unknown): TokenResponse | null => {\n if (!isRecord(body)) return null;\n const { accessToken, expiresIn, refreshToken, refreshExpiresIn } = body;\n if (typeof accessToken !== 'string' || accessToken === '') return null;\n if (typeof refreshToken !== 'string' || refreshToken === '') return null;\n if (!isSeconds(expiresIn) || !isSeconds(refreshExpiresIn)) return null;\n return { accessToken, expiresIn, refreshToken, refreshExpiresIn };\n};\n\nexport const toStoredTokens = (response: TokenResponse, nowMs: number): StoredTokens => ({\n accessToken: response.accessToken,\n accessExpiresAt: nowMs + response.expiresIn * 1000,\n refreshToken: response.refreshToken,\n refreshExpiresAt: nowMs + response.refreshExpiresIn * 1000,\n});\n","import { ProviderError } from '../provider/types';\nimport { parseTokenResponse, StoredTokens, toStoredTokens } from './tokens';\n\nconst RENEW_BEFORE_MS = 60_000;\n\nexport interface TokenSourceOptions {\n /** The address of the token function, without a trailing slash. */\n endpoint: string;\n /** Reads the tokens that this device saved, or null when the player is signed out. */\n load: () => StoredTokens | null;\n /** Saves the tokens, or removes them when null. */\n save: (tokens: StoredTokens | null) => void;\n now: () => number;\n fetch?: typeof fetch;\n}\n\n/**\n * Keeps a player signed in. `getToken` returns a token that is valid for at least a minute and\n * renews it first when needed. It is meant to be the `getToken` of `GitHubProvider`.\n */\nexport const createTokenSource = (options: TokenSourceOptions) => {\n const send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n let renewing: Promise<StoredTokens> | null = null;\n\n const post = async (path: string, body: Record<string, string>): Promise<StoredTokens> => {\n let response: Response;\n try {\n response = await send(`${options.endpoint}/${path}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n } catch {\n throw new ProviderError('network', 'The sign in service could not be reached.');\n }\n if (response.status >= 500) throw new ProviderError('server', 'The sign in service failed.');\n const parsed = response.ok ? parseTokenResponse(await response.json().catch(() => null)) : null;\n if (parsed === null)\n throw new ProviderError('auth', 'The sign in was refused.', response.status);\n const tokens = toStoredTokens(parsed, options.now());\n options.save(tokens);\n return tokens;\n };\n\n const renew = (tokens: StoredTokens): Promise<StoredTokens> => {\n if (tokens.refreshExpiresAt <= options.now()) {\n options.save(null);\n throw new ProviderError('auth', 'The sign in expired.');\n }\n renewing ??= post('refresh', { refreshToken: tokens.refreshToken })\n .catch((error: unknown) => {\n if (error instanceof ProviderError && error.kind === 'auth') options.save(null);\n throw error;\n })\n .finally(() => {\n renewing = null;\n });\n return renewing;\n };\n\n return {\n /** Finishes a sign in with the code from the callback page. */\n signIn: (code: string, redirectUri: string): Promise<StoredTokens> =>\n post('token', { code, redirectUri }),\n signOut: (): void => options.save(null),\n isSignedIn: (): boolean => options.load() !== null,\n getToken: async (): Promise<string> => {\n const tokens = options.load();\n if (tokens === null) throw new ProviderError('auth', 'The player is not signed in.');\n if (tokens.accessExpiresAt - RENEW_BEFORE_MS > options.now()) return tokens.accessToken;\n return (await renew(tokens)).accessToken;\n },\n };\n};\n","import { GITHUB_APP } from './app';\n\n/** A random value that ties a sign-in answer to the sign-in that this device started. */\nexport const newState = (): string => crypto.randomUUID();\n\nexport const authorizeUrl = (redirectUri: string, state: string): string => {\n const query = new URLSearchParams({\n client_id: GITHUB_APP.clientId,\n redirect_uri: redirectUri,\n state,\n });\n return `https://github.com/login/oauth/authorize?${query}`;\n};\n\n/** The page where the player picks the repository that the app may use. */\nexport const installUrl = (state: string): string =>\n `https://github.com/apps/${GITHUB_APP.slug}/installations/new?${new URLSearchParams({ state })}`;\n\nexport type CallbackResult =\n { ok: true; code: string } | { ok: false; reason: 'denied' | 'state' | 'missing' };\n\n/**\n * Reads the query of the sign-in callback page. The state must equal the state that was saved when\n * the sign-in started, or the answer is refused.\n */\nexport const readCallback = (search: string, expectedState: string | null): CallbackResult => {\n const query = new URLSearchParams(search);\n if (query.get('error') !== null) return { ok: false, reason: 'denied' };\n const code = query.get('code');\n if (code === null || code === '') return { ok: false, reason: 'missing' };\n if (expectedState === null || query.get('state') !== expectedState) {\n return { ok: false, reason: 'state' };\n }\n return { ok: true, code };\n};\n","import { ProviderError } from './types';\n\nexport interface InstalledRepository {\n /** The owner and the name, such as `chiefpansancolt/gamerdex-data`. */\n fullName: string;\n private: boolean;\n}\n\nconst API = 'https://api.github.com';\nconst PAGE_SIZE = 100;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst getJson = async (\n send: typeof fetch,\n token: string,\n url: string,\n): Promise<Record<string, unknown>> => {\n let response: Response;\n try {\n response = await send(url, {\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n if (response.status === 401 || response.status === 403) {\n throw new ProviderError('auth', 'The token is not valid.', response.status);\n }\n if (response.status >= 500) throw new ProviderError('server', 'GitHub failed.', response.status);\n const body: unknown = response.ok ? await response.json().catch(() => null) : null;\n if (!isRecord(body))\n throw new ProviderError('invalid', 'GitHub answer is not understood.', response.status);\n return body;\n};\n\n/**\n * Lists the repositories that the player gave to the GitHub App, for the first 100 installations\n * and the first 100 repositories of each. The player picks the repository for the synced data.\n */\nexport const listInstalledRepositories = async (\n token: string,\n send: typeof fetch = (...args) => globalThis.fetch(...args),\n): Promise<InstalledRepository[]> => {\n const installations = await getJson(\n send,\n token,\n `${API}/user/installations?per_page=${PAGE_SIZE}`,\n );\n const ids = (Array.isArray(installations.installations) ? installations.installations : [])\n .filter(isRecord)\n .map((installation) => installation.id)\n .filter((id): id is number => typeof id === 'number');\n\n const repositories: InstalledRepository[] = [];\n for (const id of ids) {\n const page = await getJson(\n send,\n token,\n `${API}/user/installations/${id}/repositories?per_page=${PAGE_SIZE}`,\n );\n for (const item of Array.isArray(page.repositories) ? page.repositories : []) {\n if (\n isRecord(item) &&\n typeof item.full_name === 'string' &&\n typeof item.private === 'boolean'\n ) {\n repositories.push({ fullName: item.full_name, private: item.private });\n }\n }\n }\n return repositories;\n};\n"],"mappings":"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","names":["own","own","childSchema","next","isRecord","metaVersion","isRecord","API","isRecord"]}
|
|
1
|
+
{"version":3,"sources":["../src/layout.ts","../src/meta.ts","../src/merge/equal.ts","../src/merge/cloud.ts","../src/merge/merge.ts","../src/merge/resolve.ts","../src/provider/base64.ts","../src/provider/detail.ts","../src/provider/types.ts","../src/provider/github.ts","../src/provider/memory.ts","../src/sync/sync.ts","../src/auth/app.ts","../src/auth/tokens.ts","../src/auth/source.ts","../src/auth/urls.ts","../src/provider/repositories.ts"],"sourcesContent":["export const ROOT_FOLDER = 'Gamerdex';\nexport const DATA_FILE = 'data.json';\nexport const META_FILE = 'meta.json';\n\nconst APP_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\n/** An app id names a folder, so it is lowercase words joined by single dashes. */\nexport const isAppId = (value: string): boolean => APP_ID.test(value);\n\nconst folderOf = (appId: string): string => {\n if (!isAppId(appId)) throw new RangeError(`Invalid app id: ${appId}`);\n return `${ROOT_FOLDER}/${appId}`;\n};\n\n/** The path of the saved data of an app, such as `Gamerdex/whiteout-survival/data.json`. */\nexport const dataPath = (appId: string): string => `${folderOf(appId)}/${DATA_FILE}`;\n\n/** The path of the bookkeeping file of an app, next to its saved data. */\nexport const metaPath = (appId: string): string => `${folderOf(appId)}/${META_FILE}`;\n","export const META_FORMAT = 1;\n\nexport interface DeviceRecord {\n name: string;\n /** ISO time of the last sync of the device. */\n lastSeenAt: string;\n}\n\n/**\n * The bookkeeping file beside the saved data. It holds no game data and no token. A revision only\n * counts up, so a device with a wrong clock cannot make old data look new.\n */\nexport interface SyncMeta {\n format: typeof META_FORMAT;\n appId: string;\n /** One counter for each chief id. A chief that was removed keeps its counter. */\n revisions: Record<string, number>;\n settingsRevision: number;\n /** Chief ids removed on purpose, with the revision of the removal. */\n removed: Record<string, number>;\n devices: Record<string, DeviceRecord>;\n lastSyncAt: string | null;\n}\n\nexport type ParsedMeta = { ok: true; meta: SyncMeta } | { ok: false; error: string };\n\nexport const emptyMeta = (appId: string): SyncMeta => ({\n format: META_FORMAT,\n appId,\n revisions: {},\n settingsRevision: 0,\n removed: {},\n devices: {},\n lastSyncAt: null,\n});\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isCounter = (value: unknown): value is number =>\n typeof value === 'number' && Number.isInteger(value) && value >= 0;\n\nconst isCounterMap = (value: unknown): value is Record<string, number> =>\n isRecord(value) && Object.values(value).every(isCounter);\n\nconst isDevice = (value: unknown): value is DeviceRecord =>\n isRecord(value) && typeof value.name === 'string' && typeof value.lastSeenAt === 'string';\n\n/** Reads the meta file and checks the whole shape. Nothing is returned for a file that fails. */\nexport const parseMeta = (text: string, appId: string): ParsedMeta => {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return { ok: false, error: 'The meta file is not valid JSON.' };\n }\n if (!isRecord(value)) return { ok: false, error: 'The meta file is not an object.' };\n if (value.format !== META_FORMAT) return { ok: false, error: 'The meta format is not known.' };\n if (value.appId !== appId) return { ok: false, error: 'The meta file belongs to another app.' };\n if (!isCounterMap(value.revisions)) return { ok: false, error: 'The revisions are not valid.' };\n if (!isCounter(value.settingsRevision)) {\n return { ok: false, error: 'The settings revision is not valid.' };\n }\n if (!isCounterMap(value.removed)) return { ok: false, error: 'The removed list is not valid.' };\n if (!isRecord(value.devices) || !Object.values(value.devices).every(isDevice)) {\n return { ok: false, error: 'The devices are not valid.' };\n }\n if (value.lastSyncAt !== null && typeof value.lastSyncAt !== 'string') {\n return { ok: false, error: 'The last sync time is not valid.' };\n }\n return { ok: true, meta: value as unknown as SyncMeta };\n};\n\n/** Counts one change of a chief. A chief that returns after a removal leaves the removed list. */\nexport const withChiefRevision = (meta: SyncMeta, chiefId: string): SyncMeta => {\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: (meta.revisions[chiefId] ?? 0) + 1 },\n removed: Object.fromEntries(Object.entries(meta.removed).filter(([id]) => id !== chiefId)),\n };\n};\n\n/** Counts the removal of a chief. The counter keeps rising so an old copy cannot return it. */\nexport const withChiefRemoved = (meta: SyncMeta, chiefId: string): SyncMeta => {\n const revision = (meta.revisions[chiefId] ?? 0) + 1;\n return {\n ...meta,\n revisions: { ...meta.revisions, [chiefId]: revision },\n removed: { ...meta.removed, [chiefId]: revision },\n };\n};\n\nexport const withSettingsRevision = (meta: SyncMeta): SyncMeta => ({\n ...meta,\n settingsRevision: meta.settingsRevision + 1,\n});\n\nexport const withDevice = (meta: SyncMeta, deviceId: string, device: DeviceRecord): SyncMeta => ({\n ...meta,\n devices: { ...meta.devices, [deviceId]: device },\n lastSyncAt: device.lastSeenAt,\n});\n","const isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/** Deep equality of two JSON values. The order of the keys of an object does not matter. */\nexport const equal = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n if (Array.isArray(a)) {\n return Array.isArray(b) && a.length === b.length && a.every((item, i) => equal(item, b[i]));\n }\n if (isObject(a) && isObject(b)) {\n const keys = Object.keys(a);\n return (\n keys.length === Object.keys(b).length &&\n keys.every((key) => Object.hasOwn(b, key) && equal(a[key], b[key]))\n );\n }\n return false;\n};\n\nexport { isObject };\n","import { isObject } from './equal';\nimport { Schema } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Extract<Schema, { type: 'object' }>, key: string): Schema =>\n (schema.fields && Object.hasOwn(schema.fields, key) ? schema.fields[key] : undefined) ??\n schema.each ?? { type: 'record' };\n\nconst holdsDeviceValue = (schema: Schema): boolean => {\n if (schema.type === 'local') return true;\n if (schema.type === 'keyed') return holdsDeviceValue(schema.item);\n if (schema.type !== 'object') return false;\n return (\n (schema.each !== undefined && holdsDeviceValue(schema.each)) ||\n Object.values(schema.fields ?? {}).some(holdsDeviceValue)\n );\n};\n\n/**\n * Returns the merged data as it must be stored in the cloud: every part with the `local` strategy\n * keeps the value that the cloud already holds, so a value of one device never changes the cloud.\n */\nexport const restoreCloudValues = (merged: unknown, remote: unknown, schema: Schema): unknown => {\n if (schema.type === 'local') return remote;\n if (!holdsDeviceValue(schema)) return merged;\n\n if (schema.type === 'object' && isObject(merged) && (isObject(remote) || remote === undefined)) {\n const cloud = remote ?? {};\n const result: Record<string, unknown> = {};\n for (const key of new Set([...Object.keys(merged), ...Object.keys(cloud)])) {\n const child = childSchema(schema, key);\n const value = restoreCloudValues(own(merged, key), own(cloud, key), child);\n if (value !== undefined) result[key] = value;\n }\n return result;\n }\n\n if (schema.type === 'keyed' && Array.isArray(merged) && Array.isArray(remote)) {\n const remoteById = new Map(\n remote.filter(isObject).map((item) => [item[schema.key], item] as const),\n );\n return merged.map((item) =>\n isObject(item)\n ? restoreCloudValues(item, remoteById.get(item[schema.key]), schema.item)\n : item,\n );\n }\n return merged;\n};\n","import { restoreCloudValues } from './cloud';\nimport { equal, isObject } from './equal';\nimport { Conflict, MergeInput, MergeResult, Path, Schema, Side } from './types';\n\ninterface Context {\n prefer: Side;\n conflicts: Conflict[];\n}\n\n/** Reads an own key only, so an id such as `constructor` never reads from the prototype. */\nconst own = <T>(map: Record<string, T> | undefined, key: string): T | undefined =>\n map !== undefined && Object.hasOwn(map, key) ? map[key] : undefined;\n\nconst record = (\n base: unknown,\n local: unknown,\n remote: unknown,\n path: Path,\n context: Context,\n): unknown => {\n context.conflicts.push({\n path,\n kind: 'both-changed',\n base,\n local,\n remote,\n taken: context.prefer,\n });\n return context.prefer === 'local' ? local : remote;\n};\n\nconst joinLists = (base: unknown[], local: unknown[], remote: unknown[]): unknown[] => {\n const has = (list: unknown[], value: unknown) => list.some((item) => equal(item, value));\n const kept = (value: unknown) => {\n const inBase = has(base, value);\n const inLocal = has(local, value);\n const inRemote = has(remote, value);\n return inBase ? inLocal && inRemote : inLocal || inRemote;\n };\n const result: unknown[] = [];\n for (const value of [...local, ...remote]) {\n if (kept(value) && !has(result, value)) result.push(value);\n }\n return result;\n};\n\nconst entriesOf = (list: unknown[], key: string): Map<string, unknown> | null => {\n const entries = new Map<string, unknown>();\n for (const item of list) {\n if (!isObject(item) || typeof item[key] !== 'string') return null;\n entries.set(item[key], item);\n }\n return entries;\n};\n\nconst mergeEntries = (\n ids: string[],\n pick: (id: string) => [unknown, unknown, unknown],\n schemaOf: (id: string) => Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): Map<string, unknown> => {\n const merged = new Map<string, unknown>();\n for (const id of ids) {\n const [base, local, remote] = pick(id);\n const value = mergeNode(base, local, remote, schemaOf(id), [...path, id], context, deleteWins);\n if (value !== undefined) merged.set(id, value);\n }\n return merged;\n};\n\nconst orderedIds = (local: Iterable<string>, remote: Iterable<string>, base: Iterable<string>) => [\n ...new Set([...local, ...remote, ...base]),\n];\n\nconst mergeNode = (\n base: unknown,\n local: unknown,\n remote: unknown,\n schema: Schema,\n path: Path,\n context: Context,\n deleteWins: boolean,\n): unknown => {\n if (schema.type === 'local') return local;\n if (equal(local, remote)) return local;\n if (equal(local, base)) return remote;\n if (equal(remote, base)) return local;\n\n if (local === undefined || remote === undefined) {\n if (deleteWins) return undefined;\n const taken: Side = local === undefined ? 'remote' : 'local';\n context.conflicts.push({ path, kind: 'delete-edit', base, local, remote, taken });\n return taken === 'local' ? local : remote;\n }\n\n switch (schema.type) {\n case 'max': {\n const l = schema.order(local);\n const r = schema.order(remote);\n if (l === r) return record(base, local, remote, path, context);\n return l > r ? local : remote;\n }\n case 'join':\n if (Array.isArray(local) && Array.isArray(remote)) {\n return joinLists(Array.isArray(base) ? base : [], local, remote);\n }\n return record(base, local, remote, path, context);\n case 'object': {\n if (!isObject(local) || !isObject(remote)) return record(base, local, remote, path, context);\n const baseObject = isObject(base) ? base : {};\n const ids = orderedIds(Object.keys(local), Object.keys(remote), Object.keys(baseObject));\n const merged = mergeEntries(\n ids,\n (id) => [own(baseObject, id), own(local, id), own(remote, id)],\n (id) => own(schema.fields, id) ?? schema.each ?? { type: 'record' },\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return Object.fromEntries(merged);\n }\n case 'keyed': {\n const localEntries = Array.isArray(local) ? entriesOf(local, schema.key) : null;\n const remoteEntries = Array.isArray(remote) ? entriesOf(remote, schema.key) : null;\n const baseEntries = (Array.isArray(base) ? entriesOf(base, schema.key) : null) ?? new Map();\n if (!localEntries || !remoteEntries) return record(base, local, remote, path, context);\n const ids = orderedIds(localEntries.keys(), remoteEntries.keys(), baseEntries.keys());\n const merged = mergeEntries(\n ids,\n (id) => [baseEntries.get(id), localEntries.get(id), remoteEntries.get(id)],\n () => schema.item,\n path,\n context,\n deleteWins || schema.deleteWins === true,\n );\n return [...merged.values()];\n }\n case 'record':\n return record(base, local, remote, path, context);\n }\n};\n\n/**\n * Merges the data of this device and the data of the cloud against the data of the last sync.\n * A part that only one side changed takes that change. A part that both sides changed merges by its\n * schema, and a part that cannot merge is reported in `conflicts` with the preferred side already\n * in `merged`.\n */\nexport const merge = ({\n base,\n local,\n remote,\n schema,\n prefer = 'remote',\n}: MergeInput): MergeResult => {\n const context: Context = { prefer, conflicts: [] };\n const merged = mergeNode(base, local, remote, schema, [], context, false);\n return {\n merged,\n cloud: restoreCloudValues(merged, remote, schema),\n conflicts: context.conflicts,\n };\n};\n","import { isObject } from './equal';\nimport { Conflict, Schema, Side } from './types';\n\nconst own = (value: Record<string, unknown>, key: string): unknown =>\n Object.hasOwn(value, key) ? value[key] : undefined;\n\nconst childSchema = (schema: Schema, step: string): Schema => {\n if (schema.type === 'object') {\n const named =\n schema.fields && Object.hasOwn(schema.fields, step) ? schema.fields[step] : undefined;\n return named ?? schema.each ?? { type: 'record' };\n }\n return { type: 'record' };\n};\n\nconst setAt = (data: unknown, schema: Schema, path: string[], value: unknown): unknown => {\n if (path.length === 0) return value;\n const [step, ...rest] = path;\n if (schema.type === 'keyed') {\n const list = Array.isArray(data) ? data : [];\n const index = list.findIndex((item) => isObject(item) && item[schema.key] === step);\n const next = setAt(index >= 0 ? list[index] : undefined, schema.item, rest, value);\n if (index < 0) return next === undefined ? list : [...list, next];\n if (next === undefined) return list.filter((_, i) => i !== index);\n return list.map((item, i) => (i === index ? next : item));\n }\n const object = isObject(data) ? data : {};\n const next = setAt(own(object, step), childSchema(schema, step), rest, value);\n if (next === undefined) {\n return Object.fromEntries(Object.entries(object).filter(([key]) => key !== step));\n }\n return { ...object, [step]: next };\n};\n\n/**\n * Puts the value of one side of a conflict into the merged data. The player uses this to pick the\n * side that the merge did not take. A side that holds no value removes the entry.\n */\nexport const resolveConflict = (\n merged: unknown,\n schema: Schema,\n conflict: Conflict,\n side: Side,\n): unknown => setAt(merged, schema, conflict.path, conflict[side]);\n","const CHUNK = 0x8000;\n\n/** Encodes text as base64 from its UTF-8 bytes. It works in the browser and in Node. */\nexport const encodeBase64 = (text: string): string => {\n const bytes = new TextEncoder().encode(text);\n let binary = '';\n for (let i = 0; i < bytes.length; i += CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));\n }\n return btoa(binary);\n};\n\n/** Decodes base64 to text. Line breaks in the base64 text are ignored. */\nexport const decodeBase64 = (base64: string): string => {\n const binary = atob(base64.replace(/\\s/g, ''));\n return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));\n};\n","const MAX_DETAIL_LENGTH = 300;\n\n/** Reads the `message` that GitHub sends with a failed request, for the logs and the player. */\nexport const detailOf = async (response: Response): Promise<string | undefined> => {\n const body: unknown = await response.json().catch(() => null);\n if (typeof body !== 'object' || body === null) return undefined;\n const message = (body as { message?: unknown }).message;\n return typeof message === 'string' ? message.slice(0, MAX_DETAIL_LENGTH) : undefined;\n};\n","/** A file in the cloud. The version changes with every write and settles which write is newer. */\nexport interface RemoteFile {\n content: string;\n version: string;\n}\n\n/**\n * Where the files of an app are kept. A provider reads, writes and removes whole files by path, and\n * refuses a write that was made from an old version of the file. That refusal is what keeps two\n * devices from overwriting each other.\n */\nexport interface SyncProvider {\n /** Returns null when the file does not exist. */\n read(path: string): Promise<RemoteFile | null>;\n /**\n * Writes the file. The expected version is the version that was read, or null to create a file\n * that must not exist yet. Throws `VersionConflictError` when the file changed since.\n */\n write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }>;\n /** Removes the file. Throws `VersionConflictError` when the file changed since. */\n remove(path: string, expectedVersion: string): Promise<void>;\n}\n\n/** The file changed in the cloud after it was read. Read it again and merge. */\nexport class VersionConflictError extends Error {\n constructor(public readonly path: string) {\n super(`The file ${path} changed in the cloud.`);\n this.name = 'VersionConflictError';\n }\n}\n\nexport type ProviderErrorKind =\n /** The token is missing, expired, revoked or has no permission. The player signs in again. */\n | 'auth'\n /** The provider limits the calls for now. Try again after `retryAfterSeconds`. */\n | 'rate-limit'\n /** The repository or folder does not exist, or the token cannot see it. */\n | 'not-found'\n /** The device is offline or the provider cannot be reached. */\n | 'network'\n /** The provider failed. A later try may work. */\n | 'server'\n /** The provider answered with something this package does not understand. */\n | 'invalid';\n\nexport class ProviderError extends Error {\n constructor(\n public readonly kind: ProviderErrorKind,\n message: string,\n public readonly status?: number,\n public readonly retryAfterSeconds?: number,\n /** The message that the provider sent with the failed request, when it sent one. */\n public readonly detail?: string,\n ) {\n super(message);\n this.name = 'ProviderError';\n }\n}\n","import { decodeBase64, encodeBase64 } from './base64';\nimport { detailOf } from './detail';\nimport { ProviderError, RemoteFile, SyncProvider, VersionConflictError } from './types';\n\nconst API = 'https://api.github.com';\nconst API_VERSION = '2022-11-28';\n\nexport interface GitHubOptions {\n /** The owner and the name of the repository, such as `chiefpansancolt/gamerdex-data`. */\n repository: string;\n /** Returns the token. It may refresh the token first, so it is called for every request. */\n getToken: () => Promise<string>;\n /** The branch to use. Without it GitHub uses the default branch of the repository. */\n branch?: string;\n /** The message of the commit that a write or a removal makes. */\n commitMessage?: (path: string) => string;\n fetch?: typeof fetch;\n}\n\nexport interface RepositoryCheck {\n private: boolean;\n canWrite: boolean;\n defaultBranch: string;\n}\n\ninterface ContentsResponse {\n sha?: unknown;\n content?: unknown;\n encoding?: unknown;\n}\n\nconst encodePath = (path: string): string => path.split('/').map(encodeURIComponent).join('/');\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst retryAfterOf = (response: Response): number | undefined => {\n const header = response.headers.get('retry-after');\n if (header !== null && Number.isFinite(Number(header))) return Number(header);\n const reset = response.headers.get('x-ratelimit-reset');\n if (reset !== null && Number.isFinite(Number(reset))) {\n return Math.max(0, Number(reset) - Math.floor(Date.now() / 1000));\n }\n return undefined;\n};\n\n/** Keeps the files of an app in a private GitHub repository, through the contents API. */\nexport class GitHubProvider implements SyncProvider {\n private readonly send: typeof fetch;\n\n constructor(private readonly options: GitHubOptions) {\n this.send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n }\n\n async read(path: string): Promise<RemoteFile | null> {\n const response = await this.request('GET', this.contentsUrl(path));\n if (response.status === 404) return null;\n await this.assertOk(response);\n const body = (await response.json()) as ContentsResponse;\n if (typeof body.sha !== 'string') throw this.invalid('The file has no version.');\n if (body.encoding === 'base64' && typeof body.content === 'string') {\n return { content: decodeBase64(body.content), version: body.sha };\n }\n if (body.encoding === 'none') {\n const raw = await this.request('GET', this.contentsUrl(path), undefined, {\n Accept: 'application/vnd.github.raw+json',\n });\n await this.assertOk(raw);\n return { content: await raw.text(), version: body.sha };\n }\n throw this.invalid('The file content is not in a known encoding.');\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const body: Record<string, unknown> = {\n message: this.message(path),\n content: encodeBase64(content),\n };\n if (expectedVersion !== null) body.sha = expectedVersion;\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('PUT', this.contentsUrl(path), body);\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n const result = (await response.json()) as { content?: { sha?: unknown } };\n const version = result.content?.sha;\n if (typeof version !== 'string') throw this.invalid('The write returned no version.');\n return { version };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const body: Record<string, unknown> = { message: this.message(path), sha: expectedVersion };\n if (this.options.branch) body.branch = this.options.branch;\n const response = await this.request('DELETE', this.contentsUrl(path), body);\n if (response.status === 404) return;\n if (response.status === 409 || response.status === 422) throw new VersionConflictError(path);\n await this.assertOk(response);\n }\n\n /**\n * Checks that the repository can be used: it exists, the token can write to it, and it is\n * private. The saved data of a player must never sit in a public repository.\n */\n async checkRepository(): Promise<RepositoryCheck> {\n const response = await this.request('GET', `${API}/repos/${this.options.repository}`);\n await this.assertOk(response);\n const body: unknown = await response.json();\n if (!isRecord(body) || typeof body.private !== 'boolean') {\n throw this.invalid('The repository answer is not understood.');\n }\n const permissions = isRecord(body.permissions) ? body.permissions : {};\n return {\n private: body.private,\n canWrite: permissions.push === true,\n defaultBranch: typeof body.default_branch === 'string' ? body.default_branch : 'main',\n };\n }\n\n private contentsUrl(path: string): string {\n const base = `${API}/repos/${this.options.repository}/contents/${encodePath(path)}`;\n return this.options.branch ? `${base}?ref=${encodeURIComponent(this.options.branch)}` : base;\n }\n\n private message(path: string): string {\n return this.options.commitMessage?.(path) ?? `Gamerdex sync: ${path}`;\n }\n\n private async request(\n method: string,\n url: string,\n body?: Record<string, unknown>,\n headers: Record<string, string> = {},\n ): Promise<Response> {\n const token = await this.options.getToken();\n try {\n return await this.send(url, {\n method,\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': API_VERSION,\n ...(body ? { 'Content-Type': 'application/json' } : {}),\n ...headers,\n },\n body: body ? JSON.stringify(body) : undefined,\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n }\n\n private async assertOk(response: Response): Promise<void> {\n if (response.ok) return;\n const { status } = response;\n const detail = await detailOf(response);\n if (status === 401) {\n throw new ProviderError('auth', 'The token is not valid.', status, undefined, detail);\n }\n if (status === 403 || status === 429) {\n const limited = status === 429 || response.headers.get('x-ratelimit-remaining') === '0';\n if (limited) {\n throw new ProviderError(\n 'rate-limit',\n 'GitHub limits the calls.',\n status,\n retryAfterOf(response),\n detail,\n );\n }\n throw new ProviderError(\n 'auth',\n 'The token has no permission for the repository.',\n status,\n undefined,\n detail,\n );\n }\n if (status === 404) {\n throw new ProviderError(\n 'not-found',\n 'The repository was not found.',\n status,\n undefined,\n detail,\n );\n }\n if (status >= 500) {\n throw new ProviderError('server', 'GitHub failed.', status, undefined, detail);\n }\n throw this.invalid(`GitHub answered ${status}.`, status, detail);\n }\n\n private invalid(message: string, status?: number, detail?: string): ProviderError {\n return new ProviderError('invalid', message, status, undefined, detail);\n }\n}\n","import { RemoteFile, SyncProvider, VersionConflictError } from './types';\n\n/**\n * A provider that keeps the files in memory. It follows the same version rules as a real provider,\n * so the sync flow of an app can be tested without a network.\n */\nexport class MemoryProvider implements SyncProvider {\n private files = new Map<string, { content: string; counter: number }>();\n private counter = 0;\n\n async read(path: string): Promise<RemoteFile | null> {\n const file = this.files.get(path);\n return file ? { content: file.content, version: String(file.counter) } : null;\n }\n\n async write(\n path: string,\n content: string,\n expectedVersion: string | null,\n ): Promise<{ version: string }> {\n const current = this.files.get(path);\n const currentVersion = current ? String(current.counter) : null;\n if (currentVersion !== expectedVersion) throw new VersionConflictError(path);\n this.counter += 1;\n this.files.set(path, { content, counter: this.counter });\n return { version: String(this.counter) };\n }\n\n async remove(path: string, expectedVersion: string): Promise<void> {\n const current = this.files.get(path);\n if (current && String(current.counter) !== expectedVersion) {\n throw new VersionConflictError(path);\n }\n this.files.delete(path);\n }\n\n /** The paths of the files that exist, in the order they were first written. */\n paths(): string[] {\n return [...this.files.keys()];\n }\n}\n","import { dataPath, metaPath } from '../layout';\nimport { restoreCloudValues } from '../merge/cloud';\nimport { equal } from '../merge/equal';\nimport { merge } from '../merge/merge';\nimport { Conflict, Schema } from '../merge/types';\nimport { emptyMeta, parseMeta, SyncMeta, withDevice } from '../meta';\nimport { SyncProvider, VersionConflictError } from '../provider/types';\n\nconst MAX_ATTEMPTS = 4;\n\n/**\n * What this device remembers about the last sync. The app keeps it in the browser, next to the\n * saved data but outside it, and passes it back on the next sync.\n */\nexport interface SyncState {\n /** The cloud data as this device last saw it. */\n base: unknown;\n dataVersion: string | null;\n metaVersion: string | null;\n}\n\n/** What to do when this device never synced and both the device and the cloud hold data. */\nexport type FirstSyncChoice = 'merge' | 'keep-local' | 'keep-cloud';\n\nexport interface SyncOptions {\n appId: string;\n deviceId: string;\n deviceName: string;\n provider: SyncProvider;\n schema: Schema;\n /** The data of this device. */\n local: unknown;\n /** The state kept after the last sync, or null when this device never synced. */\n state: SyncState | null;\n firstSync?: FirstSyncChoice;\n /** Returns true when the data is whole and safe to store. Called before any write. */\n validate?: (data: unknown) => boolean;\n /** Lets the app change the revisions of the meta file for this sync. */\n updateMeta?: (meta: SyncMeta) => SyncMeta;\n /** The current time as an ISO text. */\n now: () => string;\n}\n\nexport type SyncResult =\n | {\n status: 'synced';\n /** The data to store on this device. */\n data: unknown;\n /** The state to keep for the next sync. */\n state: SyncState;\n conflicts: Conflict[];\n /** True when the cloud changed during this sync. */\n uploaded: boolean;\n }\n | {\n /** The device and the cloud both hold data and the app has not said what to do. */\n status: 'needs-choice';\n local: unknown;\n remote: unknown;\n };\n\n/** The data in the cloud is not whole, so nothing was written. */\nexport class RemoteDataError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RemoteDataError';\n }\n}\n\n/** Other devices kept writing during every try, so the sync stopped. Try again later. */\nexport class SyncBusyError extends Error {\n constructor() {\n super('The cloud kept changing during the sync.');\n this.name = 'SyncBusyError';\n }\n}\n\nconst parseData = (text: string, validate: SyncOptions['validate']): unknown => {\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch {\n throw new RemoteDataError('The synced data is not valid JSON.');\n }\n if (validate && !validate(data)) throw new RemoteDataError('The synced data is not valid.');\n return data;\n};\n\nconst readMeta = (text: string | undefined, appId: string): SyncMeta => {\n if (text === undefined) return emptyMeta(appId);\n const parsed = parseMeta(text, appId);\n if (!parsed.ok) throw new RemoteDataError(parsed.error);\n return parsed.meta;\n};\n\n/**\n * Records this device and the time of the sync in the meta file. A sync that changed nothing writes\n * no file when this device is already recorded, so the repository gets no empty commits.\n */\nconst writeMeta = async (options: SyncOptions, uploaded: boolean): Promise<string> => {\n const { provider, appId, deviceId, deviceName, updateMeta, now } = options;\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const current = await provider.read(metaPath(appId));\n const meta = readMeta(current?.content, appId);\n if (current && !uploaded && !updateMeta && Object.hasOwn(meta.devices, deviceId)) {\n return current.version;\n }\n const time = now();\n const updated = withDevice(updateMeta ? updateMeta(meta) : meta, deviceId, {\n name: deviceName,\n lastSeenAt: time,\n });\n try {\n const written = await provider.write(\n metaPath(appId),\n JSON.stringify({ ...updated, lastSyncAt: time }, null, 2),\n current?.version ?? null,\n );\n return written.version;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n throw new SyncBusyError();\n};\n\n/**\n * Brings this device and the cloud to the same data. It reads the cloud, merges the three copies,\n * writes the cloud copy when it changed, and returns the data to store on this device with the\n * state to keep. It never changes the device by itself, and it does not write when the cloud data\n * or the merged data fail `validate`.\n *\n * Throws `ProviderError` when the provider fails, `RemoteDataError` when the cloud data is not\n * whole, and `SyncBusyError` when other devices keep writing.\n */\nexport const sync = async (options: SyncOptions): Promise<SyncResult> => {\n const { appId, provider, schema, local, state, firstSync, validate } = options;\n const path = dataPath(appId);\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n const file = await provider.read(path);\n\n if (file === null) {\n if (validate && !validate(local))\n throw new RemoteDataError('The data of this device is not valid.');\n const cloud = restoreCloudValues(local, undefined, schema);\n const created = await tryWrite(provider, path, cloud, null);\n if (created === null) continue;\n const metaVersion = await writeMeta(options, true);\n return done(local, cloud, created, metaVersion, [], true);\n }\n\n const remote = parseData(file.content, validate);\n const differs = state === null && !equal(restoreCloudValues(local, remote, schema), remote);\n const choice = differs ? firstSync : undefined;\n if (differs && choice === undefined) return { status: 'needs-choice', local, remote };\n\n const base = state === null ? undefined : state.base;\n const merged = mergeFor(choice, { base, local, remote, schema });\n if (validate && !validate(merged.merged)) {\n throw new RemoteDataError('The merged data is not valid.');\n }\n\n if (equal(merged.cloud, remote)) {\n const metaVersion = await writeMeta(options, false);\n return done(merged.merged, merged.cloud, file.version, metaVersion, merged.conflicts, false);\n }\n\n const version = await tryWrite(provider, path, merged.cloud, file.version);\n if (version === null) continue;\n const metaVersion = await writeMeta(options, true);\n return done(merged.merged, merged.cloud, version, metaVersion, merged.conflicts, true);\n }\n throw new SyncBusyError();\n};\n\nconst mergeFor = (\n choice: FirstSyncChoice | undefined,\n input: { base: unknown; local: unknown; remote: unknown; schema: Schema },\n) => {\n if (choice === 'keep-local') {\n const cloud = restoreCloudValues(input.local, input.remote, input.schema);\n return { merged: input.local, cloud, conflicts: [] };\n }\n if (choice === 'keep-cloud') {\n const merged = restoreCloudValues(input.remote, input.local, input.schema);\n return { merged, cloud: input.remote, conflicts: [] };\n }\n return merge(input);\n};\n\nconst tryWrite = async (\n provider: SyncProvider,\n path: string,\n data: unknown,\n version: string | null,\n): Promise<string | null> => {\n try {\n return (await provider.write(path, JSON.stringify(data, null, 2), version)).version;\n } catch (error) {\n if (error instanceof VersionConflictError) return null;\n throw error;\n }\n};\n\nconst done = (\n data: unknown,\n cloud: unknown,\n dataVersion: string,\n metaVersion: string,\n conflicts: Conflict[],\n uploaded: boolean,\n): SyncResult => ({\n status: 'synced',\n data,\n state: { base: cloud, dataVersion, metaVersion },\n conflicts,\n uploaded,\n});\n","/** The public values of the Gamerdex Sync GitHub App. The client secret is never kept here. */\nexport const GITHUB_APP = {\n slug: 'gamerdex-sync',\n clientId: 'Iv23liwzjj3x8O2K5ALA',\n} as const;\n","/** The tokens of a signed in player. Times are milliseconds since 1970. */\nexport interface StoredTokens {\n accessToken: string;\n accessExpiresAt: number;\n refreshToken: string;\n refreshExpiresAt: number;\n}\n\n/** The answer of the token function, with lifetimes in seconds. */\nexport interface TokenResponse {\n accessToken: string;\n expiresIn: number;\n refreshToken: string;\n refreshExpiresIn: number;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isSeconds = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0;\n\nexport const parseTokenResponse = (body: unknown): TokenResponse | null => {\n if (!isRecord(body)) return null;\n const { accessToken, expiresIn, refreshToken, refreshExpiresIn } = body;\n if (typeof accessToken !== 'string' || accessToken === '') return null;\n if (typeof refreshToken !== 'string' || refreshToken === '') return null;\n if (!isSeconds(expiresIn) || !isSeconds(refreshExpiresIn)) return null;\n return { accessToken, expiresIn, refreshToken, refreshExpiresIn };\n};\n\nexport const toStoredTokens = (response: TokenResponse, nowMs: number): StoredTokens => ({\n accessToken: response.accessToken,\n accessExpiresAt: nowMs + response.expiresIn * 1000,\n refreshToken: response.refreshToken,\n refreshExpiresAt: nowMs + response.refreshExpiresIn * 1000,\n});\n","import { ProviderError } from '../provider/types';\nimport { parseTokenResponse, StoredTokens, toStoredTokens } from './tokens';\n\nconst RENEW_BEFORE_MS = 60_000;\n\nexport interface TokenSourceOptions {\n /** The address of the token function, without a trailing slash. */\n endpoint: string;\n /** Reads the tokens that this device saved, or null when the player is signed out. */\n load: () => StoredTokens | null;\n /** Saves the tokens, or removes them when null. */\n save: (tokens: StoredTokens | null) => void;\n now: () => number;\n fetch?: typeof fetch;\n}\n\n/**\n * Keeps a player signed in. `getToken` returns a token that is valid for at least a minute and\n * renews it first when needed. It is meant to be the `getToken` of `GitHubProvider`.\n */\nexport const createTokenSource = (options: TokenSourceOptions) => {\n const send = options.fetch ?? ((...args) => globalThis.fetch(...args));\n let renewing: Promise<StoredTokens> | null = null;\n\n const post = async (path: string, body: Record<string, string>): Promise<StoredTokens> => {\n let response: Response;\n try {\n response = await send(`${options.endpoint}/${path}`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n } catch {\n throw new ProviderError('network', 'The sign in service could not be reached.');\n }\n if (response.status >= 500) throw new ProviderError('server', 'The sign in service failed.');\n const parsed = response.ok ? parseTokenResponse(await response.json().catch(() => null)) : null;\n if (parsed === null)\n throw new ProviderError('auth', 'The sign in was refused.', response.status);\n const tokens = toStoredTokens(parsed, options.now());\n options.save(tokens);\n return tokens;\n };\n\n const renew = (tokens: StoredTokens): Promise<StoredTokens> => {\n if (tokens.refreshExpiresAt <= options.now()) {\n options.save(null);\n throw new ProviderError('auth', 'The sign in expired.');\n }\n renewing ??= post('refresh', { refreshToken: tokens.refreshToken })\n .catch((error: unknown) => {\n if (error instanceof ProviderError && error.kind === 'auth') options.save(null);\n throw error;\n })\n .finally(() => {\n renewing = null;\n });\n return renewing;\n };\n\n return {\n /** Finishes a sign in with the code from the callback page. */\n signIn: (code: string, redirectUri: string): Promise<StoredTokens> =>\n post('token', { code, redirectUri }),\n signOut: (): void => options.save(null),\n isSignedIn: (): boolean => options.load() !== null,\n getToken: async (): Promise<string> => {\n const tokens = options.load();\n if (tokens === null) throw new ProviderError('auth', 'The player is not signed in.');\n if (tokens.accessExpiresAt - RENEW_BEFORE_MS > options.now()) return tokens.accessToken;\n return (await renew(tokens)).accessToken;\n },\n };\n};\n","import { GITHUB_APP } from './app';\n\n/** A random value that ties a sign-in answer to the sign-in that this device started. */\nexport const newState = (): string => crypto.randomUUID();\n\nexport const authorizeUrl = (redirectUri: string, state: string): string => {\n const query = new URLSearchParams({\n client_id: GITHUB_APP.clientId,\n redirect_uri: redirectUri,\n state,\n });\n return `https://github.com/login/oauth/authorize?${query}`;\n};\n\n/** The page where the player picks the repository that the app may use. */\nexport const installUrl = (state: string): string =>\n `https://github.com/apps/${GITHUB_APP.slug}/installations/new?${new URLSearchParams({ state })}`;\n\nexport type CallbackResult =\n { ok: true; code: string } | { ok: false; reason: 'denied' | 'state' | 'missing' };\n\n/**\n * Reads the query of the sign-in callback page. The state must equal the state that was saved when\n * the sign-in started, or the answer is refused.\n */\nexport const readCallback = (search: string, expectedState: string | null): CallbackResult => {\n const query = new URLSearchParams(search);\n if (query.get('error') !== null) return { ok: false, reason: 'denied' };\n const code = query.get('code');\n if (code === null || code === '') return { ok: false, reason: 'missing' };\n if (expectedState === null || query.get('state') !== expectedState) {\n return { ok: false, reason: 'state' };\n }\n return { ok: true, code };\n};\n","import { detailOf } from './detail';\nimport { ProviderError } from './types';\n\nexport interface InstalledRepository {\n /** The owner and the name, such as `chiefpansancolt/gamerdex-data`. */\n fullName: string;\n private: boolean;\n}\n\nconst API = 'https://api.github.com';\nconst PAGE_SIZE = 100;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst getJson = async (\n send: typeof fetch,\n token: string,\n url: string,\n): Promise<Record<string, unknown>> => {\n let response: Response;\n try {\n response = await send(url, {\n headers: {\n Accept: 'application/vnd.github+json',\n Authorization: `Bearer ${token}`,\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n });\n } catch {\n throw new ProviderError('network', 'GitHub could not be reached.');\n }\n if (!response.ok) {\n const { status } = response;\n const detail = await detailOf(response);\n if (status === 401 || status === 403) {\n throw new ProviderError('auth', 'The token is not valid.', status, undefined, detail);\n }\n if (status >= 500)\n throw new ProviderError('server', 'GitHub failed.', status, undefined, detail);\n throw new ProviderError(\n 'invalid',\n 'GitHub answer is not understood.',\n status,\n undefined,\n detail,\n );\n }\n const body: unknown = await response.json().catch(() => null);\n if (!isRecord(body)) {\n throw new ProviderError('invalid', 'GitHub answer is not understood.', response.status);\n }\n return body;\n};\n\n/**\n * Lists the repositories that the player gave to the GitHub App, for the first 100 installations\n * and the first 100 repositories of each. The player picks the repository for the synced data.\n */\nexport const listInstalledRepositories = async (\n token: string,\n send: typeof fetch = (...args) => globalThis.fetch(...args),\n): Promise<InstalledRepository[]> => {\n const installations = await getJson(\n send,\n token,\n `${API}/user/installations?per_page=${PAGE_SIZE}`,\n );\n const ids = (Array.isArray(installations.installations) ? installations.installations : [])\n .filter(isRecord)\n .map((installation) => installation.id)\n .filter((id): id is number => typeof id === 'number');\n\n const repositories: InstalledRepository[] = [];\n for (const id of ids) {\n const page = await getJson(\n send,\n token,\n `${API}/user/installations/${id}/repositories?per_page=${PAGE_SIZE}`,\n );\n for (const item of Array.isArray(page.repositories) ? page.repositories : []) {\n if (\n isRecord(item) &&\n typeof item.full_name === 'string' &&\n typeof item.private === 'boolean'\n ) {\n repositories.push({ fullName: item.full_name, private: item.private });\n }\n }\n }\n return repositories;\n};\n"],"mappings":"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","names":["own","own","childSchema","next","isRecord","metaVersion","isRecord","API","isRecord"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamerdexapp/gamerdex-sync",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Shared cloud sync for the Gamerdex apps: provider connections, file layout, revisions and a three way merge of the saved data.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|