@assetlib/sdk-core 0.7.4-preview.1 → 0.7.6-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/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.4-preview.1' };
31
+ const sdk = options.sdk ?? { name: 'sdk-core', version: '0.7.6-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.4-preview.1",
3
+ "version": "0.7.6-preview.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",