platform-types 1.10.781 → 1.10.783
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/basket.d.ts +56 -0
- package/dist/basket.js +36 -0
- package/dist/cart.d.ts +16 -0
- package/dist/cjs/basket.js +37 -1
- package/dist/cjs/storefrontDesign.js +99 -1
- package/dist/store.d.ts +19 -0
- package/dist/storefrontDesign.d.ts +84 -8
- package/dist/storefrontDesign.js +97 -1
- package/package.json +1 -1
package/dist/basket.d.ts
CHANGED
|
@@ -99,4 +99,60 @@ export type BasketErrorCode = (typeof BASKET_ERROR_CODES)[number];
|
|
|
99
99
|
*/
|
|
100
100
|
export declare const BASKET_RESUME_ERROR_CODES: readonly ["CART_RESUME_UNAVAILABLE", "CART_RESUME_INCOMPLETE"];
|
|
101
101
|
export type BasketResumeErrorCode = (typeof BASKET_RESUME_ERROR_CODES)[number];
|
|
102
|
+
/**
|
|
103
|
+
* The guest cart token's header, BOTH ways. A shopper with no account sends it
|
|
104
|
+
* on `GET`, `POST` and `DELETE /basket/guest`, and every successful guest
|
|
105
|
+
* `POST` answers a fresh one in the same header (exposed to browser JS through
|
|
106
|
+
* `Access-Control-Expose-Headers`). Keep the newest: each write slides the
|
|
107
|
+
* 30-day expiry. The first `POST`, sent without one, creates the cart and is
|
|
108
|
+
* the only way to obtain one. Never put it in a URL or a log.
|
|
109
|
+
*/
|
|
110
|
+
export declare const GUEST_CART_TOKEN_HEADER = "X-Guest-Cart-Token";
|
|
111
|
+
/**
|
|
112
|
+
* The refusals the guest cart routes answer themselves, beside the cart
|
|
113
|
+
* resolver's `BasketErrorCode` refusals they forward unchanged.
|
|
114
|
+
*
|
|
115
|
+
* - GUEST_CART_TOKEN_INVALID (401): every reason the token cannot be honoured —
|
|
116
|
+
* missing, tampered, expired, minted for another store or presented on
|
|
117
|
+
* another store's host or `x-store`, revoked by a sign-in merge, or naming a
|
|
118
|
+
* cart that is gone, belongs to an account, is held by a till, was checked
|
|
119
|
+
* out or was swept as abandoned — with one identical body, so the token is
|
|
120
|
+
* never an oracle. Drop the token and start a new guest cart.
|
|
121
|
+
* - GUEST_CHECKOUT_DISABLED (403): the shop does not sell to guests — it has not
|
|
122
|
+
* switched guest checkout on, admits buyers only after approval, or hides
|
|
123
|
+
* prices from anonymous visitors. Public `GET /store` reports the same verdict
|
|
124
|
+
* as `ecommerce.config.guestCheckoutAvailable`.
|
|
125
|
+
* - GUEST_CHECKOUT_SIGNED_IN (409): the request carried an `Authorization`
|
|
126
|
+
* header. A signed-in buyer is priced and policed as their account, so they
|
|
127
|
+
* use `/basket`; nothing was read or written.
|
|
128
|
+
*
|
|
129
|
+
* A token-holding guest write must send the `version` it read; a stale one is
|
|
130
|
+
* `409 CART_CHANGED_SINCE_CONFIRM` with `data: { currentCartVersion }` (the
|
|
131
|
+
* storefront checkout's code) and writes nothing.
|
|
132
|
+
*/
|
|
133
|
+
export declare const GUEST_CART_ERROR_CODES: readonly ["GUEST_CART_TOKEN_INVALID", "GUEST_CHECKOUT_DISABLED", "GUEST_CHECKOUT_SIGNED_IN"];
|
|
134
|
+
export type GuestCartErrorCode = (typeof GUEST_CART_ERROR_CODES)[number];
|
|
135
|
+
declare global {
|
|
136
|
+
/**
|
|
137
|
+
* A guest cart write's body: exactly the `/basket` named-action envelope.
|
|
138
|
+
* `data` is the cart without buy prices or any server-owned revocation
|
|
139
|
+
* field. The re-issued token travels in the `X-Guest-Cart-Token` response
|
|
140
|
+
* header, never in the body.
|
|
141
|
+
*/
|
|
142
|
+
type GuestCartResponse = CartActionResponse;
|
|
143
|
+
/**
|
|
144
|
+
* `POST /basket { mode: 'merge-guest' }` on the SIGNED-IN route: fold the
|
|
145
|
+
* guest cart the token names into the caller's own cart, re-priced at the
|
|
146
|
+
* account's terms (overflow in `droppedSkus`), and revoke the token in the
|
|
147
|
+
* same write that marks the guest cart `converted`. Answers the `/basket`
|
|
148
|
+
* named-action envelope; a replay of the same token is
|
|
149
|
+
* `401 GUEST_CART_TOKEN_INVALID`. `version` is the caller's OWN cart
|
|
150
|
+
* version, as on every other named action.
|
|
151
|
+
*/
|
|
152
|
+
interface GuestCartMergeRequest {
|
|
153
|
+
mode: 'merge-guest';
|
|
154
|
+
guestCartToken: string;
|
|
155
|
+
version?: number;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
102
158
|
export {};
|
package/dist/basket.js
CHANGED
|
@@ -68,3 +68,39 @@ export const BASKET_ERROR_CODES = [
|
|
|
68
68
|
* clear on retry.
|
|
69
69
|
*/
|
|
70
70
|
export const BASKET_RESUME_ERROR_CODES = ['CART_RESUME_UNAVAILABLE', 'CART_RESUME_INCOMPLETE'];
|
|
71
|
+
/**
|
|
72
|
+
* The guest cart token's header, BOTH ways. A shopper with no account sends it
|
|
73
|
+
* on `GET`, `POST` and `DELETE /basket/guest`, and every successful guest
|
|
74
|
+
* `POST` answers a fresh one in the same header (exposed to browser JS through
|
|
75
|
+
* `Access-Control-Expose-Headers`). Keep the newest: each write slides the
|
|
76
|
+
* 30-day expiry. The first `POST`, sent without one, creates the cart and is
|
|
77
|
+
* the only way to obtain one. Never put it in a URL or a log.
|
|
78
|
+
*/
|
|
79
|
+
export const GUEST_CART_TOKEN_HEADER = 'X-Guest-Cart-Token';
|
|
80
|
+
/**
|
|
81
|
+
* The refusals the guest cart routes answer themselves, beside the cart
|
|
82
|
+
* resolver's `BasketErrorCode` refusals they forward unchanged.
|
|
83
|
+
*
|
|
84
|
+
* - GUEST_CART_TOKEN_INVALID (401): every reason the token cannot be honoured —
|
|
85
|
+
* missing, tampered, expired, minted for another store or presented on
|
|
86
|
+
* another store's host or `x-store`, revoked by a sign-in merge, or naming a
|
|
87
|
+
* cart that is gone, belongs to an account, is held by a till, was checked
|
|
88
|
+
* out or was swept as abandoned — with one identical body, so the token is
|
|
89
|
+
* never an oracle. Drop the token and start a new guest cart.
|
|
90
|
+
* - GUEST_CHECKOUT_DISABLED (403): the shop does not sell to guests — it has not
|
|
91
|
+
* switched guest checkout on, admits buyers only after approval, or hides
|
|
92
|
+
* prices from anonymous visitors. Public `GET /store` reports the same verdict
|
|
93
|
+
* as `ecommerce.config.guestCheckoutAvailable`.
|
|
94
|
+
* - GUEST_CHECKOUT_SIGNED_IN (409): the request carried an `Authorization`
|
|
95
|
+
* header. A signed-in buyer is priced and policed as their account, so they
|
|
96
|
+
* use `/basket`; nothing was read or written.
|
|
97
|
+
*
|
|
98
|
+
* A token-holding guest write must send the `version` it read; a stale one is
|
|
99
|
+
* `409 CART_CHANGED_SINCE_CONFIRM` with `data: { currentCartVersion }` (the
|
|
100
|
+
* storefront checkout's code) and writes nothing.
|
|
101
|
+
*/
|
|
102
|
+
export const GUEST_CART_ERROR_CODES = [
|
|
103
|
+
'GUEST_CART_TOKEN_INVALID',
|
|
104
|
+
'GUEST_CHECKOUT_DISABLED',
|
|
105
|
+
'GUEST_CHECKOUT_SIGNED_IN',
|
|
106
|
+
];
|
package/dist/cart.d.ts
CHANGED
|
@@ -76,6 +76,22 @@ declare global {
|
|
|
76
76
|
* `parkedAt` and to the POS un-park verb, which happens to share the word.
|
|
77
77
|
*/
|
|
78
78
|
resumedAt?: number;
|
|
79
|
+
/**
|
|
80
|
+
* SERVER-OWNED, guest carts only: the revocation generation of the guest
|
|
81
|
+
* cart token. A token carries the generation it was minted at and is refused
|
|
82
|
+
* once this moves; absent reads as 0. The sign-in merge (`POST /basket
|
|
83
|
+
* { mode: 'merge-guest' }`) adds 1 in the same write that marks the cart
|
|
84
|
+
* `converted`. Never sent to a shopper; no client writes it.
|
|
85
|
+
*/
|
|
86
|
+
guestTokenGeneration?: number;
|
|
87
|
+
/**
|
|
88
|
+
* SERVER-OWNED, guest carts only: the random value the write that CREATED
|
|
89
|
+
* the guest cart stamped, carried by every guest cart token minted for it.
|
|
90
|
+
* A token is honoured only on a row bearing its own nonce, so a row deleted
|
|
91
|
+
* and re-created under the same `cartId` never revives an old token. Never
|
|
92
|
+
* sent to a shopper or an operator; no client writes it.
|
|
93
|
+
*/
|
|
94
|
+
guestTokenNonce?: string;
|
|
79
95
|
parkedAt?: number;
|
|
80
96
|
version?: number;
|
|
81
97
|
lines: CartLine[];
|
package/dist/cjs/basket.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BASKET_RESUME_ERROR_CODES = exports.BASKET_ERROR_CODES = void 0;
|
|
3
|
+
exports.GUEST_CART_ERROR_CODES = exports.GUEST_CART_TOKEN_HEADER = exports.BASKET_RESUME_ERROR_CODES = exports.BASKET_ERROR_CODES = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Domain refusals from POST /baskets (replacement, named actions and park/resume)
|
|
6
6
|
* and GET /baskets?mode=review. Check GATING_ERROR_CODES separately for middleware
|
|
@@ -71,3 +71,39 @@ exports.BASKET_ERROR_CODES = [
|
|
|
71
71
|
* clear on retry.
|
|
72
72
|
*/
|
|
73
73
|
exports.BASKET_RESUME_ERROR_CODES = ['CART_RESUME_UNAVAILABLE', 'CART_RESUME_INCOMPLETE'];
|
|
74
|
+
/**
|
|
75
|
+
* The guest cart token's header, BOTH ways. A shopper with no account sends it
|
|
76
|
+
* on `GET`, `POST` and `DELETE /basket/guest`, and every successful guest
|
|
77
|
+
* `POST` answers a fresh one in the same header (exposed to browser JS through
|
|
78
|
+
* `Access-Control-Expose-Headers`). Keep the newest: each write slides the
|
|
79
|
+
* 30-day expiry. The first `POST`, sent without one, creates the cart and is
|
|
80
|
+
* the only way to obtain one. Never put it in a URL or a log.
|
|
81
|
+
*/
|
|
82
|
+
exports.GUEST_CART_TOKEN_HEADER = 'X-Guest-Cart-Token';
|
|
83
|
+
/**
|
|
84
|
+
* The refusals the guest cart routes answer themselves, beside the cart
|
|
85
|
+
* resolver's `BasketErrorCode` refusals they forward unchanged.
|
|
86
|
+
*
|
|
87
|
+
* - GUEST_CART_TOKEN_INVALID (401): every reason the token cannot be honoured —
|
|
88
|
+
* missing, tampered, expired, minted for another store or presented on
|
|
89
|
+
* another store's host or `x-store`, revoked by a sign-in merge, or naming a
|
|
90
|
+
* cart that is gone, belongs to an account, is held by a till, was checked
|
|
91
|
+
* out or was swept as abandoned — with one identical body, so the token is
|
|
92
|
+
* never an oracle. Drop the token and start a new guest cart.
|
|
93
|
+
* - GUEST_CHECKOUT_DISABLED (403): the shop does not sell to guests — it has not
|
|
94
|
+
* switched guest checkout on, admits buyers only after approval, or hides
|
|
95
|
+
* prices from anonymous visitors. Public `GET /store` reports the same verdict
|
|
96
|
+
* as `ecommerce.config.guestCheckoutAvailable`.
|
|
97
|
+
* - GUEST_CHECKOUT_SIGNED_IN (409): the request carried an `Authorization`
|
|
98
|
+
* header. A signed-in buyer is priced and policed as their account, so they
|
|
99
|
+
* use `/basket`; nothing was read or written.
|
|
100
|
+
*
|
|
101
|
+
* A token-holding guest write must send the `version` it read; a stale one is
|
|
102
|
+
* `409 CART_CHANGED_SINCE_CONFIRM` with `data: { currentCartVersion }` (the
|
|
103
|
+
* storefront checkout's code) and writes nothing.
|
|
104
|
+
*/
|
|
105
|
+
exports.GUEST_CART_ERROR_CODES = [
|
|
106
|
+
'GUEST_CART_TOKEN_INVALID',
|
|
107
|
+
'GUEST_CHECKOUT_DISABLED',
|
|
108
|
+
'GUEST_CHECKOUT_SIGNED_IN',
|
|
109
|
+
];
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.STOREFRONT_PREVIEW_PROTOCOL_ERRORS = exports.STOREFRONT_PREVIEW_READ_OPERATIONS = exports.STOREFRONT_DESIGN_AUDIENCES = exports.STOREFRONT_BENEFIT_ICONS = exports.STOREFRONT_RESERVED_SLUGS = exports.STOREFRONT_CONTACT_FIELDS = exports.STOREFRONT_PROTECTED_BUILTIN_DESTINATIONS = exports.STOREFRONT_BUILTIN_DESTINATIONS = exports.STOREFRONT_REPEATABLE_SECTION_TYPES = exports.STOREFRONT_SECTION_TYPES = exports.isStorefrontPageSlug = exports.STOREFRONT_PAGE_SLUG_RE = exports.STOREFRONT_DESIGN_LIMITS = exports.STOREFRONT_SOCIAL_HOSTS = void 0;
|
|
4
4
|
exports.createStorefrontDesignV1 = createStorefrontDesignV1;
|
|
5
5
|
exports.parseStorefrontPreviewDocument = parseStorefrontPreviewDocument;
|
|
6
|
+
exports.matchesStorefrontVisibility = matchesStorefrontVisibility;
|
|
7
|
+
exports.projectStorefrontDocumentForAudience = projectStorefrontDocumentForAudience;
|
|
6
8
|
exports.parseStorefrontPreviewMessage = parseStorefrontPreviewMessage;
|
|
7
9
|
exports.acceptStorefrontPreviewMessage = acceptStorefrontPreviewMessage;
|
|
8
10
|
/** The social kinds a contact may link, each to its own hosts; a contact links at most one per kind. */
|
|
@@ -253,7 +255,7 @@ function previewWithinBytes(value, maximum) {
|
|
|
253
255
|
/**
|
|
254
256
|
* A `visibility` rule kept on the transport only when well-formed: known audiences, and integer
|
|
255
257
|
* price lists within the store cap. Anything else is left off rather than failing the frame — the
|
|
256
|
-
*
|
|
258
|
+
* parser never filters by it, it only carries the rule the editor authored.
|
|
257
259
|
*/
|
|
258
260
|
function previewVisibility(input) {
|
|
259
261
|
if (!previewRecord(input) || !Array.isArray(input.audiences))
|
|
@@ -362,6 +364,102 @@ function previewAnnouncement(input) {
|
|
|
362
364
|
const { visibility, ...rest } = input;
|
|
363
365
|
return { ...rest, ...previewVisibility(visibility) };
|
|
364
366
|
}
|
|
367
|
+
function audienceNamed(audiences, audience) {
|
|
368
|
+
return audiences.some((entry) => entry === audience);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* The one audience rule: whether `caller` sees an item with this `visibility`. Absent means
|
|
372
|
+
* everyone. Otherwise any listed audience that matches, or the caller's list in `priceLists`:
|
|
373
|
+
* `guest` matches only a guest; `customer` any customer; `retail` / `wholesale` a customer of that
|
|
374
|
+
* class. A rule naming a list deleted since it was authored matches no one through it.
|
|
375
|
+
*/
|
|
376
|
+
function matchesStorefrontVisibility(visibility, caller) {
|
|
377
|
+
if (!visibility)
|
|
378
|
+
return true;
|
|
379
|
+
const audiences = Array.isArray(visibility.audiences) ? visibility.audiences : [];
|
|
380
|
+
if (caller.kind === 'guest')
|
|
381
|
+
return audienceNamed(audiences, 'guest');
|
|
382
|
+
if (audienceNamed(audiences, 'customer') || audienceNamed(audiences, caller.audienceClass))
|
|
383
|
+
return true;
|
|
384
|
+
return Array.isArray(visibility.priceLists) && visibility.priceLists.some((id) => id === caller.priceList);
|
|
385
|
+
}
|
|
386
|
+
const isAudienceNode = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
387
|
+
const audienceNonEmpty = (value) => Array.isArray(value) && value.length > 0;
|
|
388
|
+
function copyAudienceValue(value) {
|
|
389
|
+
if (Array.isArray(value))
|
|
390
|
+
return value.map(copyAudienceValue);
|
|
391
|
+
if (!isAudienceNode(value))
|
|
392
|
+
return value;
|
|
393
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, copyAudienceValue(entry)]));
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Filters `value` for `caller` into a NEW tree, or `undefined` when it goes. Every `visibility` key
|
|
397
|
+
* is dropped; an item kept because its rule matched gains `audienceScoped: true`. As a published
|
|
398
|
+
* reader drops a link whose target vanished, a link (or a section's action) to a page this caller
|
|
399
|
+
* does not see goes whole. A menu entry left with neither children nor a target, and a footer group
|
|
400
|
+
* left with no links, go too: they would render as dead labels.
|
|
401
|
+
*/
|
|
402
|
+
function scopeAudienceValue(value, caller, pages) {
|
|
403
|
+
if (Array.isArray(value))
|
|
404
|
+
return scopeAudienceArray(value, caller, pages);
|
|
405
|
+
if (!isAudienceNode(value))
|
|
406
|
+
return value;
|
|
407
|
+
if (value.kind === 'page')
|
|
408
|
+
return pages.has(String(value.id)) ? copyAudienceValue(value) : undefined;
|
|
409
|
+
const visibility = value.visibility;
|
|
410
|
+
if (!matchesStorefrontVisibility(visibility, caller))
|
|
411
|
+
return undefined;
|
|
412
|
+
const result = Object.fromEntries(scopedAudienceEntries(value, visibility, caller, pages));
|
|
413
|
+
return keepsAfterAudienceFilter(value, result) ? result : undefined;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* A kept node's entries, filtered. Built as entries for `Object.fromEntries`, never by assignment:
|
|
417
|
+
* an own `__proto__` key stays a data key and never becomes the result's prototype. A rule-matched
|
|
418
|
+
* item's flag keeps its place, or comes last.
|
|
419
|
+
*/
|
|
420
|
+
function scopedAudienceEntries(value, visibility, caller, pages) {
|
|
421
|
+
const entries = [];
|
|
422
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
423
|
+
if (key === 'visibility')
|
|
424
|
+
continue;
|
|
425
|
+
const scoped = key === 'audienceScoped' && visibility ? true : scopeAudienceValue(entry, caller, pages);
|
|
426
|
+
if (scoped !== undefined)
|
|
427
|
+
entries.push([key, scoped]);
|
|
428
|
+
}
|
|
429
|
+
if (visibility && !Object.hasOwn(value, 'audienceScoped'))
|
|
430
|
+
entries.push(['audienceScoped', true]);
|
|
431
|
+
return entries;
|
|
432
|
+
}
|
|
433
|
+
function scopeAudienceArray(items, caller, pages) {
|
|
434
|
+
return items.flatMap((item) => {
|
|
435
|
+
const scoped = scopeAudienceValue(item, caller, pages);
|
|
436
|
+
return scoped === undefined ? [] : [scoped];
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/** Whether a node survives once its own parts are filtered, judged against what it had before. */
|
|
440
|
+
function keepsAfterAudienceFilter(had, result) {
|
|
441
|
+
if (had.target !== undefined && result.target === undefined)
|
|
442
|
+
return false;
|
|
443
|
+
if (audienceNonEmpty(had.children) && !audienceNonEmpty(result.children) && result.target === undefined)
|
|
444
|
+
return false;
|
|
445
|
+
return !(audienceNonEmpty(had.links) && !audienceNonEmpty(result.links));
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* A storefront design document as `caller` may see it, built as a NEW tree (the input is never
|
|
449
|
+
* changed or shared): drops every section, page, navigation entry (top level or child), footer link
|
|
450
|
+
* and announcement bar `caller` does not match, and every link to a page it does not see; strips
|
|
451
|
+
* every `visibility`; marks an item kept through a rule `audienceScoped: true`. Web `GET /store`
|
|
452
|
+
* applies it to a publication, the preview to its draft for a grant fixture's caller, and a preview
|
|
453
|
+
* frame may apply it to a posted document with the caller its design read echoed.
|
|
454
|
+
*/
|
|
455
|
+
function projectStorefrontDocumentForAudience(document, caller) {
|
|
456
|
+
const root = document;
|
|
457
|
+
const pages = new Set((Array.isArray(root.pages) ? root.pages : [])
|
|
458
|
+
.filter(isAudienceNode)
|
|
459
|
+
.filter((page) => matchesStorefrontVisibility(page.visibility, caller))
|
|
460
|
+
.map((page) => String(page.id)));
|
|
461
|
+
return (scopeAudienceValue(root, caller, pages) ?? {});
|
|
462
|
+
}
|
|
365
463
|
/** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
|
|
366
464
|
exports.STOREFRONT_PREVIEW_PROTOCOL_ERRORS = [
|
|
367
465
|
'STOREFRONT_PREVIEW_EXPIRED',
|
package/dist/store.d.ts
CHANGED
|
@@ -158,6 +158,25 @@ declare global {
|
|
|
158
158
|
* frame), so the storefront can offer the request.
|
|
159
159
|
*/
|
|
160
160
|
wholesaleRequests?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Whether the shop sells to shoppers without an account (server guest
|
|
163
|
+
* cart, guest checkout). Absent means OFF: a shop opts in. Honoured only
|
|
164
|
+
* while `registrationMode` is not `'approval'` and `showDefaultPriceList`
|
|
165
|
+
* is not `0` — a shop that vets every buyer, or hides prices from
|
|
166
|
+
* anonymous visitors, never sells to a guest whatever this says.
|
|
167
|
+
* Written by `PATCH /store` (ADMIN) and `PUT /platform/stores/{storeId}`
|
|
168
|
+
* (MANAGER). Never sent to shoppers as stored: they get
|
|
169
|
+
* `guestCheckoutAvailable`.
|
|
170
|
+
*/
|
|
171
|
+
guestCheckout?: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* SHOPPER-ONLY, never stored: present on web `GET /store` and the
|
|
174
|
+
* customer `stores` frame, `true` when the shop accepts guest carts and
|
|
175
|
+
* guest checkout right now — `guestCheckout` on AND registration open AND
|
|
176
|
+
* prices shown to anonymous visitors. The guest routes refuse
|
|
177
|
+
* `403 GUEST_CHECKOUT_DISABLED` whenever this is `false`.
|
|
178
|
+
*/
|
|
179
|
+
guestCheckoutAvailable?: boolean;
|
|
161
180
|
/**
|
|
162
181
|
* How long a storefront online payment holds the order, in whole
|
|
163
182
|
* minutes: the payment attempt's `expiresAt` and its Checkout Pro
|
|
@@ -268,12 +268,25 @@ declare global {
|
|
|
268
268
|
* so the reference and identity walkers pass over it.
|
|
269
269
|
*
|
|
270
270
|
* Web `GET /store` filters by it and strips it, marking each item visible through a rule
|
|
271
|
-
* `audienceScoped: true`. The private preview
|
|
271
|
+
* `audienceScoped: true`. The private preview carries it as authored, unless its grant names a
|
|
272
|
+
* fixture: then it is filtered exactly as web `GET /store` filters it for that fixture's caller.
|
|
272
273
|
*/
|
|
273
274
|
interface StorefrontDesignVisibility {
|
|
274
275
|
audiences: StorefrontDesignAudience[];
|
|
275
276
|
priceLists?: number[];
|
|
276
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Who reads a storefront design, as {@link matchesStorefrontVisibility} judges a `visibility`
|
|
280
|
+
* rule: no approved customer (a guest, a pending or refused registrant included), or one approved
|
|
281
|
+
* customer with its effective list and that list's `audienceClass` (absent meaning `retail`).
|
|
282
|
+
*/
|
|
283
|
+
type StorefrontAudienceCaller = {
|
|
284
|
+
kind: 'guest';
|
|
285
|
+
} | {
|
|
286
|
+
kind: 'customer';
|
|
287
|
+
priceList: number;
|
|
288
|
+
audienceClass: 'retail' | 'wholesale';
|
|
289
|
+
};
|
|
277
290
|
/**
|
|
278
291
|
* The one announcement bar a design carries above every page, never a second. `text` and
|
|
279
292
|
* `subtitle` are plain text (no control characters; renderers escape markup); `text` is required
|
|
@@ -288,7 +301,7 @@ declare global {
|
|
|
288
301
|
*
|
|
289
302
|
* Every projection drops a disabled bar, as it drops a disabled section. Web `GET /store` also
|
|
290
303
|
* drops one whose `visibility` the caller does not match; the private preview carries the rule
|
|
291
|
-
* as authored.
|
|
304
|
+
* as authored, unless its grant names a fixture, when it drops the bar as web `GET /store` would.
|
|
292
305
|
*/
|
|
293
306
|
interface StorefrontDesignAnnouncement {
|
|
294
307
|
id: string;
|
|
@@ -414,7 +427,8 @@ declare global {
|
|
|
414
427
|
* An item that can carry `visibility` (section, page, navigation entry, footer link, the
|
|
415
428
|
* announcement bar) may also
|
|
416
429
|
* carry `audienceScoped: true`: web `GET /store` sets it on an item it returned because the
|
|
417
|
-
* caller matched that item's rule, and strips every `visibility`. The preview
|
|
430
|
+
* caller matched that item's rule, and strips every `visibility`. The preview sets it only when
|
|
431
|
+
* its grant names a fixture, the same way.
|
|
418
432
|
*/
|
|
419
433
|
type PublicStorefrontDesignValue<T> = T extends StorefrontDesignImagePlacement ? T & {
|
|
420
434
|
url: string;
|
|
@@ -528,7 +542,27 @@ declare global {
|
|
|
528
542
|
type StorefrontDesignDiscardInput = StorefrontDesignRevisionInput;
|
|
529
543
|
interface StorefrontDesignPreviewInput {
|
|
530
544
|
draftRevision: string;
|
|
545
|
+
/** Absent: the unfiltered preview at the default list. */
|
|
546
|
+
fixture?: StorefrontPreviewFixture;
|
|
531
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* The audience a preview grant renders for — synthetic, never a real customer: no customer row,
|
|
550
|
+
* cart, order or balance is read. `guest` is a visitor without an account. `priceList` is an
|
|
551
|
+
* approved customer on that list of the store (its current `audienceClass` decides `retail` /
|
|
552
|
+
* `wholesale` rules), and every catalogue read prices at it; a list the store does not have is
|
|
553
|
+
* refused on issue, 400 `STOREFRONT_DESIGN_INVALID` with `UNKNOWN_PRICE_LIST` at
|
|
554
|
+
* `fixture.priceList`, and a list deleted after issue reads as a customer on it would (PUBLICO,
|
|
555
|
+
* list 1, with its class). `pending` is a registrant awaiting approval: the api answers it exactly as `guest`
|
|
556
|
+
* (content, methods and prices); the storefront may still show its in-review notices.
|
|
557
|
+
*/
|
|
558
|
+
type StorefrontPreviewFixture = {
|
|
559
|
+
kind: 'guest';
|
|
560
|
+
} | {
|
|
561
|
+
kind: 'priceList';
|
|
562
|
+
priceList: number;
|
|
563
|
+
} | {
|
|
564
|
+
kind: 'pending';
|
|
565
|
+
};
|
|
532
566
|
/**
|
|
533
567
|
* GET draft answers a baseline built without copying the legacy logo, while POST init copies it,
|
|
534
568
|
* so `document.branding.logo` can differ between them: clients rebase onto init's document.
|
|
@@ -649,11 +683,20 @@ declare global {
|
|
|
649
683
|
grantId: string;
|
|
650
684
|
iat: number;
|
|
651
685
|
exp: number;
|
|
686
|
+
/** The issue body's fixture, signed in, so only the ADMIN-gated issue route can change it. */
|
|
687
|
+
fixture?: StorefrontPreviewFixture;
|
|
652
688
|
}
|
|
653
689
|
type StorefrontPreviewOperation = (typeof STOREFRONT_PREVIEW_READ_OPERATIONS)[number];
|
|
654
690
|
/** GET query fields use their serialized string representation. Store scope comes only from the grant. */
|
|
655
691
|
type StorefrontPreviewReadQuery = {
|
|
656
|
-
operation: 'design'
|
|
692
|
+
operation: 'design';
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* The public method rows. With a grant fixture, only the rows that caller may use, as web
|
|
696
|
+
* `GET /store` and checkout judge a row's `availability`; without one, every row.
|
|
697
|
+
*/
|
|
698
|
+
| {
|
|
699
|
+
operation: 'paymentMethods' | 'deliveryMethods';
|
|
657
700
|
}
|
|
658
701
|
/**
|
|
659
702
|
* The shop's public web `GET /store` frame — the same allow-listed STORE
|
|
@@ -679,7 +722,8 @@ declare global {
|
|
|
679
722
|
}
|
|
680
723
|
/**
|
|
681
724
|
* `bestSellers` is web `GET /products?mode=best-sellers`'s read and answer, priced as a
|
|
682
|
-
* guest
|
|
725
|
+
* guest, or at a `priceList` fixture's list as every catalogue operation is.
|
|
726
|
+
* `recentlyBought` answers `{ data: [] }`: a preview has no buyer to read for.
|
|
683
727
|
*/
|
|
684
728
|
| {
|
|
685
729
|
operation: 'bestSellers' | 'recentlyBought';
|
|
@@ -769,6 +813,11 @@ declare global {
|
|
|
769
813
|
interface StorefrontPreviewDesignResult {
|
|
770
814
|
mode: 'preview';
|
|
771
815
|
draftRevision: string;
|
|
816
|
+
/**
|
|
817
|
+
* Without a grant fixture, the draft with every `visibility` as authored. With one, the
|
|
818
|
+
* draft filtered for {@link StorefrontPreviewDesignResult.audience} by
|
|
819
|
+
* {@link projectStorefrontDocumentForAudience}, as web `GET /store` filters a publication.
|
|
820
|
+
*/
|
|
772
821
|
document: PublicStorefrontDesign;
|
|
773
822
|
literalLayers: {
|
|
774
823
|
scope: LiteralScope;
|
|
@@ -776,12 +825,19 @@ declare global {
|
|
|
776
825
|
}[];
|
|
777
826
|
effectiveSettings: StorefrontDesignSettings;
|
|
778
827
|
defaultsVersion: string;
|
|
828
|
+
/**
|
|
829
|
+
* The caller the grant's fixture resolved to, present only when the grant names one: filter a
|
|
830
|
+
* posted `StorefrontPreviewDocument` with this same caller and the same walker.
|
|
831
|
+
*/
|
|
832
|
+
audience?: StorefrontAudienceCaller;
|
|
779
833
|
}
|
|
780
834
|
/**
|
|
781
835
|
* `operation=store`'s `data`: the fields web `GET /store` builds from the STORE
|
|
782
836
|
* row, built by the same projector, and `storefrontDesign` from the pinned
|
|
783
837
|
* draft. The contact is the draft's (`publicContact`, plus `email`/`phone`
|
|
784
|
-
* derived from it), as an adopted shop's comes from its publication.
|
|
838
|
+
* derived from it), as an adopted shop's comes from its publication. With a grant
|
|
839
|
+
* fixture, its method rows are only those that caller may use and its design is
|
|
840
|
+
* filtered for that caller, as web `GET /store` answers that caller.
|
|
785
841
|
*
|
|
786
842
|
* Never carries what only an open shop's public route knows — `publicStorefront`,
|
|
787
843
|
* `maintenance` and `globals` — and never answers `STOREFRONT_UNAVAILABLE`: a
|
|
@@ -796,7 +852,11 @@ declare global {
|
|
|
796
852
|
type: StorefrontDesignSectionType;
|
|
797
853
|
enabled: boolean;
|
|
798
854
|
content: Record<string, unknown>;
|
|
799
|
-
/**
|
|
855
|
+
/**
|
|
856
|
+
* Carried as authored when well-formed, as navigation and footer entries carry theirs; never
|
|
857
|
+
* applied by the parser. A frame whose design read echoed an `audience` should apply it
|
|
858
|
+
* with {@link projectStorefrontDocumentForAudience}.
|
|
859
|
+
*/
|
|
800
860
|
visibility?: StorefrontDesignVisibility;
|
|
801
861
|
}
|
|
802
862
|
interface StorefrontPreviewDocument {
|
|
@@ -824,7 +884,7 @@ declare global {
|
|
|
824
884
|
contact: Record<string, unknown>;
|
|
825
885
|
seo: object;
|
|
826
886
|
literalOverrides: Record<string, unknown>;
|
|
827
|
-
/** Carried as sent when it is an object, its `visibility` kept only when well-formed; never applied. */
|
|
887
|
+
/** Carried as sent when it is an object, its `visibility` kept only when well-formed; never applied by the parser. */
|
|
828
888
|
announcement?: object;
|
|
829
889
|
}
|
|
830
890
|
type StorefrontPreviewProtocolError = (typeof STOREFRONT_PREVIEW_PROTOCOL_ERRORS)[number];
|
|
@@ -931,6 +991,22 @@ declare global {
|
|
|
931
991
|
export declare const STOREFRONT_PREVIEW_READ_OPERATIONS: readonly ["design", "store", "product", "productBySlug", "productsByIds", "bestSellers", "recentlyBought", "products", "incomes", "group", "related", "categories", "brands", "tags", "collection", "paymentMethods", "deliveryMethods", "asset"];
|
|
932
992
|
/** Checks transport structure and UTF-8 size; unfamiliar section types are skipped, never rendered. */
|
|
933
993
|
export declare function parseStorefrontPreviewDocument(input: unknown): StorefrontPreviewDocument | undefined;
|
|
994
|
+
/**
|
|
995
|
+
* The one audience rule: whether `caller` sees an item with this `visibility`. Absent means
|
|
996
|
+
* everyone. Otherwise any listed audience that matches, or the caller's list in `priceLists`:
|
|
997
|
+
* `guest` matches only a guest; `customer` any customer; `retail` / `wholesale` a customer of that
|
|
998
|
+
* class. A rule naming a list deleted since it was authored matches no one through it.
|
|
999
|
+
*/
|
|
1000
|
+
export declare function matchesStorefrontVisibility(visibility: StorefrontDesignVisibility | undefined, caller: StorefrontAudienceCaller): boolean;
|
|
1001
|
+
/**
|
|
1002
|
+
* A storefront design document as `caller` may see it, built as a NEW tree (the input is never
|
|
1003
|
+
* changed or shared): drops every section, page, navigation entry (top level or child), footer link
|
|
1004
|
+
* and announcement bar `caller` does not match, and every link to a page it does not see; strips
|
|
1005
|
+
* every `visibility`; marks an item kept through a rule `audienceScoped: true`. Web `GET /store`
|
|
1006
|
+
* applies it to a publication, the preview to its draft for a grant fixture's caller, and a preview
|
|
1007
|
+
* frame may apply it to a posted document with the caller its design read echoed.
|
|
1008
|
+
*/
|
|
1009
|
+
export declare function projectStorefrontDocumentForAudience<T extends object>(document: T, caller: StorefrontAudienceCaller): T;
|
|
934
1010
|
/** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
|
|
935
1011
|
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"];
|
|
936
1012
|
/** Does not authenticate the grant. Keep it in memory and post only to the exact configured target origin. */
|
package/dist/storefrontDesign.js
CHANGED
|
@@ -245,7 +245,7 @@ function previewWithinBytes(value, maximum) {
|
|
|
245
245
|
/**
|
|
246
246
|
* A `visibility` rule kept on the transport only when well-formed: known audiences, and integer
|
|
247
247
|
* price lists within the store cap. Anything else is left off rather than failing the frame — the
|
|
248
|
-
*
|
|
248
|
+
* parser never filters by it, it only carries the rule the editor authored.
|
|
249
249
|
*/
|
|
250
250
|
function previewVisibility(input) {
|
|
251
251
|
if (!previewRecord(input) || !Array.isArray(input.audiences))
|
|
@@ -354,6 +354,102 @@ function previewAnnouncement(input) {
|
|
|
354
354
|
const { visibility, ...rest } = input;
|
|
355
355
|
return { ...rest, ...previewVisibility(visibility) };
|
|
356
356
|
}
|
|
357
|
+
function audienceNamed(audiences, audience) {
|
|
358
|
+
return audiences.some((entry) => entry === audience);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The one audience rule: whether `caller` sees an item with this `visibility`. Absent means
|
|
362
|
+
* everyone. Otherwise any listed audience that matches, or the caller's list in `priceLists`:
|
|
363
|
+
* `guest` matches only a guest; `customer` any customer; `retail` / `wholesale` a customer of that
|
|
364
|
+
* class. A rule naming a list deleted since it was authored matches no one through it.
|
|
365
|
+
*/
|
|
366
|
+
export function matchesStorefrontVisibility(visibility, caller) {
|
|
367
|
+
if (!visibility)
|
|
368
|
+
return true;
|
|
369
|
+
const audiences = Array.isArray(visibility.audiences) ? visibility.audiences : [];
|
|
370
|
+
if (caller.kind === 'guest')
|
|
371
|
+
return audienceNamed(audiences, 'guest');
|
|
372
|
+
if (audienceNamed(audiences, 'customer') || audienceNamed(audiences, caller.audienceClass))
|
|
373
|
+
return true;
|
|
374
|
+
return Array.isArray(visibility.priceLists) && visibility.priceLists.some((id) => id === caller.priceList);
|
|
375
|
+
}
|
|
376
|
+
const isAudienceNode = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
377
|
+
const audienceNonEmpty = (value) => Array.isArray(value) && value.length > 0;
|
|
378
|
+
function copyAudienceValue(value) {
|
|
379
|
+
if (Array.isArray(value))
|
|
380
|
+
return value.map(copyAudienceValue);
|
|
381
|
+
if (!isAudienceNode(value))
|
|
382
|
+
return value;
|
|
383
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, copyAudienceValue(entry)]));
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Filters `value` for `caller` into a NEW tree, or `undefined` when it goes. Every `visibility` key
|
|
387
|
+
* is dropped; an item kept because its rule matched gains `audienceScoped: true`. As a published
|
|
388
|
+
* reader drops a link whose target vanished, a link (or a section's action) to a page this caller
|
|
389
|
+
* does not see goes whole. A menu entry left with neither children nor a target, and a footer group
|
|
390
|
+
* left with no links, go too: they would render as dead labels.
|
|
391
|
+
*/
|
|
392
|
+
function scopeAudienceValue(value, caller, pages) {
|
|
393
|
+
if (Array.isArray(value))
|
|
394
|
+
return scopeAudienceArray(value, caller, pages);
|
|
395
|
+
if (!isAudienceNode(value))
|
|
396
|
+
return value;
|
|
397
|
+
if (value.kind === 'page')
|
|
398
|
+
return pages.has(String(value.id)) ? copyAudienceValue(value) : undefined;
|
|
399
|
+
const visibility = value.visibility;
|
|
400
|
+
if (!matchesStorefrontVisibility(visibility, caller))
|
|
401
|
+
return undefined;
|
|
402
|
+
const result = Object.fromEntries(scopedAudienceEntries(value, visibility, caller, pages));
|
|
403
|
+
return keepsAfterAudienceFilter(value, result) ? result : undefined;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* A kept node's entries, filtered. Built as entries for `Object.fromEntries`, never by assignment:
|
|
407
|
+
* an own `__proto__` key stays a data key and never becomes the result's prototype. A rule-matched
|
|
408
|
+
* item's flag keeps its place, or comes last.
|
|
409
|
+
*/
|
|
410
|
+
function scopedAudienceEntries(value, visibility, caller, pages) {
|
|
411
|
+
const entries = [];
|
|
412
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
413
|
+
if (key === 'visibility')
|
|
414
|
+
continue;
|
|
415
|
+
const scoped = key === 'audienceScoped' && visibility ? true : scopeAudienceValue(entry, caller, pages);
|
|
416
|
+
if (scoped !== undefined)
|
|
417
|
+
entries.push([key, scoped]);
|
|
418
|
+
}
|
|
419
|
+
if (visibility && !Object.hasOwn(value, 'audienceScoped'))
|
|
420
|
+
entries.push(['audienceScoped', true]);
|
|
421
|
+
return entries;
|
|
422
|
+
}
|
|
423
|
+
function scopeAudienceArray(items, caller, pages) {
|
|
424
|
+
return items.flatMap((item) => {
|
|
425
|
+
const scoped = scopeAudienceValue(item, caller, pages);
|
|
426
|
+
return scoped === undefined ? [] : [scoped];
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/** Whether a node survives once its own parts are filtered, judged against what it had before. */
|
|
430
|
+
function keepsAfterAudienceFilter(had, result) {
|
|
431
|
+
if (had.target !== undefined && result.target === undefined)
|
|
432
|
+
return false;
|
|
433
|
+
if (audienceNonEmpty(had.children) && !audienceNonEmpty(result.children) && result.target === undefined)
|
|
434
|
+
return false;
|
|
435
|
+
return !(audienceNonEmpty(had.links) && !audienceNonEmpty(result.links));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* A storefront design document as `caller` may see it, built as a NEW tree (the input is never
|
|
439
|
+
* changed or shared): drops every section, page, navigation entry (top level or child), footer link
|
|
440
|
+
* and announcement bar `caller` does not match, and every link to a page it does not see; strips
|
|
441
|
+
* every `visibility`; marks an item kept through a rule `audienceScoped: true`. Web `GET /store`
|
|
442
|
+
* applies it to a publication, the preview to its draft for a grant fixture's caller, and a preview
|
|
443
|
+
* frame may apply it to a posted document with the caller its design read echoed.
|
|
444
|
+
*/
|
|
445
|
+
export function projectStorefrontDocumentForAudience(document, caller) {
|
|
446
|
+
const root = document;
|
|
447
|
+
const pages = new Set((Array.isArray(root.pages) ? root.pages : [])
|
|
448
|
+
.filter(isAudienceNode)
|
|
449
|
+
.filter((page) => matchesStorefrontVisibility(page.visibility, caller))
|
|
450
|
+
.map((page) => String(page.id)));
|
|
451
|
+
return (scopeAudienceValue(root, caller, pages) ?? {});
|
|
452
|
+
}
|
|
357
453
|
/** The codes a preview frame may report in an `error` message; the parser accepts exactly these. */
|
|
358
454
|
export const STOREFRONT_PREVIEW_PROTOCOL_ERRORS = [
|
|
359
455
|
'STOREFRONT_PREVIEW_EXPIRED',
|