platform-types 1.10.685 → 1.10.686

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.
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.STOREFRONT_SOCIAL_HOSTS = exports.STOREFRONT_BENEFIT_ICONS = exports.STOREFRONT_RESERVED_SLUGS = exports.STOREFRONT_CONTACT_FIELDS = exports.STOREFRONT_PROTECTED_BUILTIN_DESTINATIONS = exports.STOREFRONT_BUILTIN_DESTINATIONS = exports.STOREFRONT_SECTION_TYPES = exports.STOREFRONT_DESIGN_LIMITS = void 0;
3
+ exports.STOREFRONT_PREVIEW_PROTOCOL_ERRORS = exports.STOREFRONT_PREVIEW_READ_OPERATIONS = exports.STOREFRONT_SOCIAL_HOSTS = exports.STOREFRONT_BENEFIT_ICONS = exports.STOREFRONT_RESERVED_SLUGS = exports.STOREFRONT_CONTACT_FIELDS = exports.STOREFRONT_PROTECTED_BUILTIN_DESTINATIONS = exports.STOREFRONT_BUILTIN_DESTINATIONS = exports.STOREFRONT_SECTION_TYPES = exports.STOREFRONT_DESIGN_LIMITS = void 0;
4
4
  exports.createStorefrontDesignV1 = createStorefrontDesignV1;
5
+ exports.parseStorefrontPreviewDocument = parseStorefrontPreviewDocument;
6
+ exports.parseStorefrontPreviewMessage = parseStorefrontPreviewMessage;
7
+ exports.acceptStorefrontPreviewMessage = acceptStorefrontPreviewMessage;
5
8
  exports.STOREFRONT_DESIGN_LIMITS = {
6
9
  customPages: 20,
7
10
  sectionsPerPage: 10,
@@ -32,6 +35,7 @@ exports.STOREFRONT_DESIGN_LIMITS = {
32
35
  imagePixels: 32_000_000,
33
36
  imageDimension: 8000,
34
37
  previewSeconds: 15 * 60,
38
+ previewMessageBytes: 300 * 1024,
35
39
  fieldErrors: 100,
36
40
  fieldErrorPath: 240,
37
41
  contactAddress: 500,
@@ -141,3 +145,205 @@ function createStorefrontDesignV1() {
141
145
  literalOverrides: {},
142
146
  };
143
147
  }
148
+ exports.STOREFRONT_PREVIEW_READ_OPERATIONS = [
149
+ 'design',
150
+ 'store',
151
+ 'product',
152
+ 'productBySlug',
153
+ 'productsByIds',
154
+ 'products',
155
+ 'group',
156
+ 'related',
157
+ 'categories',
158
+ 'brands',
159
+ 'tags',
160
+ 'collection',
161
+ 'paymentMethods',
162
+ 'deliveryMethods',
163
+ 'asset',
164
+ ];
165
+ const previewRecord = (value) => value !== null &&
166
+ typeof value === 'object' &&
167
+ !Array.isArray(value) &&
168
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
169
+ function previewWithinBytes(value, maximum) {
170
+ try {
171
+ const serialized = JSON.stringify(value);
172
+ return serialized !== undefined && new TextEncoder().encode(serialized).byteLength <= maximum;
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }
178
+ function previewSections(input) {
179
+ if (!Array.isArray(input) || input.length > exports.STOREFRONT_DESIGN_LIMITS.sectionsPerPage)
180
+ return undefined;
181
+ const sections = [];
182
+ for (const entry of input) {
183
+ if (!previewRecord(entry) || typeof entry.type !== 'string')
184
+ return undefined;
185
+ if (!exports.STOREFRONT_SECTION_TYPES.includes(entry.type))
186
+ continue;
187
+ if (typeof entry.id !== 'string' || typeof entry.enabled !== 'boolean' || !previewRecord(entry.content))
188
+ return undefined;
189
+ sections.push({
190
+ id: entry.id,
191
+ type: entry.type,
192
+ enabled: entry.enabled,
193
+ content: entry.content,
194
+ });
195
+ }
196
+ return sections;
197
+ }
198
+ function previewPage(input) {
199
+ if (!previewRecord(input) ||
200
+ typeof input.id !== 'string' ||
201
+ typeof input.slug !== 'string' ||
202
+ typeof input.title !== 'string' ||
203
+ typeof input.enabled !== 'boolean' ||
204
+ (input.seo !== undefined && !previewRecord(input.seo)))
205
+ return undefined;
206
+ const sections = previewSections(input.sections);
207
+ if (!sections)
208
+ return undefined;
209
+ return {
210
+ id: input.id,
211
+ slug: input.slug,
212
+ title: input.title,
213
+ enabled: input.enabled,
214
+ sections,
215
+ ...(input.seo === undefined ? {} : { seo: input.seo }),
216
+ };
217
+ }
218
+ /** Checks transport structure and UTF-8 size; unfamiliar section types are skipped, never rendered. */
219
+ function parseStorefrontPreviewDocument(input) {
220
+ if (!previewRecord(input) ||
221
+ input.schemaVersion !== 1 ||
222
+ input.templateId !== 'standard' ||
223
+ !previewWithinBytes(input, exports.STOREFRONT_DESIGN_LIMITS.documentBytes))
224
+ return undefined;
225
+ const { settings, branding, contact, seo, literalOverrides, home, pages, navigation, footer } = input;
226
+ if (![settings, branding, contact, seo, literalOverrides].every(previewRecord) ||
227
+ !previewRecord(home) ||
228
+ home.id !== 'home' ||
229
+ typeof home.title !== 'string' ||
230
+ typeof home.enabled !== 'boolean' ||
231
+ !Array.isArray(pages) ||
232
+ pages.length > exports.STOREFRONT_DESIGN_LIMITS.customPages ||
233
+ !Array.isArray(navigation) ||
234
+ !navigation.every(previewRecord) ||
235
+ !Array.isArray(footer) ||
236
+ !footer.every(previewRecord))
237
+ return undefined;
238
+ const sections = previewSections(home.sections);
239
+ const parsedPages = pages.map(previewPage);
240
+ if (!sections || parsedPages.some((page) => page === undefined))
241
+ return undefined;
242
+ return {
243
+ schemaVersion: 1,
244
+ templateId: 'standard',
245
+ settings: settings,
246
+ branding: branding,
247
+ contact: contact,
248
+ seo: seo,
249
+ literalOverrides: literalOverrides,
250
+ home: { id: 'home', title: home.title, enabled: home.enabled, sections },
251
+ pages: parsedPages,
252
+ navigation,
253
+ footer,
254
+ };
255
+ }
256
+ /** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
257
+ exports.STOREFRONT_PREVIEW_PROTOCOL_ERRORS = [
258
+ 'STOREFRONT_PREVIEW_EXPIRED',
259
+ 'STOREFRONT_PREVIEW_REVOKED',
260
+ 'STOREFRONT_PREVIEW_FORBIDDEN',
261
+ 'STOREFRONT_PREVIEW_UNAVAILABLE',
262
+ 'STOREFRONT_DESIGN_INVALID',
263
+ 'STOREFRONT_SCHEMA_UNSUPPORTED',
264
+ ];
265
+ const isPreviewProtocolError = (value) => exports.STOREFRONT_PREVIEW_PROTOCOL_ERRORS.includes(value);
266
+ const previewNonempty = (value) => typeof value === 'string' && value.length > 0;
267
+ const previewSequence = (value) => Number.isSafeInteger(value) && value >= 0;
268
+ const previewKeys = (input, keys) => Object.keys(input).every((key) => keys.includes(key));
269
+ function parsePreviewBind(input, base) {
270
+ if (!previewKeys(input, [
271
+ 'v',
272
+ 'type',
273
+ 'nonce',
274
+ 'sequence',
275
+ 'grant',
276
+ 'draftRevision',
277
+ 'document',
278
+ 'viewport',
279
+ 'appearance',
280
+ ]) ||
281
+ !previewSequence(input.sequence) ||
282
+ !previewNonempty(input.grant) ||
283
+ !previewNonempty(input.draftRevision) ||
284
+ (input.viewport !== 'desktop' && input.viewport !== 'mobile') ||
285
+ (input.appearance !== 'light' && input.appearance !== 'dark' && input.appearance !== 'system'))
286
+ return undefined;
287
+ const document = input.document === undefined ? undefined : parseStorefrontPreviewDocument(input.document);
288
+ if (input.document !== undefined && !document)
289
+ return undefined;
290
+ return {
291
+ ...base,
292
+ type: 'bind',
293
+ sequence: input.sequence,
294
+ grant: input.grant,
295
+ draftRevision: input.draftRevision,
296
+ viewport: input.viewport,
297
+ appearance: input.appearance,
298
+ ...(document ? { document } : {}),
299
+ };
300
+ }
301
+ function parsePreviewSequenced(input, base) {
302
+ if (!previewSequence(input.sequence))
303
+ return undefined;
304
+ const sequence = input.sequence;
305
+ switch (input.type) {
306
+ case 'update': {
307
+ if (!previewKeys(input, ['v', 'type', 'nonce', 'sequence', 'document']))
308
+ return undefined;
309
+ const document = parseStorefrontPreviewDocument(input.document);
310
+ return document ? { ...base, type: 'update', sequence, document } : undefined;
311
+ }
312
+ case 'ack':
313
+ return previewKeys(input, ['v', 'type', 'nonce', 'sequence', 'draftRevision']) &&
314
+ previewNonempty(input.draftRevision)
315
+ ? { ...base, type: 'ack', sequence, draftRevision: input.draftRevision }
316
+ : undefined;
317
+ case 'error':
318
+ return previewKeys(input, ['v', 'type', 'nonce', 'sequence', 'code']) && isPreviewProtocolError(input.code)
319
+ ? { ...base, type: 'error', sequence, code: input.code }
320
+ : undefined;
321
+ default:
322
+ return undefined;
323
+ }
324
+ }
325
+ /** Does not authenticate the grant. Keep it in memory and post only to the exact configured target origin. */
326
+ function parseStorefrontPreviewMessage(input) {
327
+ if (!previewRecord(input) ||
328
+ input.v !== 1 ||
329
+ !previewNonempty(input.nonce) ||
330
+ !previewWithinBytes(input, exports.STOREFRONT_DESIGN_LIMITS.previewMessageBytes))
331
+ return undefined;
332
+ const base = { v: 1, nonce: input.nonce };
333
+ if (input.type === 'ready')
334
+ return previewKeys(input, ['v', 'type', 'nonce']) ? { ...base, type: 'ready' } : undefined;
335
+ if (input.type === 'bind')
336
+ return parsePreviewBind(input, base);
337
+ return parsePreviewSequenced(input, base);
338
+ }
339
+ /** Start at sequence -1; ready is accepted only before sequenced messages. Advance after accepting. */
340
+ function acceptStorefrontPreviewMessage(event, binding) {
341
+ if (event.origin !== binding.origin || binding.source == null || event.source !== binding.source)
342
+ return undefined;
343
+ const message = parseStorefrontPreviewMessage(event.data);
344
+ if (!message || message.nonce !== binding.nonce)
345
+ return undefined;
346
+ if (message.type === 'ready')
347
+ return binding.sequence === -1 ? message : undefined;
348
+ return message.sequence > binding.sequence ? message : undefined;
349
+ }
@@ -29,6 +29,7 @@ export declare const STOREFRONT_DESIGN_LIMITS: {
29
29
  readonly imagePixels: 32000000;
30
30
  readonly imageDimension: 8000;
31
31
  readonly previewSeconds: number;
32
+ readonly previewMessageBytes: number;
32
33
  readonly fieldErrors: 100;
33
34
  readonly fieldErrorPath: 240;
34
35
  readonly contactAddress: 500;
@@ -266,6 +267,8 @@ declare global {
266
267
  * `/sf-media/{stage}/{storeId}/{publicationId}/{assetId}`, a path on the images host. Append
267
268
  * `/${StorefrontAssetVariant}` (one of `STOREFRONT_ASSET_VARIANTS`) to request it; the base
268
269
  * alone is not servable. Never route it through `/fit-in/`: the media path is refused there.
270
+ * Private preview envelopes instead supply `/preview/read?operation=asset&assetId=...`; the
271
+ * preview transport adds a variant query and its in-memory grant, then renders returned bytes.
269
272
  */
270
273
  type PublicStorefrontDesignValue<T> = T extends StorefrontDesignImagePlacement ? T & {
271
274
  url: string;
@@ -314,7 +317,7 @@ declare global {
314
317
  }
315
318
  type StorefrontDesignErrorCode = 'STOREFRONT_DESIGN_INVALID' | 'STOREFRONT_SCHEMA_UNSUPPORTED' | 'STOREFRONT_REVISION_CONFLICT' | 'STOREFRONT_DRAFT_NOT_INITIALIZED' | 'STOREFRONT_REQUEST_EXPIRED' | 'STOREFRONT_PUBLICATION_NOT_FOUND'
316
319
  /** Web `GET /store` and `GET /literals` `503`: the stored publication cannot be served; retry later. */
317
- | 'STOREFRONT_PUBLICATION_UNAVAILABLE' | 'STOREFRONT_OPERATION_NOT_FOUND' | 'STOREFRONT_DRAFT_UNAVAILABLE' | 'IDEMPOTENCY_CONFLICT' | 'STOREFRONT_EDITOR_REQUIRED' | 'STOREFRONT_ASSET_NOT_READY' | 'STOREFRONT_PREVIEW_EXPIRED' | 'STOREFRONT_PREVIEW_REVOKED' | 'STOREFRONT_PREVIEW_FORBIDDEN';
320
+ | 'STOREFRONT_PUBLICATION_UNAVAILABLE' | 'STOREFRONT_OPERATION_NOT_FOUND' | 'STOREFRONT_DRAFT_UNAVAILABLE' | 'IDEMPOTENCY_CONFLICT' | 'STOREFRONT_EDITOR_REQUIRED' | 'STOREFRONT_ASSET_NOT_READY' | 'STOREFRONT_PREVIEW_EXPIRED' | 'STOREFRONT_PREVIEW_REVOKED' | 'STOREFRONT_PREVIEW_FORBIDDEN' | 'STOREFRONT_PREVIEW_UNAVAILABLE';
318
321
  interface StorefrontDesignMappingConflict {
319
322
  path: string;
320
323
  code: 'LEGACY_MULTIPLE_PRODUCT_FEEDS' | 'LEGACY_UNMAPPED_CONTENT';
@@ -414,8 +417,188 @@ declare global {
414
417
  targets: StorefrontDesignDeliveryTarget[];
415
418
  };
416
419
  }
420
+ interface StorefrontDesignPreviewRevokeInput {
421
+ grant: string;
422
+ }
423
+ interface StorefrontDesignPreviewRevokeResult {
424
+ revoked: true;
425
+ }
426
+ interface StorefrontPreviewGrantClaims {
427
+ aud: 'storefront-preview';
428
+ stage: 'dev' | 'prod';
429
+ storeId: string;
430
+ userId: string;
431
+ family: string;
432
+ draftRevision: string;
433
+ draftDocumentId: string;
434
+ grantId: string;
435
+ iat: number;
436
+ exp: number;
437
+ }
438
+ type StorefrontPreviewOperation = (typeof STOREFRONT_PREVIEW_READ_OPERATIONS)[number];
439
+ /** GET query fields use their serialized string representation. Store scope comes only from the grant. */
440
+ type StorefrontPreviewReadQuery = {
441
+ operation: 'design' | 'paymentMethods' | 'deliveryMethods';
442
+ }
443
+ /**
444
+ * The shop's public web `GET /store` frame — the same allow-listed STORE
445
+ * fields, method rows, currencies view, `config`, `ecommerce` and contact —
446
+ * with `storefrontDesign` from the pinned draft. Answered whether or not the
447
+ * shop is open: see {@link StorefrontPreviewStoreFrame}.
448
+ */
449
+ | {
450
+ operation: 'store';
451
+ } | {
452
+ operation: 'product' | 'related';
453
+ productId: string;
454
+ }
455
+ /** Web `GET /products?mode=by-slug`'s read and answer, in the grant's store. */
456
+ | {
457
+ operation: 'productBySlug';
458
+ slug: string;
459
+ }
460
+ /** Web `GET /products?mode=by-ids`'s read and answer: 1–100 comma-separated canonical ids. */
461
+ | {
462
+ operation: 'productsByIds';
463
+ productIds: string;
464
+ } | {
465
+ operation: 'products';
466
+ search?: string;
467
+ categoryId?: string;
468
+ brandId?: string;
469
+ tagId?: string;
470
+ inOffer?: 'true';
471
+ inStock?: 'true';
472
+ isNew?: 'true';
473
+ startKey?: string;
474
+ } | {
475
+ operation: 'group';
476
+ groupId: string;
477
+ limit?: string;
478
+ cursor?: string;
479
+ } | {
480
+ operation: 'categories' | 'brands';
481
+ Limit?: string;
482
+ offSet?: string;
483
+ } | {
484
+ operation: 'tags';
485
+ cursor?: string;
486
+ }
487
+ /**
488
+ * Web `GET /collections/{slug}`'s page: a collection that changes between the
489
+ * reads bracketing a page answers 409 `CURSOR_STALE`, as the public route does.
490
+ */
491
+ | {
492
+ operation: 'collection';
493
+ slug: string;
494
+ cursor?: string;
495
+ }
496
+ /**
497
+ * Answers the image bytes, not JSON. Send `Accept: image/*`: the App API
498
+ * returns binary only when the request's Accept matches its `image/*` binary
499
+ * media type, and any other Accept receives the bytes as base64 text.
500
+ */
501
+ | {
502
+ operation: 'asset';
503
+ assetId: string;
504
+ variant?: StorefrontAssetVariant;
505
+ };
506
+ interface StorefrontPreviewDesignResult {
507
+ mode: 'preview';
508
+ draftRevision: string;
509
+ document: PublicStorefrontDesign;
510
+ literalLayers: {
511
+ scope: LiteralScope;
512
+ literals: Record<string, string>;
513
+ }[];
514
+ effectiveSettings: StorefrontDesignSettings;
515
+ defaultsVersion: string;
516
+ }
517
+ /**
518
+ * `operation=store`'s `data`: the fields web `GET /store` builds from the STORE
519
+ * row, built by the same projector, and `storefrontDesign` from the pinned
520
+ * draft. The contact is the draft's (`publicContact`, plus `email`/`phone`
521
+ * derived from it), as an adopted shop's comes from its publication.
522
+ *
523
+ * Never carries what only an open shop's public route knows — `publicStorefront`,
524
+ * `maintenance` and `globals` — and never answers `STOREFRONT_UNAVAILABLE`: a
525
+ * shop whose storefront is switched off can still preview its draft.
526
+ */
527
+ type StorefrontPreviewStoreFrame = Omit<Partial<Store>, 'storefrontDesign' | 'publicStorefront' | 'maintenance' | 'globals'> & {
528
+ storefrontDesign: StorefrontPreviewDesignResult;
529
+ };
530
+ /** A transport shape only; renderers validate known content fields before rendering them. */
531
+ interface StorefrontPreviewSection {
532
+ id: string;
533
+ type: StorefrontDesignSectionType;
534
+ enabled: boolean;
535
+ content: Record<string, unknown>;
536
+ }
537
+ interface StorefrontPreviewDocument {
538
+ schemaVersion: 1;
539
+ templateId: 'standard';
540
+ settings: Record<string, unknown>;
541
+ branding: Record<string, unknown>;
542
+ home: {
543
+ id: 'home';
544
+ title: string;
545
+ enabled: boolean;
546
+ sections: StorefrontPreviewSection[];
547
+ };
548
+ pages: {
549
+ id: string;
550
+ slug: string;
551
+ title: string;
552
+ enabled: boolean;
553
+ sections: StorefrontPreviewSection[];
554
+ seo?: object;
555
+ }[];
556
+ navigation: object[];
557
+ footer: object[];
558
+ contact: Record<string, unknown>;
559
+ seo: object;
560
+ literalOverrides: Record<string, unknown>;
561
+ }
562
+ type StorefrontPreviewProtocolError = (typeof STOREFRONT_PREVIEW_PROTOCOL_ERRORS)[number];
563
+ type StorefrontPreviewViewport = 'desktop' | 'mobile';
564
+ type StorefrontPreviewAppearance = 'light' | 'dark' | 'system';
565
+ interface StorefrontPreviewMessageBase {
566
+ v: 1;
567
+ nonce: string;
568
+ }
569
+ type StorefrontPreviewMessage = StorefrontPreviewMessageBase & ({
570
+ type: 'ready';
571
+ } | {
572
+ type: 'bind';
573
+ sequence: number;
574
+ grant: string;
575
+ draftRevision: string;
576
+ document?: StorefrontPreviewDocument;
577
+ viewport: StorefrontPreviewViewport;
578
+ appearance: StorefrontPreviewAppearance;
579
+ } | {
580
+ type: 'update';
581
+ sequence: number;
582
+ document: StorefrontPreviewDocument;
583
+ } | {
584
+ type: 'ack';
585
+ sequence: number;
586
+ draftRevision: string;
587
+ } | {
588
+ type: 'error';
589
+ sequence: number;
590
+ code: StorefrontPreviewProtocolError;
591
+ });
592
+ interface StorefrontPreviewPeerBinding {
593
+ origin: string;
594
+ source: unknown;
595
+ nonce: string;
596
+ /** Last accepted sequence from this peer. Keep a separate binding in each direction. */
597
+ sequence: number;
598
+ }
417
599
  interface StorefrontDesignPreviewResult {
418
600
  grant: string;
601
+ /** Server-configured origin echo; the app must require equality with its configured preview origin. */
419
602
  previewOrigin: string;
420
603
  draftRevision: string;
421
604
  expiresAt: string;
@@ -468,9 +651,26 @@ declare global {
468
651
  message: string;
469
652
  data: StorefrontDesignPublishResult;
470
653
  } | StorefrontDesignFailure;
654
+ type StorefrontDesignPreviewRevokeResponse = {
655
+ message: string;
656
+ data: StorefrontDesignPreviewRevokeResult;
657
+ } | StorefrontDesignFailure;
471
658
  type StorefrontDesignPreviewResponse = {
472
659
  message: string;
473
660
  data: StorefrontDesignPreviewResult;
474
661
  } | StorefrontDesignFailure;
475
662
  }
663
+ export declare const STOREFRONT_PREVIEW_READ_OPERATIONS: readonly ["design", "store", "product", "productBySlug", "productsByIds", "products", "group", "related", "categories", "brands", "tags", "collection", "paymentMethods", "deliveryMethods", "asset"];
664
+ /** Checks transport structure and UTF-8 size; unfamiliar section types are skipped, never rendered. */
665
+ export declare function parseStorefrontPreviewDocument(input: unknown): StorefrontPreviewDocument | undefined;
666
+ /** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
667
+ export declare const STOREFRONT_PREVIEW_PROTOCOL_ERRORS: readonly ["STOREFRONT_PREVIEW_EXPIRED", "STOREFRONT_PREVIEW_REVOKED", "STOREFRONT_PREVIEW_FORBIDDEN", "STOREFRONT_PREVIEW_UNAVAILABLE", "STOREFRONT_DESIGN_INVALID", "STOREFRONT_SCHEMA_UNSUPPORTED"];
668
+ /** Does not authenticate the grant. Keep it in memory and post only to the exact configured target origin. */
669
+ export declare function parseStorefrontPreviewMessage(input: unknown): StorefrontPreviewMessage | undefined;
670
+ /** Start at sequence -1; ready is accepted only before sequenced messages. Advance after accepting. */
671
+ export declare function acceptStorefrontPreviewMessage(event: {
672
+ origin: string;
673
+ source: unknown;
674
+ data: unknown;
675
+ }, binding: StorefrontPreviewPeerBinding): StorefrontPreviewMessage | undefined;
476
676
  export {};
@@ -28,6 +28,7 @@ export const STOREFRONT_DESIGN_LIMITS = {
28
28
  imagePixels: 32_000_000,
29
29
  imageDimension: 8000,
30
30
  previewSeconds: 15 * 60,
31
+ previewMessageBytes: 300 * 1024,
31
32
  fieldErrors: 100,
32
33
  fieldErrorPath: 240,
33
34
  contactAddress: 500,
@@ -137,3 +138,205 @@ export function createStorefrontDesignV1() {
137
138
  literalOverrides: {},
138
139
  };
139
140
  }
141
+ export const STOREFRONT_PREVIEW_READ_OPERATIONS = [
142
+ 'design',
143
+ 'store',
144
+ 'product',
145
+ 'productBySlug',
146
+ 'productsByIds',
147
+ 'products',
148
+ 'group',
149
+ 'related',
150
+ 'categories',
151
+ 'brands',
152
+ 'tags',
153
+ 'collection',
154
+ 'paymentMethods',
155
+ 'deliveryMethods',
156
+ 'asset',
157
+ ];
158
+ const previewRecord = (value) => value !== null &&
159
+ typeof value === 'object' &&
160
+ !Array.isArray(value) &&
161
+ (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
162
+ function previewWithinBytes(value, maximum) {
163
+ try {
164
+ const serialized = JSON.stringify(value);
165
+ return serialized !== undefined && new TextEncoder().encode(serialized).byteLength <= maximum;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ function previewSections(input) {
172
+ if (!Array.isArray(input) || input.length > STOREFRONT_DESIGN_LIMITS.sectionsPerPage)
173
+ return undefined;
174
+ const sections = [];
175
+ for (const entry of input) {
176
+ if (!previewRecord(entry) || typeof entry.type !== 'string')
177
+ return undefined;
178
+ if (!STOREFRONT_SECTION_TYPES.includes(entry.type))
179
+ continue;
180
+ if (typeof entry.id !== 'string' || typeof entry.enabled !== 'boolean' || !previewRecord(entry.content))
181
+ return undefined;
182
+ sections.push({
183
+ id: entry.id,
184
+ type: entry.type,
185
+ enabled: entry.enabled,
186
+ content: entry.content,
187
+ });
188
+ }
189
+ return sections;
190
+ }
191
+ function previewPage(input) {
192
+ if (!previewRecord(input) ||
193
+ typeof input.id !== 'string' ||
194
+ typeof input.slug !== 'string' ||
195
+ typeof input.title !== 'string' ||
196
+ typeof input.enabled !== 'boolean' ||
197
+ (input.seo !== undefined && !previewRecord(input.seo)))
198
+ return undefined;
199
+ const sections = previewSections(input.sections);
200
+ if (!sections)
201
+ return undefined;
202
+ return {
203
+ id: input.id,
204
+ slug: input.slug,
205
+ title: input.title,
206
+ enabled: input.enabled,
207
+ sections,
208
+ ...(input.seo === undefined ? {} : { seo: input.seo }),
209
+ };
210
+ }
211
+ /** Checks transport structure and UTF-8 size; unfamiliar section types are skipped, never rendered. */
212
+ export function parseStorefrontPreviewDocument(input) {
213
+ if (!previewRecord(input) ||
214
+ input.schemaVersion !== 1 ||
215
+ input.templateId !== 'standard' ||
216
+ !previewWithinBytes(input, STOREFRONT_DESIGN_LIMITS.documentBytes))
217
+ return undefined;
218
+ const { settings, branding, contact, seo, literalOverrides, home, pages, navigation, footer } = input;
219
+ if (![settings, branding, contact, seo, literalOverrides].every(previewRecord) ||
220
+ !previewRecord(home) ||
221
+ home.id !== 'home' ||
222
+ typeof home.title !== 'string' ||
223
+ typeof home.enabled !== 'boolean' ||
224
+ !Array.isArray(pages) ||
225
+ pages.length > STOREFRONT_DESIGN_LIMITS.customPages ||
226
+ !Array.isArray(navigation) ||
227
+ !navigation.every(previewRecord) ||
228
+ !Array.isArray(footer) ||
229
+ !footer.every(previewRecord))
230
+ return undefined;
231
+ const sections = previewSections(home.sections);
232
+ const parsedPages = pages.map(previewPage);
233
+ if (!sections || parsedPages.some((page) => page === undefined))
234
+ return undefined;
235
+ return {
236
+ schemaVersion: 1,
237
+ templateId: 'standard',
238
+ settings: settings,
239
+ branding: branding,
240
+ contact: contact,
241
+ seo: seo,
242
+ literalOverrides: literalOverrides,
243
+ home: { id: 'home', title: home.title, enabled: home.enabled, sections },
244
+ pages: parsedPages,
245
+ navigation,
246
+ footer,
247
+ };
248
+ }
249
+ /** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
250
+ export const STOREFRONT_PREVIEW_PROTOCOL_ERRORS = [
251
+ 'STOREFRONT_PREVIEW_EXPIRED',
252
+ 'STOREFRONT_PREVIEW_REVOKED',
253
+ 'STOREFRONT_PREVIEW_FORBIDDEN',
254
+ 'STOREFRONT_PREVIEW_UNAVAILABLE',
255
+ 'STOREFRONT_DESIGN_INVALID',
256
+ 'STOREFRONT_SCHEMA_UNSUPPORTED',
257
+ ];
258
+ const isPreviewProtocolError = (value) => STOREFRONT_PREVIEW_PROTOCOL_ERRORS.includes(value);
259
+ const previewNonempty = (value) => typeof value === 'string' && value.length > 0;
260
+ const previewSequence = (value) => Number.isSafeInteger(value) && value >= 0;
261
+ const previewKeys = (input, keys) => Object.keys(input).every((key) => keys.includes(key));
262
+ function parsePreviewBind(input, base) {
263
+ if (!previewKeys(input, [
264
+ 'v',
265
+ 'type',
266
+ 'nonce',
267
+ 'sequence',
268
+ 'grant',
269
+ 'draftRevision',
270
+ 'document',
271
+ 'viewport',
272
+ 'appearance',
273
+ ]) ||
274
+ !previewSequence(input.sequence) ||
275
+ !previewNonempty(input.grant) ||
276
+ !previewNonempty(input.draftRevision) ||
277
+ (input.viewport !== 'desktop' && input.viewport !== 'mobile') ||
278
+ (input.appearance !== 'light' && input.appearance !== 'dark' && input.appearance !== 'system'))
279
+ return undefined;
280
+ const document = input.document === undefined ? undefined : parseStorefrontPreviewDocument(input.document);
281
+ if (input.document !== undefined && !document)
282
+ return undefined;
283
+ return {
284
+ ...base,
285
+ type: 'bind',
286
+ sequence: input.sequence,
287
+ grant: input.grant,
288
+ draftRevision: input.draftRevision,
289
+ viewport: input.viewport,
290
+ appearance: input.appearance,
291
+ ...(document ? { document } : {}),
292
+ };
293
+ }
294
+ function parsePreviewSequenced(input, base) {
295
+ if (!previewSequence(input.sequence))
296
+ return undefined;
297
+ const sequence = input.sequence;
298
+ switch (input.type) {
299
+ case 'update': {
300
+ if (!previewKeys(input, ['v', 'type', 'nonce', 'sequence', 'document']))
301
+ return undefined;
302
+ const document = parseStorefrontPreviewDocument(input.document);
303
+ return document ? { ...base, type: 'update', sequence, document } : undefined;
304
+ }
305
+ case 'ack':
306
+ return previewKeys(input, ['v', 'type', 'nonce', 'sequence', 'draftRevision']) &&
307
+ previewNonempty(input.draftRevision)
308
+ ? { ...base, type: 'ack', sequence, draftRevision: input.draftRevision }
309
+ : undefined;
310
+ case 'error':
311
+ return previewKeys(input, ['v', 'type', 'nonce', 'sequence', 'code']) && isPreviewProtocolError(input.code)
312
+ ? { ...base, type: 'error', sequence, code: input.code }
313
+ : undefined;
314
+ default:
315
+ return undefined;
316
+ }
317
+ }
318
+ /** Does not authenticate the grant. Keep it in memory and post only to the exact configured target origin. */
319
+ export function parseStorefrontPreviewMessage(input) {
320
+ if (!previewRecord(input) ||
321
+ input.v !== 1 ||
322
+ !previewNonempty(input.nonce) ||
323
+ !previewWithinBytes(input, STOREFRONT_DESIGN_LIMITS.previewMessageBytes))
324
+ return undefined;
325
+ const base = { v: 1, nonce: input.nonce };
326
+ if (input.type === 'ready')
327
+ return previewKeys(input, ['v', 'type', 'nonce']) ? { ...base, type: 'ready' } : undefined;
328
+ if (input.type === 'bind')
329
+ return parsePreviewBind(input, base);
330
+ return parsePreviewSequenced(input, base);
331
+ }
332
+ /** Start at sequence -1; ready is accepted only before sequenced messages. Advance after accepting. */
333
+ export function acceptStorefrontPreviewMessage(event, binding) {
334
+ if (event.origin !== binding.origin || binding.source == null || event.source !== binding.source)
335
+ return undefined;
336
+ const message = parseStorefrontPreviewMessage(event.data);
337
+ if (!message || message.nonce !== binding.nonce)
338
+ return undefined;
339
+ if (message.type === 'ready')
340
+ return binding.sequence === -1 ? message : undefined;
341
+ return message.sequence > binding.sequence ? message : undefined;
342
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platform-types",
3
- "version": "1.10.685",
3
+ "version": "1.10.686",
4
4
  "description": "Shared wire and entity contracts for the platform.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",