@mapletip/data 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +52 -0
- package/README.md +209 -0
- package/client/contract-compatibility.d.mts +27 -0
- package/client/contract-compatibility.mjs +159 -0
- package/client/core.d.ts +150 -0
- package/client/core.js +359 -0
- package/client/equipment.d.ts +22 -0
- package/client/equipment.js +63 -0
- package/client/items.d.ts +22 -0
- package/client/items.js +63 -0
- package/client/media.d.ts +19 -0
- package/client/media.js +47 -0
- package/client/monster-client.d.ts +35 -0
- package/client/monster-client.js +155 -0
- package/client/monsters.d.ts +24 -0
- package/client/monsters.js +14 -0
- package/client/quests.d.ts +31 -0
- package/client/quests.js +124 -0
- package/client/release.d.ts +63 -0
- package/client/release.js +279 -0
- package/client/static-client.d.ts +40 -0
- package/client/static-client.js +44 -0
- package/client/static-domains.d.ts +48 -0
- package/client/static-domains.js +113 -0
- package/generated/contract-2-catalog-fact.d.ts +51 -0
- package/generated/equipment-catalog-2.d.ts +434 -0
- package/generated/equipment-catalog-2.validator.js +17 -0
- package/generated/equipment-detail-2.d.ts +350 -0
- package/generated/equipment-detail-2.validator.js +17 -0
- package/generated/equipment-evidence-pack-2.d.ts +243 -0
- package/generated/equipment-evidence-pack-2.validator.js +17 -0
- package/generated/equipment-pack-2.validator.js +17 -0
- package/generated/equipment-relationship-pack-2.d.ts +170 -0
- package/generated/equipment-relationship-pack-2.validator.js +17 -0
- package/generated/item-catalog-2.d.ts +366 -0
- package/generated/item-catalog-2.validator.js +17 -0
- package/generated/item-detail-2.d.ts +540 -0
- package/generated/item-detail-2.validator.js +17 -0
- package/generated/item-evidence-pack-2.d.ts +243 -0
- package/generated/item-evidence-pack-2.validator.js +17 -0
- package/generated/item-pack-2.validator.js +17 -0
- package/generated/item-relationship-pack-2.d.ts +170 -0
- package/generated/item-relationship-pack-2.validator.js +17 -0
- package/generated/media-catalog-2.d.ts +133 -0
- package/generated/media-catalog-2.validator.js +17 -0
- package/generated/media-detail-2.d.ts +125 -0
- package/generated/media-detail-2.validator.js +17 -0
- package/generated/media-pack-2.validator.js +17 -0
- package/generated/monster-catalog-2.d.ts +230 -0
- package/generated/monster-catalog-2.validator.js +17 -0
- package/generated/monster-catalog.d.ts +224 -0
- package/generated/monster-catalog.validator.js +1 -0
- package/generated/monster-core-relationship-pack-2.d.ts +208 -0
- package/generated/monster-core-relationship-pack-2.validator.js +17 -0
- package/generated/monster-detail-2.d.ts +372 -0
- package/generated/monster-detail-2.validator.js +17 -0
- package/generated/monster-detail.d.ts +799 -0
- package/generated/monster-detail.validator.js +17 -0
- package/generated/monster-evidence-pack-2.d.ts +378 -0
- package/generated/monster-evidence-pack-2.validator.js +17 -0
- package/generated/monster-evidence-pack.d.ts +269 -0
- package/generated/monster-evidence-pack.validator.js +17 -0
- package/generated/monster-location-pack-2.validator.js +1 -0
- package/generated/monster-pack-2.validator.js +17 -0
- package/generated/monster-pack.validator.js +17 -0
- package/generated/monster-relationship-pack.d.ts +126 -0
- package/generated/monster-relationship-pack.validator.js +1 -0
- package/generated/product-manifest.d.ts +188 -0
- package/generated/product-manifest.validator.js +1 -0
- package/generated/quest-auxiliary-pack-2.d.ts +391 -0
- package/generated/quest-auxiliary-pack-2.validator.js +17 -0
- package/generated/quest-browse-pack-2.d.ts +353 -0
- package/generated/quest-browse-pack-2.validator.js +17 -0
- package/generated/quest-catalog-2.d.ts +501 -0
- package/generated/quest-catalog-2.validator.js +17 -0
- package/generated/quest-evidence-pack-2.d.ts +269 -0
- package/generated/quest-evidence-pack-2.validator.js +17 -0
- package/generated/quest-pack-2.d.ts +410 -0
- package/generated/quest-pack-2.validator.js +17 -0
- package/generated/quest-relationship-pack-2.d.ts +868 -0
- package/generated/quest-relationship-pack-2.validator.js +17 -0
- package/generated/release-build-index.d.ts +42 -0
- package/generated/release-build-index.validator.js +1 -0
- package/generated/release-manifest.d.ts +24 -0
- package/generated/release-manifest.validator.js +1 -0
- package/generated/release-pin.d.ts +12 -0
- package/generated/release-pin.validator.js +1 -0
- package/package.json +155 -0
package/client/core.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import validateManifest from '../generated/product-manifest.validator.js';
|
|
2
|
+
import { assertCompatibleContractVersion, assertManifestCompatibility, minimumPackContractVersion, } from './contract-compatibility.mjs';
|
|
3
|
+
export class StaticClientError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class SecureContextRequiredError extends StaticClientError {
|
|
12
|
+
constructor() {
|
|
13
|
+
super('SECURE_CONTEXT_REQUIRED', 'Public data verification requires a secure context with WebCrypto SHA-256 (HTTPS or localhost)');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class IntegrityError extends StaticClientError {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super('INTEGRITY_ERROR', message);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class RequestError extends StaticClientError {
|
|
22
|
+
constructor(message, options) {
|
|
23
|
+
super('REQUEST_ERROR', message, options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class InvalidPathError extends StaticClientError {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super('INVALID_PATH', message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class InvalidJsonError extends StaticClientError {
|
|
32
|
+
constructor(path, options) {
|
|
33
|
+
super('INVALID_JSON', `Invalid JSON in ${path}`, options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class SchemaValidationError extends StaticClientError {
|
|
37
|
+
constructor(path, details) {
|
|
38
|
+
super('SCHEMA_VALIDATION_ERROR', `Invalid public data at ${path}: ${details}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export class ContractVersionError extends StaticClientError {
|
|
42
|
+
constructor(message, options) {
|
|
43
|
+
super('CONTRACT_VERSION_ERROR', message, options);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class UnsupportedCapabilityError extends StaticClientError {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super('UNSUPPORTED_CAPABILITY', message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class UnsupportedProductError extends StaticClientError {
|
|
52
|
+
constructor(productId) {
|
|
53
|
+
super('UNSUPPORTED_PRODUCT', `Unsupported public-data product: ${productId}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class UnknownBuildError extends StaticClientError {
|
|
57
|
+
constructor(buildId) {
|
|
58
|
+
super('UNKNOWN_BUILD', `Build is not present in the pinned release: ${buildId}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class ScopeError extends StaticClientError {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super('SCOPE_ERROR', message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class MissingRecordError extends StaticClientError {
|
|
67
|
+
constructor(key, domain = 'Monster') {
|
|
68
|
+
super('MISSING_RECORD', `${domain} record not found: ${key}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class MissingPageError extends StaticClientError {
|
|
72
|
+
constructor(key, kind) {
|
|
73
|
+
super('MISSING_PAGE', `No ${kind} page pack addresses ${key}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function normalizedBaseUrl(value) {
|
|
77
|
+
const url = new URL(value);
|
|
78
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
79
|
+
throw new InvalidPathError(`Product base URL must use HTTP or HTTPS: ${url.protocol}`);
|
|
80
|
+
}
|
|
81
|
+
if (!url.pathname.endsWith('/'))
|
|
82
|
+
url.pathname += '/';
|
|
83
|
+
url.search = '';
|
|
84
|
+
url.hash = '';
|
|
85
|
+
return url;
|
|
86
|
+
}
|
|
87
|
+
function resolveDescriptorUrl(baseUrl, path) {
|
|
88
|
+
if (!path || path.startsWith('/') || path.includes('\\')
|
|
89
|
+
|| path.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
90
|
+
throw new InvalidPathError(`Expected a normalized relative POSIX path, got: ${path}`);
|
|
91
|
+
}
|
|
92
|
+
const url = new URL(path, baseUrl);
|
|
93
|
+
if (url.origin !== baseUrl.origin || !url.pathname.startsWith(baseUrl.pathname)) {
|
|
94
|
+
throw new InvalidPathError(`Descriptor path escapes product base URL: ${path}`);
|
|
95
|
+
}
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
98
|
+
function validateDescriptor(descriptor) {
|
|
99
|
+
if (!/^[0-9a-f]{64}$/.test(descriptor.sha256)) {
|
|
100
|
+
throw new IntegrityError(`Descriptor has an invalid SHA-256 digest: ${descriptor.path}`);
|
|
101
|
+
}
|
|
102
|
+
if (!Number.isSafeInteger(descriptor.byteLength) || descriptor.byteLength < 0) {
|
|
103
|
+
throw new IntegrityError(`Descriptor has an invalid byte length: ${descriptor.path}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function sha256(bytes) {
|
|
107
|
+
if (globalThis.isSecureContext === false || !globalThis.crypto?.subtle) {
|
|
108
|
+
throw new SecureContextRequiredError();
|
|
109
|
+
}
|
|
110
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
111
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
112
|
+
}
|
|
113
|
+
export async function verifyBlob(bytes, descriptor) {
|
|
114
|
+
validateDescriptor(descriptor);
|
|
115
|
+
const actualSha256 = await sha256(bytes);
|
|
116
|
+
if (actualSha256 !== descriptor.sha256) {
|
|
117
|
+
throw new IntegrityError(`SHA-256 mismatch for ${descriptor.path}`);
|
|
118
|
+
}
|
|
119
|
+
if (bytes.byteLength !== descriptor.byteLength) {
|
|
120
|
+
throw new IntegrityError(`Byte length mismatch for ${descriptor.path}: expected ${descriptor.byteLength}, got ${bytes.byteLength}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function resolveMediaUrl(baseUrl, descriptor) {
|
|
124
|
+
const base = baseUrl instanceof URL ? baseUrl : normalizedBaseUrl(baseUrl);
|
|
125
|
+
return resolveDescriptorUrl(base, descriptor.path).toString();
|
|
126
|
+
}
|
|
127
|
+
function parseJson(bytes, path) {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof SyntaxError || error instanceof TypeError) {
|
|
133
|
+
throw new InvalidJsonError(path, { cause: error });
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function parseVerifiedJson(bytes, descriptor) {
|
|
139
|
+
const actualSha256 = await sha256(bytes);
|
|
140
|
+
if (actualSha256 !== descriptor.sha256) {
|
|
141
|
+
throw new IntegrityError(`SHA-256 mismatch for ${descriptor.path}`);
|
|
142
|
+
}
|
|
143
|
+
if (bytes.byteLength !== descriptor.byteLength) {
|
|
144
|
+
throw new IntegrityError(`Byte length mismatch for ${descriptor.path}: expected ${descriptor.byteLength}, got ${bytes.byteLength}`);
|
|
145
|
+
}
|
|
146
|
+
return parseJson(bytes, descriptor.path);
|
|
147
|
+
}
|
|
148
|
+
function errorMessage(error) {
|
|
149
|
+
return error instanceof Error ? error.message : String(error);
|
|
150
|
+
}
|
|
151
|
+
export function assertCompatibleManifest(manifest) {
|
|
152
|
+
try {
|
|
153
|
+
return assertManifestCompatibility(manifest);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const message = errorMessage(error);
|
|
157
|
+
if (message.toLowerCase().includes('capability')) {
|
|
158
|
+
throw new UnsupportedCapabilityError(message);
|
|
159
|
+
}
|
|
160
|
+
throw new ContractVersionError(message, { cause: error });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export function createJsonFetcher(options) {
|
|
164
|
+
return async function fetchJson(descriptor, kind, operation) {
|
|
165
|
+
validateDescriptor(descriptor);
|
|
166
|
+
const url = resolveDescriptorUrl(options.baseUrl, descriptor.path);
|
|
167
|
+
options.onRequest?.({ kind, path: descriptor.path, url: url.href, source: 'network' });
|
|
168
|
+
let response;
|
|
169
|
+
try {
|
|
170
|
+
const fetcher = options.fetcher;
|
|
171
|
+
response = await fetcher(url, {
|
|
172
|
+
redirect: 'error',
|
|
173
|
+
signal: operation?.signal ?? options.signal,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
178
|
+
throw error;
|
|
179
|
+
throw new RequestError(`Request failed for ${descriptor.path}`, { cause: error });
|
|
180
|
+
}
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new RequestError(`HTTP ${response.status} fetching ${descriptor.path}`);
|
|
183
|
+
}
|
|
184
|
+
let bytes;
|
|
185
|
+
try {
|
|
186
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw new RequestError(`Unable to read response bytes for ${descriptor.path}`, { cause: error });
|
|
190
|
+
}
|
|
191
|
+
return parseVerifiedJson(bytes, descriptor);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export async function parsePreloadedJson(value, descriptor, kind, baseUrl, onRequest) {
|
|
195
|
+
validateDescriptor(descriptor);
|
|
196
|
+
const url = resolveDescriptorUrl(baseUrl, descriptor.path);
|
|
197
|
+
onRequest?.({ kind, path: descriptor.path, url: url.href, source: 'preloaded' });
|
|
198
|
+
const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : value;
|
|
199
|
+
return parseVerifiedJson(bytes, descriptor);
|
|
200
|
+
}
|
|
201
|
+
export function assertEnvelopeVersion(version, minimumVersion, path) {
|
|
202
|
+
try {
|
|
203
|
+
assertCompatibleContractVersion(version, minimumVersion);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new ContractVersionError(`${path}: ${errorMessage(error)}`, { cause: error });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export function assertSameScope(actual, expected, path) {
|
|
210
|
+
if (actual.family_id !== expected.family_id || actual.build_id !== expected.build_id) {
|
|
211
|
+
throw new ScopeError(`${path} scope ${actual.family_id}/${actual.build_id} does not match expected `
|
|
212
|
+
+ `${expected.family_id}/${expected.build_id}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export function rangeDescriptor(descriptors, key, kind) {
|
|
216
|
+
const matches = descriptors.filter((descriptor) => key >= descriptor.firstKey && key <= descriptor.lastKey);
|
|
217
|
+
if (matches.length !== 1)
|
|
218
|
+
throw new MissingPageError(key, kind);
|
|
219
|
+
return matches[0];
|
|
220
|
+
}
|
|
221
|
+
export function recordRangeDescriptor(descriptors, key, kind, domain = 'Monster') {
|
|
222
|
+
const matches = descriptors.filter((descriptor) => key >= descriptor.firstKey && key <= descriptor.lastKey);
|
|
223
|
+
if (matches.length === 0)
|
|
224
|
+
throw new MissingRecordError(key, domain);
|
|
225
|
+
if (matches.length > 1)
|
|
226
|
+
throw new MissingPageError(key, kind);
|
|
227
|
+
return matches[0];
|
|
228
|
+
}
|
|
229
|
+
export function memoized(cache, key, load) {
|
|
230
|
+
const existing = cache.get(key);
|
|
231
|
+
if (existing)
|
|
232
|
+
return existing;
|
|
233
|
+
const pending = load();
|
|
234
|
+
cache.set(key, pending);
|
|
235
|
+
void pending.catch(() => {
|
|
236
|
+
if (cache.get(key) === pending)
|
|
237
|
+
cache.delete(key);
|
|
238
|
+
});
|
|
239
|
+
return pending;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Wrap a static `import()` of one generated validator in a memoized loader.
|
|
243
|
+
* The import specifier stays literal so bundlers keep the validator in its own
|
|
244
|
+
* on-demand chunk; a rejected load is evicted so the next call retries.
|
|
245
|
+
*/
|
|
246
|
+
export function lazyValidator(load) {
|
|
247
|
+
let promise;
|
|
248
|
+
return () => {
|
|
249
|
+
if (!promise) {
|
|
250
|
+
promise = load().then((module) => module.default);
|
|
251
|
+
void promise.catch(() => { promise = undefined; });
|
|
252
|
+
}
|
|
253
|
+
return promise;
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Static (validator-less) domain products served by the same release envelope. */
|
|
257
|
+
const STATIC_DOMAIN_NAMES = new Set([
|
|
258
|
+
'maps',
|
|
259
|
+
'npcs',
|
|
260
|
+
'reactors',
|
|
261
|
+
'skills',
|
|
262
|
+
'jobs',
|
|
263
|
+
'recipes',
|
|
264
|
+
'cash-shop',
|
|
265
|
+
'avatars',
|
|
266
|
+
]);
|
|
267
|
+
export function isStaticDomain(domain) {
|
|
268
|
+
return STATIC_DOMAIN_NAMES.has(domain);
|
|
269
|
+
}
|
|
270
|
+
/** Product capability implied by a content-addressed product ID. */
|
|
271
|
+
export function productCapability(productId) {
|
|
272
|
+
const match = /^(monsters|items|equipment|quests|media|maps|npcs|reactors|skills|jobs|recipes|cash-shop|avatars)-[0-9a-f]{64}$/u.exec(productId);
|
|
273
|
+
if (!match)
|
|
274
|
+
return undefined;
|
|
275
|
+
switch (match[1]) {
|
|
276
|
+
case 'monsters': return 'monster-core-v1';
|
|
277
|
+
case 'items': return 'items-v1';
|
|
278
|
+
case 'equipment': return 'equipment-v1';
|
|
279
|
+
case 'quests': return 'quests-v1';
|
|
280
|
+
case 'media': return 'media-v1';
|
|
281
|
+
case 'maps': return 'maps-v1';
|
|
282
|
+
case 'npcs': return 'npcs-v1';
|
|
283
|
+
case 'reactors': return 'reactors-v1';
|
|
284
|
+
case 'skills': return 'skills-v1';
|
|
285
|
+
case 'jobs': return 'jobs-v1';
|
|
286
|
+
case 'recipes': return 'recipes-v1';
|
|
287
|
+
case 'cash-shop': return 'cash-shop-v1';
|
|
288
|
+
case 'avatars': return 'avatars-v1';
|
|
289
|
+
default: return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Fetch, verify and validate one product manifest, then hand back the verified
|
|
294
|
+
* context that domain clients build on. This is the single manifest trust gate.
|
|
295
|
+
*/
|
|
296
|
+
export async function openProductContext(options) {
|
|
297
|
+
validateDescriptor(options.manifest);
|
|
298
|
+
const baseUrl = normalizedBaseUrl(options.baseUrl);
|
|
299
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
300
|
+
const fetchJson = createJsonFetcher({
|
|
301
|
+
baseUrl,
|
|
302
|
+
fetcher,
|
|
303
|
+
onRequest: options.onRequest,
|
|
304
|
+
signal: options.signal,
|
|
305
|
+
});
|
|
306
|
+
const rawManifest = await fetchJson(options.manifest, 'manifest');
|
|
307
|
+
if (!validateManifest(rawManifest)) {
|
|
308
|
+
throw new SchemaValidationError(options.manifest.path, JSON.stringify(validateManifest.errors ?? []));
|
|
309
|
+
}
|
|
310
|
+
const manifest = rawManifest;
|
|
311
|
+
assertCompatibleManifest(manifest);
|
|
312
|
+
const minimumVersion = minimumPackContractVersion(manifest);
|
|
313
|
+
const contract2 = manifest.contractVersion === '2.0';
|
|
314
|
+
if (contract2) {
|
|
315
|
+
if (!manifest.identity || manifest.identity.identityFormat !== 'mapletip-product-id-v2') {
|
|
316
|
+
throw new ContractVersionError('contract-2.0 product requires product-id-v2 identity metadata');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
assertSameScope(manifest.scope, options.expectedScope, options.manifest.path);
|
|
320
|
+
async function fetchEnvelope(descriptor, kind, validator, operation) {
|
|
321
|
+
const raw = await fetchJson(descriptor, kind, operation);
|
|
322
|
+
if (!validator(raw)) {
|
|
323
|
+
throw new SchemaValidationError(descriptor.path, JSON.stringify(validator.errors ?? []));
|
|
324
|
+
}
|
|
325
|
+
assertEnvelopeVersion(raw.contractVersion, minimumVersion, descriptor.path);
|
|
326
|
+
assertSameScope(raw.scope, manifest.scope, descriptor.path);
|
|
327
|
+
return raw;
|
|
328
|
+
}
|
|
329
|
+
async function fetchBlob(descriptor, operation) {
|
|
330
|
+
validateDescriptor(descriptor);
|
|
331
|
+
const url = resolveDescriptorUrl(baseUrl, descriptor.path);
|
|
332
|
+
options.onRequest?.({ kind: 'blob', path: descriptor.path, url: url.href, source: 'network' });
|
|
333
|
+
let response;
|
|
334
|
+
try {
|
|
335
|
+
response = await fetcher(url, {
|
|
336
|
+
redirect: 'error',
|
|
337
|
+
signal: operation?.signal ?? options.signal,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
342
|
+
throw error;
|
|
343
|
+
throw new RequestError(`Request failed for ${descriptor.path}`, { cause: error });
|
|
344
|
+
}
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
throw new RequestError(`HTTP ${response.status} fetching ${descriptor.path}`);
|
|
347
|
+
}
|
|
348
|
+
let bytes;
|
|
349
|
+
try {
|
|
350
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
throw new RequestError(`Unable to read response bytes for ${descriptor.path}`, { cause: error });
|
|
354
|
+
}
|
|
355
|
+
await verifyBlob(bytes, descriptor);
|
|
356
|
+
return bytes;
|
|
357
|
+
}
|
|
358
|
+
return { manifest, minimumVersion, contract2, fetchJson, fetchEnvelope, fetchBlob };
|
|
359
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EquipmentCatalog } from '../generated/equipment-catalog-2.d.ts';
|
|
2
|
+
import type { EquipmentDetailRecord } from '../generated/equipment-detail-2.d.ts';
|
|
3
|
+
import type { EquipmentRelationshipPage } from '../generated/equipment-relationship-pack-2.d.ts';
|
|
4
|
+
import type { EquipmentEvidencePage } from '../generated/equipment-evidence-pack-2.d.ts';
|
|
5
|
+
import { type GetRelationshipOptions, type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
|
|
6
|
+
export type { GetRelationshipOptions };
|
|
7
|
+
export type StaticEquipmentCatalog = EquipmentCatalog;
|
|
8
|
+
export type StaticEquipmentDetail = EquipmentDetailRecord;
|
|
9
|
+
export interface StaticEquipmentProductClient {
|
|
10
|
+
readonly manifest: StaticProductManifest;
|
|
11
|
+
loadCatalog(options?: OperationOptions): Promise<StaticEquipmentCatalog>;
|
|
12
|
+
getDetail(key: string, options?: OperationOptions): Promise<StaticEquipmentDetail>;
|
|
13
|
+
getRelationshipPage(pageKey: string, options?: GetRelationshipOptions): Promise<EquipmentRelationshipPage>;
|
|
14
|
+
getEvidencePage(pageKey: string, options?: OperationOptions): Promise<EquipmentEvidencePage>;
|
|
15
|
+
}
|
|
16
|
+
/** Build an Equipment client over an already verified product manifest. */
|
|
17
|
+
export declare function equipmentProductClient(context: VerifiedProductContext): StaticEquipmentProductClient;
|
|
18
|
+
/** Open one pinned Equipment product. */
|
|
19
|
+
export declare function openEquipmentProduct(options: OpenProductOptions): Promise<StaticEquipmentProductClient>;
|
|
20
|
+
export declare function isEquipmentProductClient(client: {
|
|
21
|
+
readonly manifest: StaticProductManifest;
|
|
22
|
+
}): client is StaticEquipmentProductClient;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import validateEquipmentCatalog from '../generated/equipment-catalog-2.validator.js';
|
|
2
|
+
import validateEquipmentDetail from '../generated/equipment-detail-2.validator.js';
|
|
3
|
+
import validateEquipmentDetailPack from '../generated/equipment-pack-2.validator.js';
|
|
4
|
+
import validateEquipmentRelationshipPack from '../generated/equipment-relationship-pack-2.validator.js';
|
|
5
|
+
import validateEquipmentEvidencePack from '../generated/equipment-evidence-pack-2.validator.js';
|
|
6
|
+
import { MissingPageError, MissingRecordError, SchemaValidationError, ScopeError, UnsupportedProductError, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, } from './core.js';
|
|
7
|
+
/** Build an Equipment client over an already verified product manifest. */
|
|
8
|
+
export function equipmentProductClient(context) {
|
|
9
|
+
const { manifest, fetchEnvelope } = context;
|
|
10
|
+
if (manifest.productId !== 'equipment')
|
|
11
|
+
throw new UnsupportedProductError(manifest.productId);
|
|
12
|
+
let catalogPromise;
|
|
13
|
+
const detailPackCache = new Map();
|
|
14
|
+
const relationshipPackCache = new Map();
|
|
15
|
+
const evidencePackCache = new Map();
|
|
16
|
+
async function loadCatalog(operation) {
|
|
17
|
+
if (!catalogPromise) {
|
|
18
|
+
catalogPromise = fetchEnvelope(manifest.catalog, 'catalog', validateEquipmentCatalog, operation);
|
|
19
|
+
void catalogPromise.catch(() => { catalogPromise = undefined; });
|
|
20
|
+
}
|
|
21
|
+
return catalogPromise;
|
|
22
|
+
}
|
|
23
|
+
async function getDetail(key, operation) {
|
|
24
|
+
const descriptor = recordRangeDescriptor(manifest.detailPacks, key, 'detail', 'Equipment');
|
|
25
|
+
const pack = await memoized(detailPackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'detail', validateEquipmentDetailPack, operation)));
|
|
26
|
+
if (!Object.hasOwn(pack.records, key))
|
|
27
|
+
throw new MissingRecordError(key, 'Equipment');
|
|
28
|
+
const detail = pack.records[key];
|
|
29
|
+
if (!detail)
|
|
30
|
+
throw new MissingRecordError(key, 'Equipment');
|
|
31
|
+
if (!validateEquipmentDetail(detail)) {
|
|
32
|
+
throw new SchemaValidationError(descriptor.path, JSON.stringify(validateEquipmentDetail.errors ?? []));
|
|
33
|
+
}
|
|
34
|
+
return detail;
|
|
35
|
+
}
|
|
36
|
+
async function getRelationshipPage(pageKey, operation) {
|
|
37
|
+
const descriptor = rangeDescriptor(manifest.relationshipPacks, pageKey, 'relationship');
|
|
38
|
+
const pack = await memoized(relationshipPackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'relationship', validateEquipmentRelationshipPack, operation)));
|
|
39
|
+
const page = pack.records[pageKey];
|
|
40
|
+
if (!page)
|
|
41
|
+
throw new MissingPageError(pageKey, 'relationship');
|
|
42
|
+
if (operation?.expectedOwnerKey !== undefined && page.owner !== operation.expectedOwnerKey) {
|
|
43
|
+
throw new ScopeError(`Relationship page ${pageKey} owner ${page.owner} does not match expected ${operation.expectedOwnerKey}`);
|
|
44
|
+
}
|
|
45
|
+
return page;
|
|
46
|
+
}
|
|
47
|
+
async function getEvidencePage(pageKey, operation) {
|
|
48
|
+
const descriptor = rangeDescriptor(manifest.evidencePacks ?? [], pageKey, 'evidence');
|
|
49
|
+
const pack = await memoized(evidencePackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'evidence', validateEquipmentEvidencePack, operation)));
|
|
50
|
+
const page = pack.records[pageKey];
|
|
51
|
+
if (!page)
|
|
52
|
+
throw new MissingPageError(pageKey, 'evidence');
|
|
53
|
+
return page;
|
|
54
|
+
}
|
|
55
|
+
return { manifest, loadCatalog, getDetail, getRelationshipPage, getEvidencePage };
|
|
56
|
+
}
|
|
57
|
+
/** Open one pinned Equipment product. */
|
|
58
|
+
export async function openEquipmentProduct(options) {
|
|
59
|
+
return equipmentProductClient(await openProductContext(options));
|
|
60
|
+
}
|
|
61
|
+
export function isEquipmentProductClient(client) {
|
|
62
|
+
return client.manifest.productId === 'equipment';
|
|
63
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ItemCatalog } from '../generated/item-catalog-2.d.ts';
|
|
2
|
+
import type { ItemDetailRecord } from '../generated/item-detail-2.d.ts';
|
|
3
|
+
import type { ItemRelationshipPage } from '../generated/item-relationship-pack-2.d.ts';
|
|
4
|
+
import type { ItemEvidencePage } from '../generated/item-evidence-pack-2.d.ts';
|
|
5
|
+
import { type GetRelationshipOptions, type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
|
|
6
|
+
export type StaticItemCatalog = ItemCatalog;
|
|
7
|
+
export type StaticItemDetail = ItemDetailRecord;
|
|
8
|
+
export type { GetRelationshipOptions };
|
|
9
|
+
export interface StaticItemProductClient {
|
|
10
|
+
readonly manifest: StaticProductManifest;
|
|
11
|
+
loadCatalog(options?: OperationOptions): Promise<StaticItemCatalog>;
|
|
12
|
+
getDetail(key: string, options?: OperationOptions): Promise<StaticItemDetail>;
|
|
13
|
+
getRelationshipPage(pageKey: string, options?: GetRelationshipOptions): Promise<ItemRelationshipPage>;
|
|
14
|
+
getEvidencePage(pageKey: string, options?: OperationOptions): Promise<ItemEvidencePage>;
|
|
15
|
+
}
|
|
16
|
+
/** Build an Item client over an already verified product manifest. */
|
|
17
|
+
export declare function itemProductClient(context: VerifiedProductContext): StaticItemProductClient;
|
|
18
|
+
/** Open one pinned Item product. */
|
|
19
|
+
export declare function openItemProduct(options: OpenProductOptions): Promise<StaticItemProductClient>;
|
|
20
|
+
export declare function isItemProductClient(client: {
|
|
21
|
+
readonly manifest: StaticProductManifest;
|
|
22
|
+
}): client is StaticItemProductClient;
|
package/client/items.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import validateItemCatalog from '../generated/item-catalog-2.validator.js';
|
|
2
|
+
import validateItemDetail from '../generated/item-detail-2.validator.js';
|
|
3
|
+
import validateItemDetailPack from '../generated/item-pack-2.validator.js';
|
|
4
|
+
import validateItemRelationshipPack from '../generated/item-relationship-pack-2.validator.js';
|
|
5
|
+
import validateItemEvidencePack from '../generated/item-evidence-pack-2.validator.js';
|
|
6
|
+
import { MissingPageError, MissingRecordError, SchemaValidationError, ScopeError, UnsupportedProductError, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, } from './core.js';
|
|
7
|
+
/** Build an Item client over an already verified product manifest. */
|
|
8
|
+
export function itemProductClient(context) {
|
|
9
|
+
const { manifest, fetchEnvelope } = context;
|
|
10
|
+
if (manifest.productId !== 'items')
|
|
11
|
+
throw new UnsupportedProductError(manifest.productId);
|
|
12
|
+
let catalogPromise;
|
|
13
|
+
const detailPackCache = new Map();
|
|
14
|
+
const relationshipPackCache = new Map();
|
|
15
|
+
const evidencePackCache = new Map();
|
|
16
|
+
async function loadCatalog(operation) {
|
|
17
|
+
if (!catalogPromise) {
|
|
18
|
+
catalogPromise = fetchEnvelope(manifest.catalog, 'catalog', validateItemCatalog, operation);
|
|
19
|
+
void catalogPromise.catch(() => { catalogPromise = undefined; });
|
|
20
|
+
}
|
|
21
|
+
return catalogPromise;
|
|
22
|
+
}
|
|
23
|
+
async function getDetail(key, operation) {
|
|
24
|
+
const descriptor = recordRangeDescriptor(manifest.detailPacks, key, 'detail', 'Item');
|
|
25
|
+
const pack = await memoized(detailPackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'detail', validateItemDetailPack, operation)));
|
|
26
|
+
if (!Object.hasOwn(pack.records, key))
|
|
27
|
+
throw new MissingRecordError(key, 'Item');
|
|
28
|
+
const detail = pack.records[key];
|
|
29
|
+
if (!detail)
|
|
30
|
+
throw new MissingRecordError(key, 'Item');
|
|
31
|
+
if (!validateItemDetail(detail)) {
|
|
32
|
+
throw new SchemaValidationError(descriptor.path, JSON.stringify(validateItemDetail.errors ?? []));
|
|
33
|
+
}
|
|
34
|
+
return detail;
|
|
35
|
+
}
|
|
36
|
+
async function getRelationshipPage(pageKey, operation) {
|
|
37
|
+
const descriptor = rangeDescriptor(manifest.relationshipPacks, pageKey, 'relationship');
|
|
38
|
+
const pack = await memoized(relationshipPackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'relationship', validateItemRelationshipPack, operation)));
|
|
39
|
+
const page = pack.records[pageKey];
|
|
40
|
+
if (!page)
|
|
41
|
+
throw new MissingPageError(pageKey, 'relationship');
|
|
42
|
+
if (operation?.expectedOwnerKey !== undefined && page.owner !== operation.expectedOwnerKey) {
|
|
43
|
+
throw new ScopeError(`Relationship page ${pageKey} owner ${page.owner} does not match expected ${operation.expectedOwnerKey}`);
|
|
44
|
+
}
|
|
45
|
+
return page;
|
|
46
|
+
}
|
|
47
|
+
async function getEvidencePage(pageKey, operation) {
|
|
48
|
+
const descriptor = rangeDescriptor(manifest.evidencePacks ?? [], pageKey, 'evidence');
|
|
49
|
+
const pack = await memoized(evidencePackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'evidence', validateItemEvidencePack, operation)));
|
|
50
|
+
const page = pack.records[pageKey];
|
|
51
|
+
if (!page)
|
|
52
|
+
throw new MissingPageError(pageKey, 'evidence');
|
|
53
|
+
return page;
|
|
54
|
+
}
|
|
55
|
+
return { manifest, loadCatalog, getDetail, getRelationshipPage, getEvidencePage };
|
|
56
|
+
}
|
|
57
|
+
/** Open one pinned Item product. */
|
|
58
|
+
export async function openItemProduct(options) {
|
|
59
|
+
return itemProductClient(await openProductContext(options));
|
|
60
|
+
}
|
|
61
|
+
export function isItemProductClient(client) {
|
|
62
|
+
return client.manifest.productId === 'items';
|
|
63
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MediaCatalog } from '../generated/media-catalog-2.d.ts';
|
|
2
|
+
import type { MediaDetailRecord } from '../generated/media-detail-2.d.ts';
|
|
3
|
+
import { type IntegrityDescriptor, type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
|
|
4
|
+
export { resolveMediaUrl, verifyBlob } from './core.js';
|
|
5
|
+
export type StaticMediaCatalog = MediaCatalog;
|
|
6
|
+
export type StaticMediaDetail = MediaDetailRecord;
|
|
7
|
+
export interface StaticMediaProductClient {
|
|
8
|
+
readonly manifest: StaticProductManifest;
|
|
9
|
+
loadCatalog(options?: OperationOptions): Promise<StaticMediaCatalog>;
|
|
10
|
+
getDetail(key: string, options?: OperationOptions): Promise<StaticMediaDetail>;
|
|
11
|
+
fetchBlob(descriptor: IntegrityDescriptor, options?: OperationOptions): Promise<Uint8Array>;
|
|
12
|
+
}
|
|
13
|
+
/** Build a Media client over an already verified product manifest. */
|
|
14
|
+
export declare function mediaProductClient(context: VerifiedProductContext): StaticMediaProductClient;
|
|
15
|
+
/** Open one pinned Media product. */
|
|
16
|
+
export declare function openMediaProduct(options: OpenProductOptions): Promise<StaticMediaProductClient>;
|
|
17
|
+
export declare function isMediaProductClient(client: {
|
|
18
|
+
readonly manifest: StaticProductManifest;
|
|
19
|
+
}): client is StaticMediaProductClient;
|
package/client/media.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import validateMediaCatalog from '../generated/media-catalog-2.validator.js';
|
|
2
|
+
import validateMediaDetail from '../generated/media-detail-2.validator.js';
|
|
3
|
+
import validateMediaDetailPack from '../generated/media-pack-2.validator.js';
|
|
4
|
+
import { MissingRecordError, SchemaValidationError, UnsupportedProductError, assertEnvelopeVersion, assertSameScope, memoized, openProductContext, recordRangeDescriptor, } from './core.js';
|
|
5
|
+
export { resolveMediaUrl, verifyBlob } from './core.js';
|
|
6
|
+
/** Build a Media client over an already verified product manifest. */
|
|
7
|
+
export function mediaProductClient(context) {
|
|
8
|
+
const { manifest, fetchEnvelope, fetchBlob } = context;
|
|
9
|
+
if (manifest.productId !== 'media')
|
|
10
|
+
throw new UnsupportedProductError(manifest.productId);
|
|
11
|
+
let catalogPromise;
|
|
12
|
+
const detailPackCache = new Map();
|
|
13
|
+
async function loadCatalog(operation) {
|
|
14
|
+
if (!catalogPromise) {
|
|
15
|
+
catalogPromise = fetchEnvelope(manifest.catalog, 'catalog', validateMediaCatalog, operation);
|
|
16
|
+
void catalogPromise.catch(() => { catalogPromise = undefined; });
|
|
17
|
+
}
|
|
18
|
+
const catalog = await catalogPromise;
|
|
19
|
+
if (catalog.records.length !== catalog.recordCount) {
|
|
20
|
+
throw new SchemaValidationError(manifest.catalog.path, 'recordCount does not match records length');
|
|
21
|
+
}
|
|
22
|
+
return catalog;
|
|
23
|
+
}
|
|
24
|
+
async function getDetail(key, operation) {
|
|
25
|
+
const descriptor = recordRangeDescriptor(manifest.detailPacks, key, 'detail', 'Media');
|
|
26
|
+
const pack = await memoized(detailPackCache, descriptor.path, () => (fetchEnvelope(descriptor, 'detail', validateMediaDetailPack, operation)));
|
|
27
|
+
if (!Object.hasOwn(pack.records, key))
|
|
28
|
+
throw new MissingRecordError(key);
|
|
29
|
+
const detail = pack.records[key];
|
|
30
|
+
if (!detail)
|
|
31
|
+
throw new MissingRecordError(key);
|
|
32
|
+
if (!validateMediaDetail(detail)) {
|
|
33
|
+
throw new SchemaValidationError(descriptor.path, JSON.stringify(validateMediaDetail.errors ?? []));
|
|
34
|
+
}
|
|
35
|
+
assertEnvelopeVersion(detail.contractVersion, context.minimumVersion, key);
|
|
36
|
+
assertSameScope(detail.scope, manifest.scope, key);
|
|
37
|
+
return detail;
|
|
38
|
+
}
|
|
39
|
+
return { manifest, loadCatalog, getDetail, fetchBlob };
|
|
40
|
+
}
|
|
41
|
+
/** Open one pinned Media product. */
|
|
42
|
+
export async function openMediaProduct(options) {
|
|
43
|
+
return mediaProductClient(await openProductContext(options));
|
|
44
|
+
}
|
|
45
|
+
export function isMediaProductClient(client) {
|
|
46
|
+
return client.manifest.productId === 'media';
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monster product client: verified catalog browse plus on-demand detail,
|
|
3
|
+
* location/relationship and evidence pages.
|
|
4
|
+
*
|
|
5
|
+
* Catalog validators are imported eagerly because a Monster browse cannot complete
|
|
6
|
+
* without them. Detail, relationship and evidence validators are loaded on the first
|
|
7
|
+
* operation that needs them, so a browse-only consumer never downloads them.
|
|
8
|
+
*/
|
|
9
|
+
import type { MonsterCatalog } from '../generated/monster-catalog.d.ts';
|
|
10
|
+
import type { MonsterCatalogV2 } from '../generated/monster-catalog-2.d.ts';
|
|
11
|
+
import type { MonsterDetailRecord, PageDescriptor } from '../generated/monster-detail.d.ts';
|
|
12
|
+
import type { MonsterDetailRecordV2 } from '../generated/monster-detail-2.d.ts';
|
|
13
|
+
import type { MonsterEvidencePage } from '../generated/monster-evidence-pack.d.ts';
|
|
14
|
+
import type { MonsterEvidencePageV2 } from '../generated/monster-evidence-pack-2.d.ts';
|
|
15
|
+
import type { MonsterLocationOccurrence } from '../generated/monster-relationship-pack.d.ts';
|
|
16
|
+
import type { MonsterCoreRelationshipPackV2 } from '../generated/monster-core-relationship-pack-2.d.ts';
|
|
17
|
+
import { type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
|
|
18
|
+
export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2;
|
|
19
|
+
export type StaticMonsterDetail = MonsterDetailRecord | MonsterDetailRecordV2;
|
|
20
|
+
export type StaticMonsterEvidencePage = MonsterEvidencePage | MonsterEvidencePageV2;
|
|
21
|
+
export interface StaticProductClient {
|
|
22
|
+
readonly manifest: StaticProductManifest;
|
|
23
|
+
loadCatalog(options?: OperationOptions): Promise<StaticMonsterCatalog>;
|
|
24
|
+
getDetail(key: string, options?: OperationOptions): Promise<StaticMonsterDetail>;
|
|
25
|
+
getRelationshipPage(page: PageDescriptor, options?: OperationOptions): Promise<MonsterLocationOccurrence[]>;
|
|
26
|
+
getEvidencePage(page: PageDescriptor, options?: OperationOptions): Promise<StaticMonsterEvidencePage>;
|
|
27
|
+
getCoreRelationshipPage(page: PageDescriptor, options?: OperationOptions): Promise<MonsterCoreRelationshipPackV2['records'][string]>;
|
|
28
|
+
}
|
|
29
|
+
/** Build a Monster client over an already verified product manifest. */
|
|
30
|
+
export declare function monsterProductClient(context: VerifiedProductContext): StaticProductClient;
|
|
31
|
+
/** Open one pinned Monster product. */
|
|
32
|
+
export declare function openMonsterProduct(options: OpenProductOptions): Promise<StaticProductClient>;
|
|
33
|
+
export declare function isMonsterProductClient(client: {
|
|
34
|
+
readonly manifest: StaticProductManifest;
|
|
35
|
+
}): client is StaticProductClient;
|