@lamgig/content-schemas 1.0.0 → 1.1.0

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/README.md CHANGED
@@ -44,8 +44,35 @@ get uniform strictness):
44
44
  - Consumers MUST use the tolerant schemas and ignore unknown fields. The forward-compat tests
45
45
  in [`test/contract.test.ts`](test/contract.test.ts) enforce exactly this: the v1 tolerant
46
46
  schema accepts `full.json` plus an injected unknown optional field and an unknown module key.
47
- - All three v1 modules (`siteSettings`, `branding`, `menu`) are **required**. Empty states are
48
- valid payloads (`menu.categories: []`), never missing keys.
47
+ - The three original v1 modules (`siteSettings`, `branding`, `menu`) are **required**. Empty
48
+ states are valid payloads (`menu.categories: []`), never missing keys.
49
+ - **`commerce` (added in 1.1.0) is OPTIONAL**, and this is the compatibility rule working
50
+ rather than an inconsistency: every snapshot published before the module existed lacks the
51
+ key, and a rollback can make one of those the live payload at any time. `minimal.json` is
52
+ deliberately kept commerce-free as the pinned example of that shape. A consumer must treat
53
+ `modules.commerce` as possibly absent — `parsed.modules.commerce?.acceptsOnlineOrders`.
54
+
55
+ ## The `commerce` module (1.1.0)
56
+
57
+ Online-ordering **configuration**. LamGig does not process orders and stores no provider
58
+ credentials: everything here is a public identifier or a display value, because a published
59
+ payload is world-readable immutable JSON.
60
+
61
+ | Field | Notes |
62
+ |---|---|
63
+ | `provider` | `'none' \| 'other'` only. A published enum is a consumer-visible commitment, and no provider has been evaluated yet — do not switch on a brand name that is not in this union |
64
+ | `orderingUrl` | optional; **https only** |
65
+ | `locationRef`, `menuRef` | optional public provider identifiers |
66
+ | `currency` | ISO 4217 alpha-3, matching `menu` item prices |
67
+ | `acceptsOnlineOrders` | boolean |
68
+ | `prepTimeMinutes` | optional, 0–240 |
69
+ | `hours` | at most one entry per day, ordered `mon → sun`. A **closed** day is simply ABSENT |
70
+
71
+ **Two things a consumer must get right.** Times are `HH:MM` in the site's own
72
+ `siteSettings.timezone` and carry no offset. And `closes < opens` is **legal** — it means the
73
+ business closes after midnight — so a renderer that treats it as an error will break on a
74
+ perfectly valid payload. `fixtures/v1/full.json` pins an overnight Friday for exactly that
75
+ reason.
49
76
 
50
77
  ## Canonical serialization
51
78
 
@@ -10,6 +10,9 @@ var CURRENCY_PATTERN_SOURCE = "^[A-Z]{3}$";
10
10
  var CURRENCY_PATTERN = new RegExp(CURRENCY_PATTERN_SOURCE);
11
11
  var COUNTRY_PATTERN_SOURCE = "^[A-Z]{2}$";
12
12
  var COUNTRY_PATTERN = new RegExp(COUNTRY_PATTERN_SOURCE);
13
+ var COMMERCE_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
14
+ var TIME_24H_PATTERN_SOURCE = "^([01][0-9]|2[0-3]):[0-5][0-9]$";
15
+ var TIME_24H_PATTERN = new RegExp(TIME_24H_PATTERN_SOURCE);
13
16
  function isIanaTimeZone(tz) {
14
17
  try {
15
18
  new Intl.DateTimeFormat("en-US", { timeZone: tz });
@@ -100,18 +103,70 @@ var makeMenuSchema = (o) => o({
100
103
  });
101
104
  var menuSchemaStrict = makeMenuSchema(strictFactory);
102
105
  var menuSchema = makeMenuSchema(tolerantFactory);
106
+ var makeCommerceHoursSchema = (o) => z.array(
107
+ o({
108
+ day: z.enum(COMMERCE_DAYS),
109
+ opens: z.string().regex(TIME_24H_PATTERN),
110
+ closes: z.string().regex(TIME_24H_PATTERN)
111
+ })
112
+ ).max(7).superRefine((entries, ctx) => {
113
+ const seen = /* @__PURE__ */ new Set();
114
+ let previous = -1;
115
+ entries.forEach((entry, index) => {
116
+ if (seen.has(entry.day)) {
117
+ ctx.addIssue({
118
+ code: "custom",
119
+ path: [index, "day"],
120
+ message: `duplicate day "${entry.day}"`
121
+ });
122
+ }
123
+ seen.add(entry.day);
124
+ const position = COMMERCE_DAYS.indexOf(entry.day);
125
+ if (position < previous) {
126
+ ctx.addIssue({
127
+ code: "custom",
128
+ path: [index, "day"],
129
+ message: "hours must be ordered mon \u2192 sun"
130
+ });
131
+ }
132
+ previous = position;
133
+ });
134
+ });
135
+ var makeCommerceSchema = (o) => o({
136
+ provider: z.enum(["none", "other"]),
137
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
138
+ orderingUrl: z.url({ protocol: /^https$/ }).max(2048).optional(),
139
+ /** PUBLIC provider identifiers — never a key, token or secret. */
140
+ locationRef: z.string().min(1).max(128).optional(),
141
+ menuRef: z.string().min(1).max(128).optional(),
142
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
143
+ currency: z.string().regex(CURRENCY_PATTERN),
144
+ acceptsOnlineOrders: z.boolean(),
145
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
146
+ prepTimeMinutes: z.number().int().min(0).max(240).optional(),
147
+ hours: makeCommerceHoursSchema(o)
148
+ });
149
+ var commerceSchemaStrict = makeCommerceSchema(strictFactory);
150
+ var commerceSchema = makeCommerceSchema(tolerantFactory);
103
151
  var makeContentFileSchema = (o) => o({
104
152
  schemaVersion: z.literal(1),
105
153
  site: o({ id: z.uuid(), slug: z.string().min(1) }),
106
154
  publishedAt: z.iso.datetime(),
107
- // All three modules REQUIRED; empty states are valid module payloads
155
+ // The three v1 modules are REQUIRED; empty states are valid module payloads
108
156
  // (e.g. menu.categories: []), never missing keys. A new required module is
109
157
  // breaking (retained/rollback snapshots won't contain it) — bump
110
158
  // SCHEMA_VERSION instead.
159
+ //
160
+ // `commerce` is therefore OPTIONAL (phase-7 D-3), and that is not a
161
+ // stylistic choice: every snapshot published before the module existed
162
+ // lacks the key, and a ROLLBACK can activate one of those at any time. A
163
+ // required `commerce` would retroactively invalidate the whole publish
164
+ // history. See ARCHITECTURE.md § 5 and docs/seams/commerce.md § 1.
111
165
  modules: o({
112
166
  siteSettings: makeSiteSettingsSchema(o),
113
167
  branding: makeBrandingSchema(o),
114
- menu: makeMenuSchema(o)
168
+ menu: makeMenuSchema(o),
169
+ commerce: makeCommerceSchema(o).optional()
115
170
  })
116
171
  });
117
172
  var contentFileSchemaStrict = makeContentFileSchema(strictFactory);
@@ -172,6 +227,9 @@ export {
172
227
  CURRENCY_PATTERN,
173
228
  COUNTRY_PATTERN_SOURCE,
174
229
  COUNTRY_PATTERN,
230
+ COMMERCE_DAYS,
231
+ TIME_24H_PATTERN_SOURCE,
232
+ TIME_24H_PATTERN,
175
233
  isIanaTimeZone,
176
234
  siteSettingsSchemaStrict,
177
235
  siteSettingsSchema,
@@ -179,8 +237,10 @@ export {
179
237
  brandingSchema,
180
238
  menuSchemaStrict,
181
239
  menuSchema,
240
+ commerceSchemaStrict,
241
+ commerceSchema,
182
242
  contentFileSchemaStrict,
183
243
  contentFileSchema,
184
244
  canonicalize
185
245
  };
186
- //# sourceMappingURL=chunk-KBPOR3WL.js.map
246
+ //# sourceMappingURL=chunk-GUUCVLL3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Version of the published-JSON contract. Additive changes (new OPTIONAL fields\n * and new OPTIONAL module keys) keep this number; any required addition is\n * breaking and bumps it. The full compatibility rule lives in this package's\n * README. See PLAN.md § Published JSON delivery and phase-2.md T1.5.\n */\nexport const SCHEMA_VERSION = 1;\n\n/** Publisher-enforced ceiling on the canonical payload size (1 MiB). */\nexport const MAX_CONTENT_BYTES = 1_048_576;\n\n// ---------------------------------------------------------------------------\n// Shared value patterns — single source of truth.\n//\n// LOCALE_PATTERN_SOURCE is the EXACT string used by the `site_settings.locale`\n// DB CHECK (supabase/migrations/*_content_site_settings.sql). Contract and\n// database must never disagree (phase-2.md T1.1): v1 supports the named BCP-47\n// subset `ll` or `ll-CC` only (e.g. `en`, `en-US`); script/numeric-region forms\n// (`zh-Hant`, `es-419`) are out of scope — widening later is additive.\n// HEX_COLOR / CURRENCY are likewise mirrored by DB CHECKs on `branding.colors`\n// and `menu_items.price_currency`.\n// ---------------------------------------------------------------------------\n\nexport const LOCALE_PATTERN_SOURCE = '^[a-z]{2}(-[A-Z]{2})?$';\nexport const LOCALE_PATTERN = new RegExp(LOCALE_PATTERN_SOURCE);\n\nexport const HEX_COLOR_PATTERN_SOURCE = '^#[0-9A-Fa-f]{6}$';\nexport const HEX_COLOR_PATTERN = new RegExp(HEX_COLOR_PATTERN_SOURCE);\n\n/** ISO 4217 alpha code shape (mirrored by the `price_currency` DB CHECK). */\nexport const CURRENCY_PATTERN_SOURCE = '^[A-Z]{3}$';\nexport const CURRENCY_PATTERN = new RegExp(CURRENCY_PATTERN_SOURCE);\n\n/** ISO 3166-1 alpha-2 country code shape. */\nexport const COUNTRY_PATTERN_SOURCE = '^[A-Z]{2}$';\nexport const COUNTRY_PATTERN = new RegExp(COUNTRY_PATTERN_SOURCE);\n\n/**\n * Days of the week, in the canonical order `commerce.hours` must be sorted by.\n * The order is part of the contract: a consumer rendering opening hours reads\n * the array straight through, so a payload listing Sunday before Monday would\n * render wrong without being malformed.\n */\nexport const COMMERCE_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const;\n\n/**\n * 24-hour `HH:MM`. UNLIKE the four patterns above this one is deliberately NOT\n * mirrored by a DB CHECK: `commerce_settings.hours` is validated at the DB only\n * as \"an array of at most 7\" (see the migration header), and its element shape,\n * time syntax, day uniqueness and ordering are publish-gated here instead. A\n * schema-invalid draft may therefore exist in the table; it cannot be published.\n */\nexport const TIME_24H_PATTERN_SOURCE = '^([01][0-9]|2[0-3]):[0-5][0-9]$';\nexport const TIME_24H_PATTERN = new RegExp(TIME_24H_PATTERN_SOURCE);\n\n/** IANA time zone check via the runtime's own tz database. */\nexport function isIanaTimeZone(tz: string): boolean {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Two parse modes from ONE schema definition (phase-2.md T1):\n//\n// - producer-strict — `.strict()` everywhere; the dashboard validates its own\n// output against this before publishing. Unknown keys are defects.\n// - consumer-tolerant — unknown object keys are STRIPPED and unknown module\n// keys ignored, so a shipped consumer accepts a v1 payload that later gained\n// an optional field or a new module key (the additive changes the\n// compatibility rule promises).\n//\n// zod v4's `.strip()`/`.passthrough()` do NOT recurse into nested objects, so a\n// post-hoc transform of one tree silently leaves nested objects in the original\n// mode. Instead every object in the tree is built through an injected object\n// factory: `z.strictObject` for the strict pair, `z.object` (strips unknown\n// keys) for the tolerant pair. One definition, two instantiations.\n// ---------------------------------------------------------------------------\n\ntype ObjectFactory = <T extends z.ZodRawShape>(shape: T) => z.ZodObject<T>;\n\nconst strictFactory = z.strictObject as ObjectFactory;\nconst tolerantFactory = z.object as ObjectFactory;\n\n// --- siteSettings ----------------------------------------------------------\n\nconst makeSiteSettingsSchema = (o: ObjectFactory) =>\n o({\n siteName: z.string().min(1).max(120),\n tagline: z.string().max(200).optional(),\n contact: o({\n email: z.email().optional(),\n phone: z.string().min(1).optional(),\n address: o({\n line1: z.string().min(1),\n line2: z.string().min(1).optional(),\n city: z.string().min(1),\n region: z.string().min(1),\n postalCode: z.string().min(1),\n country: z.string().regex(COUNTRY_PATTERN),\n }).optional(),\n }),\n // Explicit known keys (NOT a record): strict rejects unknown platforms,\n // tolerant strips them — adding a platform later is additive.\n socialLinks: o({\n facebook: z.url().optional(),\n instagram: z.url().optional(),\n tiktok: z.url().optional(),\n x: z.url().optional(),\n youtube: z.url().optional(),\n yelp: z.url().optional(),\n google: z.url().optional(),\n }).optional(),\n locale: z.string().regex(LOCALE_PATTERN).default('en-US'),\n timezone: z\n .string()\n .min(1)\n .refine(isIanaTimeZone, 'must be an IANA time zone name')\n .default('America/Los_Angeles'),\n });\n\nexport const siteSettingsSchemaStrict = makeSiteSettingsSchema(strictFactory);\nexport const siteSettingsSchema = makeSiteSettingsSchema(tolerantFactory);\n\n// --- branding --------------------------------------------------------------\n\nconst makeBrandingSchema = (o: ObjectFactory) =>\n o({\n // Absolute URLs produced by the dashboard's storagePublicUrl builder\n // (apps/dashboard/src/lib/content-urls.ts). The DB stores storage PATHS;\n // the publish-time compose maps path -> URL. This package never imports\n // from the app (dependency direction, phase-2.md T1.7).\n logoUrl: z.url().optional(),\n faviconUrl: z.url().optional(),\n colors: o({\n primary: z.string().regex(HEX_COLOR_PATTERN),\n secondary: z.string().regex(HEX_COLOR_PATTERN).optional(),\n accent: z.string().regex(HEX_COLOR_PATTERN).optional(),\n background: z.string().regex(HEX_COLOR_PATTERN).optional(),\n text: z.string().regex(HEX_COLOR_PATTERN).optional(),\n }),\n fonts: o({\n heading: z.string().min(1).optional(),\n body: z.string().min(1).optional(),\n }).optional(),\n });\n\nexport const brandingSchemaStrict = makeBrandingSchema(strictFactory);\nexport const brandingSchema = makeBrandingSchema(tolerantFactory);\n\n// --- menu ------------------------------------------------------------------\n\nconst makeMenuItemSchema = (o: ObjectFactory) =>\n o({\n /** Stable DB UUID — identical across publishes (phase-2.md T1.3). */\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n price: o({\n /** Integer minor units (e.g. cents). */\n amount: z.number().int().min(0),\n currency: z.string().regex(CURRENCY_PATTERN),\n }),\n imageUrl: z.url().optional(),\n tags: z.array(z.string().min(1)).max(20),\n sortOrder: z.number().int().min(0).max(100_000),\n isAvailable: z.boolean(),\n });\n\nconst makeMenuCategorySchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n sortOrder: z.number().int().min(0).max(100_000),\n items: z.array(makeMenuItemSchema(o)),\n });\n\nconst makeMenuSchema = (o: ObjectFactory) =>\n o({\n /** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */\n categories: z.array(makeMenuCategorySchema(o)),\n });\n\nexport const menuSchemaStrict = makeMenuSchema(strictFactory);\nexport const menuSchema = makeMenuSchema(tolerantFactory);\n\n// --- commerce --------------------------------------------------------------\n//\n// Online-ordering CONFIGURATION, never orders and never provider secrets\n// (docs/seams/commerce.md § \"What does NOT go in the module\"): a published\n// payload is world-readable immutable JSON, so a credential written here would\n// be permanent and public. Everything below is a public identifier or a display\n// value.\n//\n// The `provider` enum is `none | other` ONLY (phase-7 D-9). No provider has\n// been selected, and a published enum is a consumer-visible commitment: a\n// consumer that switches on `\"square\"` would be relying on a name the seam says\n// requires an evaluation and a decision record first. Adding a member later is\n// additive for a TOLERANT consumer and breaking for a strict one, which is the\n// right way round.\n//\n// TIMES ARE LOCAL to `siteSettings.timezone` and carry no offset — the site's\n// own clock is what a customer reads on the door. `closes < opens` is LEGAL and\n// means \"closes after midnight\" (docs/seams/commerce.md § 1); a consumer must\n// not treat it as an error.\n\nconst makeCommerceHoursSchema = (o: ObjectFactory) =>\n z\n .array(\n o({\n day: z.enum(COMMERCE_DAYS),\n opens: z.string().regex(TIME_24H_PATTERN),\n closes: z.string().regex(TIME_24H_PATTERN),\n }),\n )\n .max(7)\n // At most one entry per day, listed in COMMERCE_DAYS order. The DB CHECK\n // stops at \"an array of at most 7\" (deliberately — see the migration\n // header), so THIS is the layer that makes `hours` meaningful, and it runs\n // at publish time rather than at write time.\n .superRefine((entries, ctx) => {\n const seen = new Set<string>();\n let previous = -1;\n entries.forEach((entry, index) => {\n if (seen.has(entry.day)) {\n ctx.addIssue({\n code: 'custom',\n path: [index, 'day'],\n message: `duplicate day \"${entry.day}\"`,\n });\n }\n seen.add(entry.day);\n const position = COMMERCE_DAYS.indexOf(entry.day);\n if (position < previous) {\n ctx.addIssue({\n code: 'custom',\n path: [index, 'day'],\n message: 'hours must be ordered mon → sun',\n });\n }\n previous = position;\n });\n });\n\nconst makeCommerceSchema = (o: ObjectFactory) =>\n o({\n provider: z.enum(['none', 'other']),\n /** Where a customer is sent to order. Absent when `provider` is `none`. */\n orderingUrl: z.url({ protocol: /^https$/ }).max(2048).optional(),\n /** PUBLIC provider identifiers — never a key, token or secret. */\n locationRef: z.string().min(1).max(128).optional(),\n menuRef: z.string().min(1).max(128).optional(),\n /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */\n currency: z.string().regex(CURRENCY_PATTERN),\n acceptsOnlineOrders: z.boolean(),\n /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */\n prepTimeMinutes: z.number().int().min(0).max(240).optional(),\n hours: makeCommerceHoursSchema(o),\n });\n\nexport const commerceSchemaStrict = makeCommerceSchema(strictFactory);\nexport const commerceSchema = makeCommerceSchema(tolerantFactory);\n\n// --- content file ----------------------------------------------------------\n\nconst makeContentFileSchema = (o: ObjectFactory) =>\n o({\n schemaVersion: z.literal(1),\n site: o({ id: z.uuid(), slug: z.string().min(1) }),\n publishedAt: z.iso.datetime(),\n // The three v1 modules are REQUIRED; empty states are valid module payloads\n // (e.g. menu.categories: []), never missing keys. A new required module is\n // breaking (retained/rollback snapshots won't contain it) — bump\n // SCHEMA_VERSION instead.\n //\n // `commerce` is therefore OPTIONAL (phase-7 D-3), and that is not a\n // stylistic choice: every snapshot published before the module existed\n // lacks the key, and a ROLLBACK can activate one of those at any time. A\n // required `commerce` would retroactively invalidate the whole publish\n // history. See ARCHITECTURE.md § 5 and docs/seams/commerce.md § 1.\n modules: o({\n siteSettings: makeSiteSettingsSchema(o),\n branding: makeBrandingSchema(o),\n menu: makeMenuSchema(o),\n commerce: makeCommerceSchema(o).optional(),\n }),\n });\n\n/** Producer gate: the dashboard validates its own output against this. */\nexport const contentFileSchemaStrict = makeContentFileSchema(strictFactory);\n/** Public consumer schema: tolerant of additive v1 changes. */\nexport const contentFileSchema = makeContentFileSchema(tolerantFactory);\n\nexport type SiteSettings = z.infer<typeof siteSettingsSchemaStrict>;\nexport type Branding = z.infer<typeof brandingSchemaStrict>;\nexport type Menu = z.infer<typeof menuSchemaStrict>;\nexport type MenuCategory = Menu['categories'][number];\nexport type MenuItem = MenuCategory['items'][number];\nexport type Commerce = z.infer<typeof commerceSchemaStrict>;\nexport type CommerceDay = (typeof COMMERCE_DAYS)[number];\nexport type CommerceHours = Commerce['hours'];\nexport type ContentFile = z.infer<typeof contentFileSchemaStrict>;\n\n// ---------------------------------------------------------------------------\n// Canonical serialization (phase-2.md T1.6).\n//\n// Recursively key-sorted JSON with no whitespace, produced by a hand-rolled\n// serializer rather than JSON.stringify over a re-built object — rebuilding\n// depends on JS insertion-order semantics and silently reorders integer-like\n// keys. Determinism here is what makes `hashContent` a byte-equality contract\n// shared by the publisher and every consumer test.\n//\n// Rules (documented in the README):\n// - arrays keep their order; object keys are sorted with Array.prototype.sort\n// (code-unit order);\n// - object properties whose value is `undefined` are omitted (JSON semantics);\n// - `undefined` anywhere else, non-finite numbers, bigint, functions and\n// symbols THROW — never silently coerced the way JSON.stringify would.\n// ---------------------------------------------------------------------------\n\nexport function canonicalize(payload: unknown): string {\n const out = serialize(payload);\n if (out === undefined) {\n throw new TypeError('canonicalize: payload itself serializes to nothing (undefined)');\n }\n return out;\n}\n\nfunction serialize(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (value === null) return 'null';\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return JSON.stringify(value);\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(`canonicalize: non-finite number ${String(value)}`);\n }\n return JSON.stringify(value);\n case 'object':\n break;\n default:\n throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);\n }\n if (Array.isArray(value)) {\n const parts = value.map((entry) => {\n const s = serialize(entry);\n if (s === undefined) {\n throw new TypeError('canonicalize: undefined array element');\n }\n return s;\n });\n return `[${parts.join(',')}]`;\n }\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort();\n const parts: string[] = [];\n for (const key of keys) {\n const s = serialize(record[key]);\n if (s === undefined) continue; // undefined property -> omitted key\n parts.push(`${JSON.stringify(key)}:${s}`);\n }\n return `{${parts.join(',')}}`;\n}\n"],"mappings":"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","names":["parts"]}
package/dist/hash.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  canonicalize
3
- } from "./chunk-KBPOR3WL.js";
3
+ } from "./chunk-GUUCVLL3.js";
4
4
 
5
5
  // src/hash.ts
6
6
  import { createHash } from "crypto";
package/dist/index.d.ts CHANGED
@@ -19,6 +19,22 @@ declare const CURRENCY_PATTERN: RegExp;
19
19
  /** ISO 3166-1 alpha-2 country code shape. */
20
20
  declare const COUNTRY_PATTERN_SOURCE = "^[A-Z]{2}$";
21
21
  declare const COUNTRY_PATTERN: RegExp;
22
+ /**
23
+ * Days of the week, in the canonical order `commerce.hours` must be sorted by.
24
+ * The order is part of the contract: a consumer rendering opening hours reads
25
+ * the array straight through, so a payload listing Sunday before Monday would
26
+ * render wrong without being malformed.
27
+ */
28
+ declare const COMMERCE_DAYS: readonly ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
29
+ /**
30
+ * 24-hour `HH:MM`. UNLIKE the four patterns above this one is deliberately NOT
31
+ * mirrored by a DB CHECK: `commerce_settings.hours` is validated at the DB only
32
+ * as "an array of at most 7" (see the migration header), and its element shape,
33
+ * time syntax, day uniqueness and ordering are publish-gated here instead. A
34
+ * schema-invalid draft may therefore exist in the table; it cannot be published.
35
+ */
36
+ declare const TIME_24H_PATTERN_SOURCE = "^([01][0-9]|2[0-3]):[0-5][0-9]$";
37
+ declare const TIME_24H_PATTERN: RegExp;
22
38
  /** IANA time zone check via the runtime's own tz database. */
23
39
  declare function isIanaTimeZone(tz: string): boolean;
24
40
  declare const siteSettingsSchemaStrict: z.ZodObject<{
@@ -153,6 +169,64 @@ declare const menuSchema: z.ZodObject<{
153
169
  }, z.core.$strip>>;
154
170
  }, z.core.$strip>>;
155
171
  }, z.core.$strip>;
172
+ declare const commerceSchemaStrict: z.ZodObject<{
173
+ provider: z.ZodEnum<{
174
+ none: "none";
175
+ other: "other";
176
+ }>;
177
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
178
+ orderingUrl: z.ZodOptional<z.ZodURL>;
179
+ /** PUBLIC provider identifiers — never a key, token or secret. */
180
+ locationRef: z.ZodOptional<z.ZodString>;
181
+ menuRef: z.ZodOptional<z.ZodString>;
182
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
183
+ currency: z.ZodString;
184
+ acceptsOnlineOrders: z.ZodBoolean;
185
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
186
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
187
+ hours: z.ZodArray<z.ZodObject<{
188
+ day: z.ZodEnum<{
189
+ mon: "mon";
190
+ tue: "tue";
191
+ wed: "wed";
192
+ thu: "thu";
193
+ fri: "fri";
194
+ sat: "sat";
195
+ sun: "sun";
196
+ }>;
197
+ opens: z.ZodString;
198
+ closes: z.ZodString;
199
+ }, z.core.$strip>>;
200
+ }, z.core.$strip>;
201
+ declare const commerceSchema: z.ZodObject<{
202
+ provider: z.ZodEnum<{
203
+ none: "none";
204
+ other: "other";
205
+ }>;
206
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
207
+ orderingUrl: z.ZodOptional<z.ZodURL>;
208
+ /** PUBLIC provider identifiers — never a key, token or secret. */
209
+ locationRef: z.ZodOptional<z.ZodString>;
210
+ menuRef: z.ZodOptional<z.ZodString>;
211
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
212
+ currency: z.ZodString;
213
+ acceptsOnlineOrders: z.ZodBoolean;
214
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
215
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
216
+ hours: z.ZodArray<z.ZodObject<{
217
+ day: z.ZodEnum<{
218
+ mon: "mon";
219
+ tue: "tue";
220
+ wed: "wed";
221
+ thu: "thu";
222
+ fri: "fri";
223
+ sat: "sat";
224
+ sun: "sun";
225
+ }>;
226
+ opens: z.ZodString;
227
+ closes: z.ZodString;
228
+ }, z.core.$strip>>;
229
+ }, z.core.$strip>;
156
230
  /** Producer gate: the dashboard validates its own output against this. */
157
231
  declare const contentFileSchemaStrict: z.ZodObject<{
158
232
  schemaVersion: z.ZodLiteral<1>;
@@ -228,6 +302,35 @@ declare const contentFileSchemaStrict: z.ZodObject<{
228
302
  }, z.core.$strip>>;
229
303
  }, z.core.$strip>>;
230
304
  }, z.core.$strip>;
305
+ commerce: z.ZodOptional<z.ZodObject<{
306
+ provider: z.ZodEnum<{
307
+ none: "none";
308
+ other: "other";
309
+ }>;
310
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
311
+ orderingUrl: z.ZodOptional<z.ZodURL>;
312
+ /** PUBLIC provider identifiers — never a key, token or secret. */
313
+ locationRef: z.ZodOptional<z.ZodString>;
314
+ menuRef: z.ZodOptional<z.ZodString>;
315
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
316
+ currency: z.ZodString;
317
+ acceptsOnlineOrders: z.ZodBoolean;
318
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
319
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
320
+ hours: z.ZodArray<z.ZodObject<{
321
+ day: z.ZodEnum<{
322
+ mon: "mon";
323
+ tue: "tue";
324
+ wed: "wed";
325
+ thu: "thu";
326
+ fri: "fri";
327
+ sat: "sat";
328
+ sun: "sun";
329
+ }>;
330
+ opens: z.ZodString;
331
+ closes: z.ZodString;
332
+ }, z.core.$strip>>;
333
+ }, z.core.$strip>>;
231
334
  }, z.core.$strip>;
232
335
  }, z.core.$strip>;
233
336
  /** Public consumer schema: tolerant of additive v1 changes. */
@@ -305,6 +408,35 @@ declare const contentFileSchema: z.ZodObject<{
305
408
  }, z.core.$strip>>;
306
409
  }, z.core.$strip>>;
307
410
  }, z.core.$strip>;
411
+ commerce: z.ZodOptional<z.ZodObject<{
412
+ provider: z.ZodEnum<{
413
+ none: "none";
414
+ other: "other";
415
+ }>;
416
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
417
+ orderingUrl: z.ZodOptional<z.ZodURL>;
418
+ /** PUBLIC provider identifiers — never a key, token or secret. */
419
+ locationRef: z.ZodOptional<z.ZodString>;
420
+ menuRef: z.ZodOptional<z.ZodString>;
421
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
422
+ currency: z.ZodString;
423
+ acceptsOnlineOrders: z.ZodBoolean;
424
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
425
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
426
+ hours: z.ZodArray<z.ZodObject<{
427
+ day: z.ZodEnum<{
428
+ mon: "mon";
429
+ tue: "tue";
430
+ wed: "wed";
431
+ thu: "thu";
432
+ fri: "fri";
433
+ sat: "sat";
434
+ sun: "sun";
435
+ }>;
436
+ opens: z.ZodString;
437
+ closes: z.ZodString;
438
+ }, z.core.$strip>>;
439
+ }, z.core.$strip>>;
308
440
  }, z.core.$strip>;
309
441
  }, z.core.$strip>;
310
442
  type SiteSettings = z.infer<typeof siteSettingsSchemaStrict>;
@@ -312,7 +444,10 @@ type Branding = z.infer<typeof brandingSchemaStrict>;
312
444
  type Menu = z.infer<typeof menuSchemaStrict>;
313
445
  type MenuCategory = Menu['categories'][number];
314
446
  type MenuItem = MenuCategory['items'][number];
447
+ type Commerce = z.infer<typeof commerceSchemaStrict>;
448
+ type CommerceDay = (typeof COMMERCE_DAYS)[number];
449
+ type CommerceHours = Commerce['hours'];
315
450
  type ContentFile = z.infer<typeof contentFileSchemaStrict>;
316
451
  declare function canonicalize(payload: unknown): string;
317
452
 
318
- export { type Branding, COUNTRY_PATTERN, COUNTRY_PATTERN_SOURCE, CURRENCY_PATTERN, CURRENCY_PATTERN_SOURCE, type ContentFile, HEX_COLOR_PATTERN, HEX_COLOR_PATTERN_SOURCE, LOCALE_PATTERN, LOCALE_PATTERN_SOURCE, MAX_CONTENT_BYTES, type Menu, type MenuCategory, type MenuItem, SCHEMA_VERSION, type SiteSettings, brandingSchema, brandingSchemaStrict, canonicalize, contentFileSchema, contentFileSchemaStrict, isIanaTimeZone, menuSchema, menuSchemaStrict, siteSettingsSchema, siteSettingsSchemaStrict };
453
+ export { type Branding, COMMERCE_DAYS, COUNTRY_PATTERN, COUNTRY_PATTERN_SOURCE, CURRENCY_PATTERN, CURRENCY_PATTERN_SOURCE, type Commerce, type CommerceDay, type CommerceHours, type ContentFile, HEX_COLOR_PATTERN, HEX_COLOR_PATTERN_SOURCE, LOCALE_PATTERN, LOCALE_PATTERN_SOURCE, MAX_CONTENT_BYTES, type Menu, type MenuCategory, type MenuItem, SCHEMA_VERSION, type SiteSettings, TIME_24H_PATTERN, TIME_24H_PATTERN_SOURCE, brandingSchema, brandingSchemaStrict, canonicalize, commerceSchema, commerceSchemaStrict, contentFileSchema, contentFileSchemaStrict, isIanaTimeZone, menuSchema, menuSchemaStrict, siteSettingsSchema, siteSettingsSchemaStrict };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ COMMERCE_DAYS,
2
3
  COUNTRY_PATTERN,
3
4
  COUNTRY_PATTERN_SOURCE,
4
5
  CURRENCY_PATTERN,
@@ -9,9 +10,13 @@ import {
9
10
  LOCALE_PATTERN_SOURCE,
10
11
  MAX_CONTENT_BYTES,
11
12
  SCHEMA_VERSION,
13
+ TIME_24H_PATTERN,
14
+ TIME_24H_PATTERN_SOURCE,
12
15
  brandingSchema,
13
16
  brandingSchemaStrict,
14
17
  canonicalize,
18
+ commerceSchema,
19
+ commerceSchemaStrict,
15
20
  contentFileSchema,
16
21
  contentFileSchemaStrict,
17
22
  isIanaTimeZone,
@@ -19,8 +24,9 @@ import {
19
24
  menuSchemaStrict,
20
25
  siteSettingsSchema,
21
26
  siteSettingsSchemaStrict
22
- } from "./chunk-KBPOR3WL.js";
27
+ } from "./chunk-GUUCVLL3.js";
23
28
  export {
29
+ COMMERCE_DAYS,
24
30
  COUNTRY_PATTERN,
25
31
  COUNTRY_PATTERN_SOURCE,
26
32
  CURRENCY_PATTERN,
@@ -31,9 +37,13 @@ export {
31
37
  LOCALE_PATTERN_SOURCE,
32
38
  MAX_CONTENT_BYTES,
33
39
  SCHEMA_VERSION,
40
+ TIME_24H_PATTERN,
41
+ TIME_24H_PATTERN_SOURCE,
34
42
  brandingSchema,
35
43
  brandingSchemaStrict,
36
44
  canonicalize,
45
+ commerceSchema,
46
+ commerceSchemaStrict,
37
47
  contentFileSchema,
38
48
  contentFileSchemaStrict,
39
49
  isIanaTimeZone,
@@ -126,6 +126,23 @@
126
126
  ]
127
127
  }
128
128
  ]
129
+ },
130
+ "commerce": {
131
+ "provider": "other",
132
+ "orderingUrl": "https://order.example.com/pho-palace",
133
+ "locationRef": "loc-charlotte-tryon",
134
+ "menuRef": "menu-dinner-2026",
135
+ "currency": "USD",
136
+ "acceptsOnlineOrders": true,
137
+ "prepTimeMinutes": 25,
138
+ "hours": [
139
+ { "day": "mon", "opens": "11:00", "closes": "21:00" },
140
+ { "day": "tue", "opens": "11:00", "closes": "21:00" },
141
+ { "day": "wed", "opens": "11:00", "closes": "21:00" },
142
+ { "day": "thu", "opens": "11:00", "closes": "21:00" },
143
+ { "day": "fri", "opens": "11:00", "closes": "01:30" },
144
+ { "day": "sat", "opens": "10:00", "closes": "01:30" }
145
+ ]
129
146
  }
130
147
  }
131
148
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Checked-in hashContent() snapshots of the v1 fixtures — the serialization-determinism contract. If a test fails against these, the canonical serialization or a fixture changed; BOTH are locked decisions (phase-2.md T1.6/T1.8). Regenerate only with founder sign-off.",
3
- "full.json": "sha256:34c778b74dc2ae260830ee072bdc3c197229f76f28b58775a61c57ab55abf3ef",
2
+ "_comment": "Checked-in hashContent() snapshots of the v1 fixtures — the serialization-determinism contract. If a test fails against these, the canonical serialization or a fixture changed; BOTH are locked decisions (phase-2.md T1.6/T1.8). Regenerate only with founder sign-off. REGENERATED ONCE, 2026-08-25: full.json gained the optional `commerce` module (phase-7 Slice 2), signed off in the proposal's Slice 2 line — 'full.json fixture ... publish fixtures and hash updates'. minimal.json is UNCHANGED and must stay so: commerce is optional, so the minimal payload never carries it.",
3
+ "full.json": "sha256:30bd220fda88f5dd511cd0c05fe888e9abad4f5e5946b398aade99ba8d3c8a08",
4
4
  "minimal.json": "sha256:7b4ebc88bf2c17e1182a7e89fe994c3a38f70e0e638b37a0caa662f0c7e8db22"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamgig/content-schemas",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "The LamGig published-JSON contract: Zod schemas, canonical serialization, and fixtures for consuming content published by the LamGig platform.",
5
5
  "keywords": [
6
6
  "lamgig",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\n/**\n * Version of the published-JSON contract. Additive changes (new OPTIONAL fields\n * and new OPTIONAL module keys) keep this number; any required addition is\n * breaking and bumps it. The full compatibility rule lives in this package's\n * README. See PLAN.md § Published JSON delivery and phase-2.md T1.5.\n */\nexport const SCHEMA_VERSION = 1;\n\n/** Publisher-enforced ceiling on the canonical payload size (1 MiB). */\nexport const MAX_CONTENT_BYTES = 1_048_576;\n\n// ---------------------------------------------------------------------------\n// Shared value patterns — single source of truth.\n//\n// LOCALE_PATTERN_SOURCE is the EXACT string used by the `site_settings.locale`\n// DB CHECK (supabase/migrations/*_content_site_settings.sql). Contract and\n// database must never disagree (phase-2.md T1.1): v1 supports the named BCP-47\n// subset `ll` or `ll-CC` only (e.g. `en`, `en-US`); script/numeric-region forms\n// (`zh-Hant`, `es-419`) are out of scope — widening later is additive.\n// HEX_COLOR / CURRENCY are likewise mirrored by DB CHECKs on `branding.colors`\n// and `menu_items.price_currency`.\n// ---------------------------------------------------------------------------\n\nexport const LOCALE_PATTERN_SOURCE = '^[a-z]{2}(-[A-Z]{2})?$';\nexport const LOCALE_PATTERN = new RegExp(LOCALE_PATTERN_SOURCE);\n\nexport const HEX_COLOR_PATTERN_SOURCE = '^#[0-9A-Fa-f]{6}$';\nexport const HEX_COLOR_PATTERN = new RegExp(HEX_COLOR_PATTERN_SOURCE);\n\n/** ISO 4217 alpha code shape (mirrored by the `price_currency` DB CHECK). */\nexport const CURRENCY_PATTERN_SOURCE = '^[A-Z]{3}$';\nexport const CURRENCY_PATTERN = new RegExp(CURRENCY_PATTERN_SOURCE);\n\n/** ISO 3166-1 alpha-2 country code shape. */\nexport const COUNTRY_PATTERN_SOURCE = '^[A-Z]{2}$';\nexport const COUNTRY_PATTERN = new RegExp(COUNTRY_PATTERN_SOURCE);\n\n/** IANA time zone check via the runtime's own tz database. */\nexport function isIanaTimeZone(tz: string): boolean {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Two parse modes from ONE schema definition (phase-2.md T1):\n//\n// - producer-strict — `.strict()` everywhere; the dashboard validates its own\n// output against this before publishing. Unknown keys are defects.\n// - consumer-tolerant — unknown object keys are STRIPPED and unknown module\n// keys ignored, so a shipped consumer accepts a v1 payload that later gained\n// an optional field or a new module key (the additive changes the\n// compatibility rule promises).\n//\n// zod v4's `.strip()`/`.passthrough()` do NOT recurse into nested objects, so a\n// post-hoc transform of one tree silently leaves nested objects in the original\n// mode. Instead every object in the tree is built through an injected object\n// factory: `z.strictObject` for the strict pair, `z.object` (strips unknown\n// keys) for the tolerant pair. One definition, two instantiations.\n// ---------------------------------------------------------------------------\n\ntype ObjectFactory = <T extends z.ZodRawShape>(shape: T) => z.ZodObject<T>;\n\nconst strictFactory = z.strictObject as ObjectFactory;\nconst tolerantFactory = z.object as ObjectFactory;\n\n// --- siteSettings ----------------------------------------------------------\n\nconst makeSiteSettingsSchema = (o: ObjectFactory) =>\n o({\n siteName: z.string().min(1).max(120),\n tagline: z.string().max(200).optional(),\n contact: o({\n email: z.email().optional(),\n phone: z.string().min(1).optional(),\n address: o({\n line1: z.string().min(1),\n line2: z.string().min(1).optional(),\n city: z.string().min(1),\n region: z.string().min(1),\n postalCode: z.string().min(1),\n country: z.string().regex(COUNTRY_PATTERN),\n }).optional(),\n }),\n // Explicit known keys (NOT a record): strict rejects unknown platforms,\n // tolerant strips them — adding a platform later is additive.\n socialLinks: o({\n facebook: z.url().optional(),\n instagram: z.url().optional(),\n tiktok: z.url().optional(),\n x: z.url().optional(),\n youtube: z.url().optional(),\n yelp: z.url().optional(),\n google: z.url().optional(),\n }).optional(),\n locale: z.string().regex(LOCALE_PATTERN).default('en-US'),\n timezone: z\n .string()\n .min(1)\n .refine(isIanaTimeZone, 'must be an IANA time zone name')\n .default('America/Los_Angeles'),\n });\n\nexport const siteSettingsSchemaStrict = makeSiteSettingsSchema(strictFactory);\nexport const siteSettingsSchema = makeSiteSettingsSchema(tolerantFactory);\n\n// --- branding --------------------------------------------------------------\n\nconst makeBrandingSchema = (o: ObjectFactory) =>\n o({\n // Absolute URLs produced by the dashboard's storagePublicUrl builder\n // (apps/dashboard/src/lib/content-urls.ts). The DB stores storage PATHS;\n // the publish-time compose maps path -> URL. This package never imports\n // from the app (dependency direction, phase-2.md T1.7).\n logoUrl: z.url().optional(),\n faviconUrl: z.url().optional(),\n colors: o({\n primary: z.string().regex(HEX_COLOR_PATTERN),\n secondary: z.string().regex(HEX_COLOR_PATTERN).optional(),\n accent: z.string().regex(HEX_COLOR_PATTERN).optional(),\n background: z.string().regex(HEX_COLOR_PATTERN).optional(),\n text: z.string().regex(HEX_COLOR_PATTERN).optional(),\n }),\n fonts: o({\n heading: z.string().min(1).optional(),\n body: z.string().min(1).optional(),\n }).optional(),\n });\n\nexport const brandingSchemaStrict = makeBrandingSchema(strictFactory);\nexport const brandingSchema = makeBrandingSchema(tolerantFactory);\n\n// --- menu ------------------------------------------------------------------\n\nconst makeMenuItemSchema = (o: ObjectFactory) =>\n o({\n /** Stable DB UUID — identical across publishes (phase-2.md T1.3). */\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n price: o({\n /** Integer minor units (e.g. cents). */\n amount: z.number().int().min(0),\n currency: z.string().regex(CURRENCY_PATTERN),\n }),\n imageUrl: z.url().optional(),\n tags: z.array(z.string().min(1)).max(20),\n sortOrder: z.number().int().min(0).max(100_000),\n isAvailable: z.boolean(),\n });\n\nconst makeMenuCategorySchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(200),\n description: z.string().max(2000).optional(),\n sortOrder: z.number().int().min(0).max(100_000),\n items: z.array(makeMenuItemSchema(o)),\n });\n\nconst makeMenuSchema = (o: ObjectFactory) =>\n o({\n /** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */\n categories: z.array(makeMenuCategorySchema(o)),\n });\n\nexport const menuSchemaStrict = makeMenuSchema(strictFactory);\nexport const menuSchema = makeMenuSchema(tolerantFactory);\n\n// --- content file ----------------------------------------------------------\n\nconst makeContentFileSchema = (o: ObjectFactory) =>\n o({\n schemaVersion: z.literal(1),\n site: o({ id: z.uuid(), slug: z.string().min(1) }),\n publishedAt: z.iso.datetime(),\n // All three modules REQUIRED; empty states are valid module payloads\n // (e.g. menu.categories: []), never missing keys. A new required module is\n // breaking (retained/rollback snapshots won't contain it) — bump\n // SCHEMA_VERSION instead.\n modules: o({\n siteSettings: makeSiteSettingsSchema(o),\n branding: makeBrandingSchema(o),\n menu: makeMenuSchema(o),\n }),\n });\n\n/** Producer gate: the dashboard validates its own output against this. */\nexport const contentFileSchemaStrict = makeContentFileSchema(strictFactory);\n/** Public consumer schema: tolerant of additive v1 changes. */\nexport const contentFileSchema = makeContentFileSchema(tolerantFactory);\n\nexport type SiteSettings = z.infer<typeof siteSettingsSchemaStrict>;\nexport type Branding = z.infer<typeof brandingSchemaStrict>;\nexport type Menu = z.infer<typeof menuSchemaStrict>;\nexport type MenuCategory = Menu['categories'][number];\nexport type MenuItem = MenuCategory['items'][number];\nexport type ContentFile = z.infer<typeof contentFileSchemaStrict>;\n\n// ---------------------------------------------------------------------------\n// Canonical serialization (phase-2.md T1.6).\n//\n// Recursively key-sorted JSON with no whitespace, produced by a hand-rolled\n// serializer rather than JSON.stringify over a re-built object — rebuilding\n// depends on JS insertion-order semantics and silently reorders integer-like\n// keys. Determinism here is what makes `hashContent` a byte-equality contract\n// shared by the publisher and every consumer test.\n//\n// Rules (documented in the README):\n// - arrays keep their order; object keys are sorted with Array.prototype.sort\n// (code-unit order);\n// - object properties whose value is `undefined` are omitted (JSON semantics);\n// - `undefined` anywhere else, non-finite numbers, bigint, functions and\n// symbols THROW — never silently coerced the way JSON.stringify would.\n// ---------------------------------------------------------------------------\n\nexport function canonicalize(payload: unknown): string {\n const out = serialize(payload);\n if (out === undefined) {\n throw new TypeError('canonicalize: payload itself serializes to nothing (undefined)');\n }\n return out;\n}\n\nfunction serialize(value: unknown): string | undefined {\n if (value === undefined) return undefined;\n if (value === null) return 'null';\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return JSON.stringify(value);\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(`canonicalize: non-finite number ${String(value)}`);\n }\n return JSON.stringify(value);\n case 'object':\n break;\n default:\n throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);\n }\n if (Array.isArray(value)) {\n const parts = value.map((entry) => {\n const s = serialize(entry);\n if (s === undefined) {\n throw new TypeError('canonicalize: undefined array element');\n }\n return s;\n });\n return `[${parts.join(',')}]`;\n }\n const record = value as Record<string, unknown>;\n const keys = Object.keys(record).sort();\n const parts: string[] = [];\n for (const key of keys) {\n const s = serialize(record[key]);\n if (s === undefined) continue; // undefined property -> omitted key\n parts.push(`${JSON.stringify(key)}:${s}`);\n }\n return `{${parts.join(',')}}`;\n}\n"],"mappings":"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","names":["parts"]}