@lamgig/content-schemas 1.0.0 → 1.2.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,69 @@ 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` | The producer publishes `'none' \| 'other'` today (`COMMERCE_PROVIDERS`). **An open enum** — see below: the tolerant schema accepts any well-formed provider name |
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.
76
+
77
+ ### Open enums: `commerce.provider` (1.2.0)
78
+
79
+ `provider` is the **only open enum** in the contract. The platform will publish new providers
80
+ (`stripe`, then `square`) as it learns to serve them, one per release, and a consumer built
81
+ before that must keep working.
82
+
83
+ | Schema | Accepts |
84
+ |---|---|
85
+ | `contentFileSchemaStrict` / `commerceSchemaStrict` (producer) | exactly `COMMERCE_PROVIDERS` |
86
+ | `contentFileSchema` / `commerceSchema` (consumer) | `COMMERCE_PROVIDERS`, **or any name matching `COMMERCE_PROVIDER_PATTERN`** (`^[a-z][a-z0-9_]{0,31}$`), passed through unchanged. Malformed values (`"Stripe!"`, `""`) still fail |
87
+
88
+ **The consumer rule.** An unknown provider means "a provider newer than this consumer". Treat it
89
+ exactly like `other`: when `acceptsOnlineOrders` is true and `orderingUrl` is present, offer the
90
+ hand-off to `orderingUrl`; **never** show a cart or a payment form for a provider you don't
91
+ know. `isKnownCommerceProvider(p)` narrows a parsed value to the providers this version knows.
92
+ The platform always publishes `orderingUrl` when online ordering is on, in every mode, so this
93
+ rule keeps every consumer working.
94
+
95
+ **Upgrade before the platform publishes a new provider.** Up to **1.1.x the tolerant schema
96
+ rejected unknown providers** — it declared the same closed enum as the strict schema, so a
97
+ payload naming a newer provider failed to parse *as a whole* (tolerance covered unknown keys,
98
+ never unknown enum values, despite what 1.1.0's docs said). Consumers on 1.1.x must move to
99
+ ≥ 1.2.0 before any site publishes a provider outside `none | other`. Consumers on 1.0.x are
100
+ unaffected: 1.0.x has no `commerce` schema and strips the module entirely.
101
+
102
+ **TypeScript consumers: type parsed content as `ConsumerContentFile`** (and `ConsumerCommerce`),
103
+ not `ContentFile`. `ContentFile` is the *producer's* type, inferred from the strict schema, so its
104
+ `provider` is the closed union; `contentFileSchema.parse` returns the widened type. A consumer
105
+ that annotated tolerant output as `ContentFile` gets a compile error on `commerce.provider` when
106
+ it upgrades to 1.2.0 — that error is the point: it marks the one place the consumer must handle
107
+ a provider it doesn't know. The JSON contract itself is unchanged (no schemaVersion bump).
108
+
109
+ `hours[].day` stays a **closed** enum: the days of the week are not going to grow.
49
110
 
50
111
  ## Canonical serialization
51
112
 
@@ -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,22 +103,85 @@ var makeMenuSchema = (o) => o({
100
103
  });
101
104
  var menuSchemaStrict = makeMenuSchema(strictFactory);
102
105
  var menuSchema = makeMenuSchema(tolerantFactory);
103
- var makeContentFileSchema = (o) => o({
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 COMMERCE_PROVIDERS = ["none", "other"];
136
+ var COMMERCE_PROVIDER_PATTERN_SOURCE = "^[a-z][a-z0-9_]{0,31}$";
137
+ var COMMERCE_PROVIDER_PATTERN = new RegExp(COMMERCE_PROVIDER_PATTERN_SOURCE);
138
+ var commerceProviderStrict = z.enum(COMMERCE_PROVIDERS);
139
+ var commerceProviderTolerant = z.union([
140
+ z.enum(COMMERCE_PROVIDERS),
141
+ z.string().regex(COMMERCE_PROVIDER_PATTERN)
142
+ ]);
143
+ function isKnownCommerceProvider(provider) {
144
+ return COMMERCE_PROVIDERS.includes(provider);
145
+ }
146
+ var makeCommerceSchema = (o, provider) => o({
147
+ provider,
148
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
149
+ orderingUrl: z.url({ protocol: /^https$/ }).max(2048).optional(),
150
+ /** PUBLIC provider identifiers — never a key, token or secret. */
151
+ locationRef: z.string().min(1).max(128).optional(),
152
+ menuRef: z.string().min(1).max(128).optional(),
153
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
154
+ currency: z.string().regex(CURRENCY_PATTERN),
155
+ acceptsOnlineOrders: z.boolean(),
156
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
157
+ prepTimeMinutes: z.number().int().min(0).max(240).optional(),
158
+ hours: makeCommerceHoursSchema(o)
159
+ });
160
+ var commerceSchemaStrict = makeCommerceSchema(strictFactory, commerceProviderStrict);
161
+ var commerceSchema = makeCommerceSchema(tolerantFactory, commerceProviderTolerant);
162
+ var makeContentFileSchema = (o, commerceProvider) => o({
104
163
  schemaVersion: z.literal(1),
105
164
  site: o({ id: z.uuid(), slug: z.string().min(1) }),
106
165
  publishedAt: z.iso.datetime(),
107
- // All three modules REQUIRED; empty states are valid module payloads
166
+ // The three v1 modules are REQUIRED; empty states are valid module payloads
108
167
  // (e.g. menu.categories: []), never missing keys. A new required module is
109
168
  // breaking (retained/rollback snapshots won't contain it) — bump
110
169
  // SCHEMA_VERSION instead.
170
+ //
171
+ // `commerce` is therefore OPTIONAL (phase-7 D-3), and that is not a
172
+ // stylistic choice: every snapshot published before the module existed
173
+ // lacks the key, and a ROLLBACK can activate one of those at any time. A
174
+ // required `commerce` would retroactively invalidate the whole publish
175
+ // history. See ARCHITECTURE.md § 5 and docs/seams/commerce.md § 1.
111
176
  modules: o({
112
177
  siteSettings: makeSiteSettingsSchema(o),
113
178
  branding: makeBrandingSchema(o),
114
- menu: makeMenuSchema(o)
179
+ menu: makeMenuSchema(o),
180
+ commerce: makeCommerceSchema(o, commerceProvider).optional()
115
181
  })
116
182
  });
117
- var contentFileSchemaStrict = makeContentFileSchema(strictFactory);
118
- var contentFileSchema = makeContentFileSchema(tolerantFactory);
183
+ var contentFileSchemaStrict = makeContentFileSchema(strictFactory, commerceProviderStrict);
184
+ var contentFileSchema = makeContentFileSchema(tolerantFactory, commerceProviderTolerant);
119
185
  function canonicalize(payload) {
120
186
  const out = serialize(payload);
121
187
  if (out === void 0) {
@@ -172,6 +238,9 @@ export {
172
238
  CURRENCY_PATTERN,
173
239
  COUNTRY_PATTERN_SOURCE,
174
240
  COUNTRY_PATTERN,
241
+ COMMERCE_DAYS,
242
+ TIME_24H_PATTERN_SOURCE,
243
+ TIME_24H_PATTERN,
175
244
  isIanaTimeZone,
176
245
  siteSettingsSchemaStrict,
177
246
  siteSettingsSchema,
@@ -179,8 +248,14 @@ export {
179
248
  brandingSchema,
180
249
  menuSchemaStrict,
181
250
  menuSchema,
251
+ COMMERCE_PROVIDERS,
252
+ COMMERCE_PROVIDER_PATTERN_SOURCE,
253
+ COMMERCE_PROVIDER_PATTERN,
254
+ isKnownCommerceProvider,
255
+ commerceSchemaStrict,
256
+ commerceSchema,
182
257
  contentFileSchemaStrict,
183
258
  contentFileSchema,
184
259
  canonicalize
185
260
  };
186
- //# sourceMappingURL=chunk-KBPOR3WL.js.map
261
+ //# sourceMappingURL=chunk-PMUVTXAV.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// `provider` is the one OPEN enum in the contract (docs/proposals/online-commerce\n// OC-2). The PRODUCER enum is closed and grows by one member per phase, only\n// when the platform can actually serve the new value — today `none | other`.\n// The CONSUMER schema accepts any well-formed provider name and hands it through\n// unchanged, so a consumer that predates a new provider keeps parsing.\n//\n// That second half did not exist before 1.2.0. Until then the tolerant schema\n// declared the same closed enum as the strict one, so an unknown provider made\n// the WHOLE payload fail to parse — the comment here claimed widening was\n// \"additive for a tolerant consumer\", and the code said otherwise. Tolerance for\n// unknown KEYS (the object factories) never covered unknown enum VALUES.\n//\n// Consumer rule (README § Open enums): an unknown provider means \"newer than\n// me\" — treat it like `other`: offer the `orderingUrl` hand-off when\n// `acceptsOnlineOrders` is true, and never show a cart.\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\n/** Providers the PRODUCER may publish. Grows one member per phase (OC-2). */\nexport const COMMERCE_PROVIDERS = ['none', 'other'] as const;\nexport type KnownCommerceProvider = (typeof COMMERCE_PROVIDERS)[number];\n\n/**\n * Shape of a provider name a TOLERANT consumer accepts: lowercase, starts with a\n * letter, at most 32 characters. Tolerance is for FUTURE values, not malformed\n * ones — `\"Stripe!\"` or `\"\"` still fail.\n */\nexport const COMMERCE_PROVIDER_PATTERN_SOURCE = '^[a-z][a-z0-9_]{0,31}$';\nexport const COMMERCE_PROVIDER_PATTERN = new RegExp(COMMERCE_PROVIDER_PATTERN_SOURCE);\n\nconst commerceProviderStrict = z.enum(COMMERCE_PROVIDERS);\nconst commerceProviderTolerant = z.union([\n z.enum(COMMERCE_PROVIDERS),\n z.string().regex(COMMERCE_PROVIDER_PATTERN),\n]);\n\n/** Narrow a tolerantly-parsed provider to one this version of the package knows. */\nexport function isKnownCommerceProvider(provider: string): provider is KnownCommerceProvider {\n return (COMMERCE_PROVIDERS as readonly string[]).includes(provider);\n}\n\ntype CommerceProviderSchema = typeof commerceProviderStrict | typeof commerceProviderTolerant;\n\nconst makeCommerceSchema = <P extends CommerceProviderSchema>(o: ObjectFactory, provider: P) =>\n o({\n provider,\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, commerceProviderStrict);\nexport const commerceSchema = makeCommerceSchema(tolerantFactory, commerceProviderTolerant);\n\n// --- content file ----------------------------------------------------------\n\nconst makeContentFileSchema = <P extends CommerceProviderSchema>(\n o: ObjectFactory,\n commerceProvider: P,\n) =>\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, commerceProvider).optional(),\n }),\n });\n\n/** Producer gate: the dashboard validates its own output against this. */\nexport const contentFileSchemaStrict = makeContentFileSchema(strictFactory, commerceProviderStrict);\n/** Public consumer schema: tolerant of additive v1 changes. */\nexport const contentFileSchema = makeContentFileSchema(tolerantFactory, commerceProviderTolerant);\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 * What a CONSUMER gets back from `contentFileSchema.parse` (1.2.0+). Identical to\n * `ContentFile` except that open enums are widened — `commerce.provider` is any\n * well-formed string, because a payload may name a provider newer than the\n * consumer. Type parsed content with THIS, not with the producer's `ContentFile`:\n * annotating tolerant output as `ContentFile` claims the provider is one this\n * package version knows, which is exactly what tolerance cannot promise.\n */\nexport type ConsumerContentFile = z.infer<typeof contentFileSchema>;\nexport type ConsumerCommerce = z.infer<typeof commerceSchema>;\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-PMUVTXAV.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,76 @@ declare const menuSchema: z.ZodObject<{
153
169
  }, z.core.$strip>>;
154
170
  }, z.core.$strip>>;
155
171
  }, z.core.$strip>;
172
+ /** Providers the PRODUCER may publish. Grows one member per phase (OC-2). */
173
+ declare const COMMERCE_PROVIDERS: readonly ["none", "other"];
174
+ type KnownCommerceProvider = (typeof COMMERCE_PROVIDERS)[number];
175
+ /**
176
+ * Shape of a provider name a TOLERANT consumer accepts: lowercase, starts with a
177
+ * letter, at most 32 characters. Tolerance is for FUTURE values, not malformed
178
+ * ones — `"Stripe!"` or `""` still fail.
179
+ */
180
+ declare const COMMERCE_PROVIDER_PATTERN_SOURCE = "^[a-z][a-z0-9_]{0,31}$";
181
+ declare const COMMERCE_PROVIDER_PATTERN: RegExp;
182
+ /** Narrow a tolerantly-parsed provider to one this version of the package knows. */
183
+ declare function isKnownCommerceProvider(provider: string): provider is KnownCommerceProvider;
184
+ declare const commerceSchemaStrict: z.ZodObject<{
185
+ provider: z.ZodEnum<{
186
+ none: "none";
187
+ other: "other";
188
+ }>;
189
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
190
+ orderingUrl: z.ZodOptional<z.ZodURL>;
191
+ /** PUBLIC provider identifiers — never a key, token or secret. */
192
+ locationRef: z.ZodOptional<z.ZodString>;
193
+ menuRef: z.ZodOptional<z.ZodString>;
194
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
195
+ currency: z.ZodString;
196
+ acceptsOnlineOrders: z.ZodBoolean;
197
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
198
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
199
+ hours: z.ZodArray<z.ZodObject<{
200
+ day: z.ZodEnum<{
201
+ mon: "mon";
202
+ tue: "tue";
203
+ wed: "wed";
204
+ thu: "thu";
205
+ fri: "fri";
206
+ sat: "sat";
207
+ sun: "sun";
208
+ }>;
209
+ opens: z.ZodString;
210
+ closes: z.ZodString;
211
+ }, z.core.$strip>>;
212
+ }, z.core.$strip>;
213
+ declare const commerceSchema: z.ZodObject<{
214
+ provider: z.ZodUnion<readonly [z.ZodEnum<{
215
+ none: "none";
216
+ other: "other";
217
+ }>, z.ZodString]>;
218
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
219
+ orderingUrl: z.ZodOptional<z.ZodURL>;
220
+ /** PUBLIC provider identifiers — never a key, token or secret. */
221
+ locationRef: z.ZodOptional<z.ZodString>;
222
+ menuRef: z.ZodOptional<z.ZodString>;
223
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
224
+ currency: z.ZodString;
225
+ acceptsOnlineOrders: z.ZodBoolean;
226
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
227
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
228
+ hours: z.ZodArray<z.ZodObject<{
229
+ day: z.ZodEnum<{
230
+ mon: "mon";
231
+ tue: "tue";
232
+ wed: "wed";
233
+ thu: "thu";
234
+ fri: "fri";
235
+ sat: "sat";
236
+ sun: "sun";
237
+ }>;
238
+ opens: z.ZodString;
239
+ closes: z.ZodString;
240
+ }, z.core.$strip>>;
241
+ }, z.core.$strip>;
156
242
  /** Producer gate: the dashboard validates its own output against this. */
157
243
  declare const contentFileSchemaStrict: z.ZodObject<{
158
244
  schemaVersion: z.ZodLiteral<1>;
@@ -228,6 +314,35 @@ declare const contentFileSchemaStrict: z.ZodObject<{
228
314
  }, z.core.$strip>>;
229
315
  }, z.core.$strip>>;
230
316
  }, z.core.$strip>;
317
+ commerce: z.ZodOptional<z.ZodObject<{
318
+ provider: z.ZodEnum<{
319
+ none: "none";
320
+ other: "other";
321
+ }>;
322
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
323
+ orderingUrl: z.ZodOptional<z.ZodURL>;
324
+ /** PUBLIC provider identifiers — never a key, token or secret. */
325
+ locationRef: z.ZodOptional<z.ZodString>;
326
+ menuRef: z.ZodOptional<z.ZodString>;
327
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
328
+ currency: z.ZodString;
329
+ acceptsOnlineOrders: z.ZodBoolean;
330
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
331
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
332
+ hours: z.ZodArray<z.ZodObject<{
333
+ day: z.ZodEnum<{
334
+ mon: "mon";
335
+ tue: "tue";
336
+ wed: "wed";
337
+ thu: "thu";
338
+ fri: "fri";
339
+ sat: "sat";
340
+ sun: "sun";
341
+ }>;
342
+ opens: z.ZodString;
343
+ closes: z.ZodString;
344
+ }, z.core.$strip>>;
345
+ }, z.core.$strip>>;
231
346
  }, z.core.$strip>;
232
347
  }, z.core.$strip>;
233
348
  /** Public consumer schema: tolerant of additive v1 changes. */
@@ -305,6 +420,35 @@ declare const contentFileSchema: z.ZodObject<{
305
420
  }, z.core.$strip>>;
306
421
  }, z.core.$strip>>;
307
422
  }, z.core.$strip>;
423
+ commerce: z.ZodOptional<z.ZodObject<{
424
+ provider: z.ZodUnion<readonly [z.ZodEnum<{
425
+ none: "none";
426
+ other: "other";
427
+ }>, z.ZodString]>;
428
+ /** Where a customer is sent to order. Absent when `provider` is `none`. */
429
+ orderingUrl: z.ZodOptional<z.ZodURL>;
430
+ /** PUBLIC provider identifiers — never a key, token or secret. */
431
+ locationRef: z.ZodOptional<z.ZodString>;
432
+ menuRef: z.ZodOptional<z.ZodString>;
433
+ /** Matches `menu` item prices; the DB CHECK mirrors CURRENCY_PATTERN_SOURCE. */
434
+ currency: z.ZodString;
435
+ acceptsOnlineOrders: z.ZodBoolean;
436
+ /** Quoted preparation time. Meaningless — and normally absent — when ordering is off. */
437
+ prepTimeMinutes: z.ZodOptional<z.ZodNumber>;
438
+ hours: z.ZodArray<z.ZodObject<{
439
+ day: z.ZodEnum<{
440
+ mon: "mon";
441
+ tue: "tue";
442
+ wed: "wed";
443
+ thu: "thu";
444
+ fri: "fri";
445
+ sat: "sat";
446
+ sun: "sun";
447
+ }>;
448
+ opens: z.ZodString;
449
+ closes: z.ZodString;
450
+ }, z.core.$strip>>;
451
+ }, z.core.$strip>>;
308
452
  }, z.core.$strip>;
309
453
  }, z.core.$strip>;
310
454
  type SiteSettings = z.infer<typeof siteSettingsSchemaStrict>;
@@ -312,7 +456,20 @@ type Branding = z.infer<typeof brandingSchemaStrict>;
312
456
  type Menu = z.infer<typeof menuSchemaStrict>;
313
457
  type MenuCategory = Menu['categories'][number];
314
458
  type MenuItem = MenuCategory['items'][number];
459
+ type Commerce = z.infer<typeof commerceSchemaStrict>;
460
+ type CommerceDay = (typeof COMMERCE_DAYS)[number];
461
+ type CommerceHours = Commerce['hours'];
315
462
  type ContentFile = z.infer<typeof contentFileSchemaStrict>;
463
+ /**
464
+ * What a CONSUMER gets back from `contentFileSchema.parse` (1.2.0+). Identical to
465
+ * `ContentFile` except that open enums are widened — `commerce.provider` is any
466
+ * well-formed string, because a payload may name a provider newer than the
467
+ * consumer. Type parsed content with THIS, not with the producer's `ContentFile`:
468
+ * annotating tolerant output as `ContentFile` claims the provider is one this
469
+ * package version knows, which is exactly what tolerance cannot promise.
470
+ */
471
+ type ConsumerContentFile = z.infer<typeof contentFileSchema>;
472
+ type ConsumerCommerce = z.infer<typeof commerceSchema>;
316
473
  declare function canonicalize(payload: unknown): string;
317
474
 
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 };
475
+ export { type Branding, COMMERCE_DAYS, COMMERCE_PROVIDERS, COMMERCE_PROVIDER_PATTERN, COMMERCE_PROVIDER_PATTERN_SOURCE, COUNTRY_PATTERN, COUNTRY_PATTERN_SOURCE, CURRENCY_PATTERN, CURRENCY_PATTERN_SOURCE, type Commerce, type CommerceDay, type CommerceHours, type ConsumerCommerce, type ConsumerContentFile, type ContentFile, HEX_COLOR_PATTERN, HEX_COLOR_PATTERN_SOURCE, type KnownCommerceProvider, 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, isKnownCommerceProvider, menuSchema, menuSchemaStrict, siteSettingsSchema, siteSettingsSchemaStrict };
package/dist/index.js CHANGED
@@ -1,4 +1,8 @@
1
1
  import {
2
+ COMMERCE_DAYS,
3
+ COMMERCE_PROVIDERS,
4
+ COMMERCE_PROVIDER_PATTERN,
5
+ COMMERCE_PROVIDER_PATTERN_SOURCE,
2
6
  COUNTRY_PATTERN,
3
7
  COUNTRY_PATTERN_SOURCE,
4
8
  CURRENCY_PATTERN,
@@ -9,18 +13,27 @@ import {
9
13
  LOCALE_PATTERN_SOURCE,
10
14
  MAX_CONTENT_BYTES,
11
15
  SCHEMA_VERSION,
16
+ TIME_24H_PATTERN,
17
+ TIME_24H_PATTERN_SOURCE,
12
18
  brandingSchema,
13
19
  brandingSchemaStrict,
14
20
  canonicalize,
21
+ commerceSchema,
22
+ commerceSchemaStrict,
15
23
  contentFileSchema,
16
24
  contentFileSchemaStrict,
17
25
  isIanaTimeZone,
26
+ isKnownCommerceProvider,
18
27
  menuSchema,
19
28
  menuSchemaStrict,
20
29
  siteSettingsSchema,
21
30
  siteSettingsSchemaStrict
22
- } from "./chunk-KBPOR3WL.js";
31
+ } from "./chunk-PMUVTXAV.js";
23
32
  export {
33
+ COMMERCE_DAYS,
34
+ COMMERCE_PROVIDERS,
35
+ COMMERCE_PROVIDER_PATTERN,
36
+ COMMERCE_PROVIDER_PATTERN_SOURCE,
24
37
  COUNTRY_PATTERN,
25
38
  COUNTRY_PATTERN_SOURCE,
26
39
  CURRENCY_PATTERN,
@@ -31,12 +44,17 @@ export {
31
44
  LOCALE_PATTERN_SOURCE,
32
45
  MAX_CONTENT_BYTES,
33
46
  SCHEMA_VERSION,
47
+ TIME_24H_PATTERN,
48
+ TIME_24H_PATTERN_SOURCE,
34
49
  brandingSchema,
35
50
  brandingSchemaStrict,
36
51
  canonicalize,
52
+ commerceSchema,
53
+ commerceSchemaStrict,
37
54
  contentFileSchema,
38
55
  contentFileSchemaStrict,
39
56
  isIanaTimeZone,
57
+ isKnownCommerceProvider,
40
58
  menuSchema,
41
59
  menuSchemaStrict,
42
60
  siteSettingsSchema,
@@ -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.2.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",
@@ -57,6 +57,6 @@
57
57
  "devDependencies": {
58
58
  "tsup": "^8.5.1",
59
59
  "typescript": "^5",
60
- "vitest": "^4.1.10"
60
+ "vitest": "^5.0.3"
61
61
  }
62
62
  }
@@ -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"]}