@assetlib/sdk-core 0.7.5-preview.1 → 0.7.7-preview.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/README.md CHANGED
@@ -86,7 +86,7 @@ App-owned catalog placements may include `bundledAccessibility` with the same sh
86
86
 
87
87
  ## Image formats and sizes
88
88
 
89
- Preview 0.2 accepts the optional signed `renditionSchemaVersion: 1` extension. Existing schema-1 manifests still work; older clients can use the mandatory WebP fallback in new manifests. By default, the client selects PNG or WebP. Ask for physical pixels with both `pixelWidth` and `pixelHeight`; without a target, selection uses the generated placement dimensions. Layout and display density remain the application's responsibility.
89
+ Preview 0.2 accepts the optional signed `renditionSchemaVersion: 1` extension. Existing schema-1 manifests still work; older clients can use the mandatory WebP fallback in new manifests. By default, the client selects PNG or WebP. Ask for physical pixels with both `pixelWidth` and `pixelHeight`; without a target, selection uses the generated reference dimensions. Core cannot see layout: the Expo components pass the measured view × display scale (see the [Expo README](../sdk-expo/README.md#sizing)); other callers pass their own target.
90
90
 
91
91
  Selection uses the smallest rendition covering both dimensions, then byte size and hash. If every rendition is smaller, the largest is preferred. Failed candidates advance to the next candidate, then the legacy fallback. Historical releases remain cache-only. The result describes the actual selected MIME, hash and pixel dimensions. It does not convert one format into another on the device.
92
92
 
@@ -102,7 +102,7 @@ A catalog placement can declare `"rendering": "template"`. Assetlib then deliver
102
102
 
103
103
  Signed image descriptors (placements, state members, variant cells and catalog page images) may carry `rendering`. A present value that is not a string matching `^[a-z][a-z0-9-]{0,31}$` rejects the release; absent means `original`. Resolution compares the reference's rendering with the descriptor actually selected after arm, appearance and state selection. If they differ, or the descriptor names a value this client does not know, that release's descriptor is skipped as for an incompatible size: its cache entry is not read and nothing is downloaded. When the current release publishes the key, resolution continues with the usual order, a compatible verified cached image from an older retained release and then the bundled fallback (`fallbackReason: 'missing'`). Other placements are unaffected. Catalog page images resolve as `original`. `ResolvedAsset.rendering` reports the delivered descriptor's rendering, `'original'` or `'template'`.
104
104
 
105
- The bundled fallback must also be a single-color shape, such as a PNG with alpha, because the app applies the same tint to remote and bundled artwork. Remote masks are rasters, so request physical pixels with `pixelWidth` and `pixelHeight` (logical size × display scale); the Expo adapter does this for template references. Template rendering needs this SDK and CLI or newer: older code generation drops the field, and older runtimes do not check it. Publishing template descriptors from the hosted console is planned; until then releases carry no `rendering` field.
105
+ The bundled fallback must also be a single-color shape, such as a PNG with alpha, because the app applies the same tint to remote and bundled artwork. Remote masks are rasters, so request physical pixels with `pixelWidth` and `pixelHeight` (view size × display scale); the Expo components do this for every placement. Template rendering needs this SDK and CLI or newer: older code generation drops the field, and older runtimes do not check it. Publishing template descriptors from the hosted console is planned; until then releases carry no `rendering` field.
106
106
 
107
107
  ## Offline typed references
108
108
 
@@ -121,7 +121,7 @@ Check a catalog into your application:
121
121
  node node_modules/@assetlib/sdk-core/bin/codegen.mjs assetlib.catalog.json > src/assets.generated.ts
122
122
  ```
123
123
 
124
- Use `AppAssets.Travel.coast` in application code. Existing compatible artwork updates do not require regenerating this catalog. Adding a new application placement does. Schema v1 identifies placements by key and by width and height. The size is a reference, not a crop: it fixes the aspect ratio every published image must match (the console checks it when an image is assigned and again at publish), and it is the size `resolve` requests when the caller passes no `pixelWidth` and `pixelHeight`; published images may have any pixel size at that aspect ratio. Stable placement IDs and separately versioned contracts are not implemented yet. Renaming a key requires a coordinated migration, not simply renaming it in the console.
124
+ Use `AppAssets.Travel.coast` in application code. Existing compatible artwork updates do not require regenerating this catalog. Adding a new application placement does. Schema v1 identifies placements by key and by width and height. Width and height are a reference size: they fix the aspect ratio and identify the placement; the SDK sizes requests from the view. Every published image must match that aspect ratio (the console checks it when an image is assigned and again at publish) and may have any pixel size. `resolve` without `pixelWidth` and `pixelHeight` uses the reference size as its target; the Expo components measure the view instead. Changing the numbers changes the placement's identity, so never renumber a catalog to change sharpness. Stable placement IDs and separately versioned contracts are not implemented yet. Renaming a key requires a coordinated migration, not simply renaming it in the console.
125
125
 
126
126
  ## Appearance and arm variants
127
127
 
package/dist/index.d.ts CHANGED
@@ -43,6 +43,8 @@ export declare class AssetClient {
43
43
  private options;
44
44
  readonly config: AssetlibConfig;
45
45
  private state;
46
+ private adoptedRaw;
47
+ private payloads;
46
48
  private initialized;
47
49
  private storageFailure;
48
50
  private lastError;
package/dist/index.js CHANGED
@@ -224,18 +224,35 @@ async function fetchBounded(fetcher, url, maxBytes, timeoutMs, accept = 'image/w
224
224
  controller.abort();
225
225
  }
226
226
  }
227
+ /** Iterative, so any depth JSON.parse accepted freezes without exhausting the stack. */
228
+ function deepFreeze(value) {
229
+ const pending = [value];
230
+ while (pending.length) {
231
+ const item = pending.pop();
232
+ if (item === null || typeof item !== 'object')
233
+ continue;
234
+ Object.freeze(item);
235
+ for (const child of Object.values(item))
236
+ pending.push(child);
237
+ }
238
+ return value;
239
+ }
227
240
  /** Durable replay identity survives authorized route and signing-key changes. */
228
241
  export function storageNamespace(config) {
229
242
  return hashBytes(utf8ToBytes(JSON.stringify([new URL(config.manifestUrl).origin, config.orgId, config.appId, config.environment]))).slice(0, 32);
230
243
  }
231
244
  /** Use the same verification for normal state loads and legacy namespace migration. */
232
245
  export function verifyStoredState(serialized, config) {
246
+ return verifyStoredHistory(serialized, config).state;
247
+ }
248
+ function verifyStoredHistory(serialized, config) {
233
249
  if (utf8ToBytes(serialized).length > SDK_LIMITS.stateBytes)
234
250
  throw new Error('Stored SDK state exceeds its bound.');
235
251
  const state = JSON.parse(serialized);
236
252
  if (!record(state) || state.version !== 1 || !integer(state.highestSequence, 1, 2_147_483_647) || !Array.isArray(state.history) || state.history.length < 1 || state.history.length > SDK_LIMITS.retainedReleases)
237
253
  throw new Error('Stored SDK state is invalid.');
238
254
  let previous = state.highestSequence + 1;
255
+ const payloads = [];
239
256
  for (const [index, entry] of state.history.entries()) {
240
257
  const { payload } = verifySignedManifest(entry, config);
241
258
  if (payload.sequence >= previous)
@@ -243,13 +260,16 @@ export function verifyStoredState(serialized, config) {
243
260
  if (index === 0 && payload.sequence !== state.highestSequence)
244
261
  throw new Error('Stored sequence does not match its signed manifest.');
245
262
  previous = payload.sequence;
263
+ payloads.push(payload);
246
264
  }
247
- return state;
265
+ return { state: state, payloads };
248
266
  }
249
267
  export class AssetClient {
250
268
  options;
251
269
  config;
252
270
  state = { version: 1, highestSequence: 0, history: [] };
271
+ adoptedRaw = null;
272
+ payloads = [];
253
273
  initialized = false;
254
274
  storageFailure = null;
255
275
  lastError = null;
@@ -322,16 +342,22 @@ export class AssetClient {
322
342
  throw new Error('Stored release state disappeared.');
323
343
  return;
324
344
  }
325
- const next = verifyStoredState(raw, this.config);
345
+ // These exact bytes were already verified with this client's fixed config.
346
+ if (raw === this.adoptedRaw)
347
+ return;
348
+ const { state: next, payloads } = verifyStoredHistory(raw, this.config);
326
349
  if (next.highestSequence < this.state.highestSequence)
327
350
  throw new Error('Stored release state moved backwards.');
328
- const accepted = new Map(this.state.history.map(entry => [verifySignedManifest(entry, this.config).payload.sequence, entry.payload]));
329
- for (const entry of next.history) {
330
- const previous = accepted.get(verifySignedManifest(entry, this.config).payload.sequence);
351
+ const accepted = new Map(this.state.history.map((entry, index) => [this.payloads[index].sequence, entry.payload]));
352
+ for (const [index, entry] of next.history.entries()) {
353
+ const previous = accepted.get(payloads[index].sequence);
331
354
  if (previous !== undefined && previous !== entry.payload)
332
355
  throw new Error('Stored release state conflicts with an accepted release.');
333
356
  }
357
+ const frozen = payloads.map(deepFreeze);
334
358
  this.state = next;
359
+ this.payloads = frozen;
360
+ this.adoptedRaw = raw;
335
361
  }
336
362
  catch {
337
363
  this.storageFailure = 'Stored release state could not be verified. Using bundled assets; repair or explicitly reset app data before reconnecting.';
@@ -367,8 +393,12 @@ export class AssetClient {
367
393
  const serialized = JSON.stringify(next);
368
394
  if (utf8ToBytes(serialized).length > SDK_LIMITS.stateBytes)
369
395
  throw new Error('Release history exceeds the storage bound.');
396
+ const payloads = [deepFreeze(payload), ...this.payloads].slice(0, SDK_LIMITS.retainedReleases);
370
397
  await this.options.storage.saveState(serialized);
398
+ // Nothing may throw between these: state, payloads and adoptedRaw describe one history.
371
399
  this.state = next;
400
+ this.payloads = payloads;
401
+ this.adoptedRaw = serialized;
372
402
  this.lastError = null;
373
403
  return { updated: true, sequence: payload.sequence };
374
404
  }
@@ -382,7 +412,7 @@ export class AssetClient {
382
412
  }
383
413
  /** Only metadata transitions use the serial queue. Image transfers are bounded and concurrent. */
384
414
  snapshot() {
385
- return this.serial(async () => { await this.load(); return this.storageFailure ? [] : this.state.history.map(entry => ({ payload: verifySignedManifest(entry, this.config).payload })); });
415
+ return this.serial(async () => { await this.load(); return this.storageFailure ? [] : this.payloads.map(payload => ({ payload })); });
386
416
  }
387
417
  withTransfer(work, signal) {
388
418
  throwIfAborted(signal);
package/dist/telemetry.js CHANGED
@@ -28,7 +28,7 @@ export class ObservationReporter {
28
28
  this.fetcher = fetcher;
29
29
  this.interval = boundedInteger(options.flushIntervalMs, 60_000, 2_147_483_647);
30
30
  this.maxBatch = boundedInteger(options.maxBatch, 200, 200);
31
- const sdk = options.sdk ?? { name: 'sdk-core', version: '0.7.5-preview.1' };
31
+ const sdk = options.sdk ?? { name: 'sdk-core', version: '0.7.7-preview.1' };
32
32
  const build = options.build ?? { platform: 'web', appVersion: 'unknown', buildNumber: 'unknown' };
33
33
  if (!version(sdk.name) || !version(sdk.version) || !['ios', 'android', 'web', 'expo'].includes(build.platform) || !version(build.appVersion) || !version(build.buildNumber) || (options.installId !== undefined && !validId(options.installId)))
34
34
  throw new Error('Invalid observation configuration.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assetlib/sdk-core",
3
- "version": "0.7.5-preview.1",
3
+ "version": "0.7.7-preview.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",