@tokenoftrust/storefront-runner 4.7.10 → 4.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/apps/storefront/astro.config.mjs +2 -1
  2. package/apps/storefront/dev-plugins/dev-auth-outbox.mjs +181 -0
  3. package/apps/storefront/src/components/content/storefront/blog/PostGrid.astro +8 -4
  4. package/apps/storefront/src/lib/access/auth/devAuth.ts +34 -2
  5. package/apps/storefront/src/lib/access/auth/route.ts +36 -2
  6. package/apps/storefront/src/lib/access/auth/totAccessClient.ts +6 -18
  7. package/apps/storefront/src/lib/access/dashboard/staffStoreEntry.ts +2 -1
  8. package/apps/storefront/src/lib/access/dashboard/staffVendorAccess.ts +6 -10
  9. package/apps/storefront/src/lib/access/shared/accessExplain.ts +106 -28
  10. package/apps/storefront/src/lib/access/shared/developerInvites.ts +40 -38
  11. package/apps/storefront/src/lib/access/shared/invitePreflight.ts +1 -1
  12. package/apps/storefront/src/lib/access/shared/principalGrants.ts +16 -17
  13. package/apps/storefront/src/lib/content/shared/homeVisualParity.ts +69 -0
  14. package/apps/storefront/src/lib/content/storyblok/content-model.ts +21 -0
  15. package/apps/storefront/src/lib/engagement/messaging/messagesClient.ts +13 -7
  16. package/apps/storefront/src/lib/observability/diag/failureReason.ts +37 -0
  17. package/apps/storefront/src/lib/observability/diag/redact.ts +11 -14
  18. package/apps/storefront/src/lib/orders/admin/orderSource.ts +26 -5
  19. package/apps/storefront/src/lib/orders/admin/services/orders.ts +121 -22
  20. package/apps/storefront/src/lib/orders/admin/viewmodels/orders.ts +7 -0
  21. package/apps/storefront/src/lib/orders/fixtures/orders.ts +2 -0
  22. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +4 -7
  23. package/apps/storefront/src/pages/api/auth/send.ts +12 -12
  24. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +2 -1
  25. package/apps/storefront/src/pages/api/auth/verify.ts +7 -3
  26. package/apps/storefront/src/pages/auth/login.astro +24 -6
  27. package/apps/storefront/src/pages/auth/magic.astro +4 -7
  28. package/apps/storefront/src/pages/index.astro +43 -1
  29. package/package.json +1 -1
  30. package/packages/cli/src/declared-config.mjs +42 -1
  31. package/packages/public-runtime/src/declared-tenant-config.d.mts +4 -1
  32. package/packages/public-runtime/src/declared-tenant-config.mjs +40 -1
  33. package/packages/public-runtime/src/extension-contract-values.d.mts +4 -1
  34. package/packages/public-runtime/src/extension-contract-values.mjs +6 -1
  35. package/packages/public-runtime/src/extension-contract.ts +2 -0
  36. package/packages/public-runtime/src/tenant.ts +6 -0
  37. package/scripts/tenant/validate.mjs +17 -0
@@ -27,6 +27,7 @@ export const DECLARED_TENANT_CONFIG_FIELDS = Object.freeze([
27
27
  "embeds",
28
28
  "seo",
29
29
  "currency",
30
+ "timezone",
30
31
  "shipping",
31
32
  ]);
32
33
 
@@ -149,6 +150,42 @@ export function currencyDeclarationProblems(currency) {
149
150
  : [`\`currency\` is ${JSON.stringify(currency)} — must be an uppercase ISO 4217 code such as "USD" or "CAD"`];
150
151
  }
151
152
 
153
+ /** The IANA time zone a store's days are counted in when `.tot/config.json` declares none. */
154
+ export const DEFAULT_TIMEZONE = "UTC";
155
+
156
+ /**
157
+ * What is wrong with a declared `timezone`, one sentence; `[]` when it is valid. A valid one is
158
+ * an IANA zone name the runtime's Intl knows, such as "America/Los_Angeles" or "UTC"; a numeric
159
+ * offset is not a zone, so "today" would drift across daylight-saving changes.
160
+ *
161
+ * @param {unknown} timezone
162
+ * @returns {string[]}
163
+ */
164
+ export function timezoneDeclarationProblems(timezone) {
165
+ const named = typeof timezone === "string" && /^[A-Za-z][A-Za-z0-9_+-]*(\/[A-Za-z0-9_+-]+)*$/.test(timezone);
166
+ if (named) {
167
+ try {
168
+ new Intl.DateTimeFormat("en-US", { timeZone: /** @type {string} */ (timezone) });
169
+ return [];
170
+ } catch {
171
+ /* not a zone the runtime knows */
172
+ }
173
+ }
174
+ return [`\`timezone\` is ${JSON.stringify(timezone)} — must be an IANA time zone name such as "America/Los_Angeles" or "UTC"`];
175
+ }
176
+
177
+ /**
178
+ * The ONE place the time zone a store's days are counted in is resolved. A declared IANA zone
179
+ * wins; anything else (absent, wrong shape, unknown zone) is UTC. "Today", a daily total and an
180
+ * order's displayed time take the zone from here, never from the server's clock.
181
+ *
182
+ * @param {unknown} declared
183
+ * @returns {string}
184
+ */
185
+ export function resolveStoreTimeZone(declared) {
186
+ return timezoneDeclarationProblems(declared).length === 0 ? /** @type {string} */ (declared) : DEFAULT_TIMEZONE;
187
+ }
188
+
152
189
  const SITE_TYPES = new Set(SITE_TYPES_LIST);
153
190
 
154
191
  const isPlainObject = (value) =>
@@ -343,7 +380,7 @@ export function declaredTenantConfig(config) {
343
380
  /** @type {Record<string, unknown>} */
344
381
  const out = {};
345
382
  if (!isPlainObject(config)) return out;
346
- const { displayName, siteType, compliance, capabilities, features, embeds, seo, currency, shipping } = /** @type {Record<string, unknown>} */ (config);
383
+ const { displayName, siteType, compliance, capabilities, features, embeds, seo, currency, timezone, shipping } = /** @type {Record<string, unknown>} */ (config);
347
384
  // The store's name as visitors read it: the `<title>` suffix, og:site_name,
348
385
  // Organization JSON-LD, breadcrumbs, the default blog heading, the copyright line.
349
386
  if (typeof displayName === "string" && displayName.trim()) out.displayName = displayName.trim();
@@ -356,6 +393,8 @@ export function declaredTenantConfig(config) {
356
393
  if (seo !== undefined && seoDeclarationProblems(seo).length === 0) out.seo = seo;
357
394
  // Wrong-shaped or unknown codes are omitted: the store sells in USD (`tot validate` reports it).
358
395
  if (currency !== undefined && currencyDeclarationProblems(currency).length === 0) out.currency = currency;
396
+ // Wrong-shaped or unknown zones are omitted: the store counts its days in UTC (`tot validate` reports it).
397
+ if (timezone !== undefined && timezoneDeclarationProblems(timezone).length === 0) out.timezone = timezone;
359
398
  // The shipping charge: refused whole when any part is malformed (`tot validate` and the
360
399
  // reconcile report it), so checkout keeps charging no shipping rather than a guess.
361
400
  if (shipping !== undefined && shippingDeclarationProblems(shipping).length === 0) {
@@ -1,4 +1,4 @@
1
- export declare const BLOCK_PALETTE_CURRENT_CONTRACT: "block-palette@7";
1
+ export declare const BLOCK_PALETTE_CURRENT_CONTRACT: "block-palette@8";
2
2
  export declare const BLOCK_PALETTE_SUPPORTED_CONTRACTS: readonly [
3
3
  "block-palette@1",
4
4
  "block-palette@2",
@@ -7,8 +7,11 @@ export declare const BLOCK_PALETTE_SUPPORTED_CONTRACTS: readonly [
7
7
  "block-palette@5",
8
8
  "block-palette@6",
9
9
  "block-palette@7",
10
+ "block-palette@8",
10
11
  ];
11
12
  export declare const FEATURED_PRODUCT_PRESENTATIONS: readonly ["grid", "carousel"];
13
+ export declare const LATEST_POSTS_DEFAULT_LIMIT: 3;
14
+ export declare const LATEST_POSTS_MAX_LIMIT: 12;
12
15
  export declare const PROMO_TILE_PRESENTATIONS: readonly [
13
16
  "overlay",
14
17
  "portrait-cards",
@@ -5,7 +5,7 @@
5
5
  * runtime and repository Node tooling. The separately published CLI mirrors
6
6
  * these values and pins that mirror with a parity test.
7
7
  */
8
- export const BLOCK_PALETTE_CURRENT_CONTRACT = "block-palette@7";
8
+ export const BLOCK_PALETTE_CURRENT_CONTRACT = "block-palette@8";
9
9
 
10
10
  export const BLOCK_PALETTE_SUPPORTED_CONTRACTS = Object.freeze([
11
11
  "block-palette@1",
@@ -14,11 +14,16 @@ export const BLOCK_PALETTE_SUPPORTED_CONTRACTS = Object.freeze([
14
14
  "block-palette@4",
15
15
  "block-palette@5",
16
16
  "block-palette@6",
17
+ "block-palette@7",
17
18
  BLOCK_PALETTE_CURRENT_CONTRACT,
18
19
  ]);
19
20
 
20
21
  export const FEATURED_PRODUCT_PRESENTATIONS = Object.freeze(["grid", "carousel"]);
21
22
 
23
+ /** `latest_posts` card count: the default when `limit` is absent, and the most a block may show. */
24
+ export const LATEST_POSTS_DEFAULT_LIMIT = 3;
25
+ export const LATEST_POSTS_MAX_LIMIT = 12;
26
+
22
27
  export const PROMO_TILE_PRESENTATIONS = Object.freeze([
23
28
  "overlay",
24
29
  "portrait-cards",
@@ -15,6 +15,8 @@ export {
15
15
  BLOCK_PALETTE_CURRENT_CONTRACT,
16
16
  BLOCK_PALETTE_SUPPORTED_CONTRACTS,
17
17
  FEATURED_PRODUCT_PRESENTATIONS,
18
+ LATEST_POSTS_DEFAULT_LIMIT,
19
+ LATEST_POSTS_MAX_LIMIT,
18
20
  PROMO_TILE_PRESENTATIONS,
19
21
  THEME_HEADING_LEVELS,
20
22
  imageSourceUrlRefusal,
@@ -524,6 +524,12 @@ export interface TenantConfig {
524
524
  * (declared-tenant-config.mjs), never as a literal.
525
525
  */
526
526
  currency: string;
527
+ /**
528
+ * The IANA time zone the store's days are counted in: the Orders "Today" total and the order
529
+ * times the admin shows. UTC unless `.tot/config.json` declares another; read it through
530
+ * `resolveStoreTimeZone` (declared-tenant-config.mjs), never as a literal.
531
+ */
532
+ timezone?: string;
527
533
  locale: string;
528
534
  /** Explicit publication spaces for this tenant, including one canonical root. */
529
535
  publicationSpaces?: readonly PublicationSpaceManifestV1[];
@@ -34,6 +34,7 @@ import { listTenantDirIds, tenantDirSegments } from "../lib/tenant-dirs.mjs";
34
34
  import {
35
35
  BLOCK_PALETTE_SUPPORTED_CONTRACTS,
36
36
  FEATURED_PRODUCT_PRESENTATIONS,
37
+ LATEST_POSTS_MAX_LIMIT,
37
38
  PROMO_TILE_PRESENTATIONS,
38
39
  THEME_HEADING_LEVELS,
39
40
  isPageBlockImageReference,
@@ -364,6 +365,7 @@ const KNOWN_BLOCKS = new Set([
364
365
  "membership_band",
365
366
  "editorial",
366
367
  "newsletter",
368
+ "latest_posts",
367
369
  "marketing_hero",
368
370
  "trust_bar",
369
371
  "split_compare",
@@ -584,6 +586,20 @@ function validateEditorial(block, file, at, out) {
584
586
  }
585
587
  }
586
588
 
589
+ function validateLatestPosts(block, file, at, out) {
590
+ if (block.limit !== undefined && (!Number.isInteger(block.limit) || block.limit < 1 || block.limit > LATEST_POSTS_MAX_LIMIT)) {
591
+ out.push(mk(ERROR, "block-prop", file, `${at}.limit must be a whole number from 1 to ${LATEST_POSTS_MAX_LIMIT}`));
592
+ }
593
+ for (const prop of ["eyebrow", "title", "tag", "ctaLabel", "ctaHref"]) {
594
+ if (block[prop] !== undefined && (typeof block[prop] !== "string" || !block[prop].trim())) {
595
+ out.push(mk(ERROR, "block-prop", file, `${at}.${prop} must be a non-empty string`));
596
+ }
597
+ }
598
+ if (typeof block.ctaHref === "string" && block.ctaHref.trim() && !isSafeStorefrontHref(block.ctaHref)) {
599
+ out.push(mk(ERROR, "navigation-href", file, `${at}.ctaHref must be a same-storefront root-relative path`));
600
+ }
601
+ }
602
+
587
603
  function validateHomeDoc(doc, file, tenantId) {
588
604
  const out = [];
589
605
  if (doc == null || typeof doc !== "object" || Array.isArray(doc)) {
@@ -648,6 +664,7 @@ function validateHomeDoc(doc, file, tenantId) {
648
664
  if (component === "promo_tiles") validatePromoTiles(block, file, at, out, tenantId);
649
665
  if (component === "trust_strip") validateTrustStrip(block, file, at, out, tenantId);
650
666
  if (component === "faq") validateFaq(block, file, at, out);
667
+ if (component === "latest_posts") validateLatestPosts(block, file, at, out);
651
668
  });
652
669
  return out;
653
670
  }