platform-types 1.10.684 → 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.
- package/dist/ai.d.ts +4 -0
- package/dist/cjs/storefrontDesign.js +207 -1
- package/dist/storefrontDesign.d.ts +201 -1
- package/dist/storefrontDesign.js +203 -0
- package/package.json +1 -1
package/dist/ai.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ declare global {
|
|
|
35
35
|
type ProductEnrichTone = 'neutro y profesional' | 'formal' | 'cercano y amigable' | 'técnico y directo' | 'entusiasta y persuasivo';
|
|
36
36
|
interface ProductEnrichRequest {
|
|
37
37
|
productId: string;
|
|
38
|
+
draft?: {
|
|
39
|
+
name?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
};
|
|
38
42
|
fields?: ProductEnrichField[];
|
|
39
43
|
tone?: ProductEnrichTone;
|
|
40
44
|
regenerate?: boolean;
|
|
@@ -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 {};
|
package/dist/storefrontDesign.js
CHANGED
|
@@ -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
|
+
}
|