@lamgig/content-schemas 1.2.0 → 1.3.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 +43 -1
- package/dist/{chunk-PMUVTXAV.js → chunk-7B6AENRI.js} +111 -8
- package/dist/chunk-7B6AENRI.js.map +1 -0
- package/dist/hash.js +1 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.js +9 -1
- package/fixtures/v1/full.json +114 -9
- package/fixtures/v1/hashes.json +2 -2
- package/package.json +1 -1
- package/dist/chunk-PMUVTXAV.js.map +0 -1
package/README.md
CHANGED
|
@@ -52,6 +52,47 @@ get uniform strictness):
|
|
|
52
52
|
deliberately kept commerce-free as the pinned example of that shape. A consumer must treat
|
|
53
53
|
`modules.commerce` as possibly absent — `parsed.modules.commerce?.acceptsOnlineOrders`.
|
|
54
54
|
|
|
55
|
+
## Menu option groups (1.3.0)
|
|
56
|
+
|
|
57
|
+
Sizes and add-ons, as **one** concept: an option group. A size is `min: 1, max: 1`; an add-on
|
|
58
|
+
list is `min: 0, max: n`. Two **optional** keys inside `modules.menu`; nothing existing changes
|
|
59
|
+
and `schemaVersion` stays 1.
|
|
60
|
+
|
|
61
|
+
| Key | Where | Notes |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `optionGroups` | `menu` | The site's groups, each `{ id, name, min, max, sortOrder, options }`; each option `{ id, name, priceDelta, isAvailable, sortOrder }` |
|
|
64
|
+
| `optionGroupIds` | each item | Ids into `optionGroups`, **in display order** |
|
|
65
|
+
|
|
66
|
+
- **Absent ≡ `[]`** — the producer omits both keys when there is nothing to say, so a site that
|
|
67
|
+
uses no options publishes exactly its 1.2.0 shape, and every snapshot published before 1.3.0
|
|
68
|
+
stays valid (a rollback can activate one at any time). Never `null`.
|
|
69
|
+
- **Price rule:** unit price = `item.price.amount` + Σ chosen options' `priceDelta`. Deltas are
|
|
70
|
+
integer minor units, ≥ 0, **in the item's currency** — they carry no currency of their own.
|
|
71
|
+
- **Archived groups and options are absent**, and an archived group is dropped from every
|
|
72
|
+
item's `optionGroupIds`. An option that is merely unavailable is present with
|
|
73
|
+
`isAvailable: false` — show it as unavailable rather than hiding it. A group no item uses is
|
|
74
|
+
still published.
|
|
75
|
+
- **Strict-only publish gate** (`menuSchemaStrict` / `contentFileSchemaStrict`): a required group
|
|
76
|
+
has an available option; `max` ≤ available options; ≤ `MAX_OPTION_GROUPS_PER_ITEM` (10)
|
|
77
|
+
groups per item; ≤ `MAX_OPTIONS_PER_GROUP` (30) options per group; one currency per group;
|
|
78
|
+
references resolve, each once; ids unique. Each message names the group or item. The
|
|
79
|
+
**tolerant** schemas do not apply these — a consumer must never reject a payload over a rule
|
|
80
|
+
a later release may relax — but field bounds (`min`/`max` 0–30, `priceDelta` 0–100000, name
|
|
81
|
+
1–120) hold in both modes, as for every other field.
|
|
82
|
+
- **Consumers on ≤ 1.2.x** strip both keys and keep working: they show the base price only.
|
|
83
|
+
|
|
84
|
+
Types: `MenuOptionGroup`, `MenuOption` (producer); read them off `ConsumerContentFile` in a
|
|
85
|
+
consumer.
|
|
86
|
+
|
|
87
|
+
## Changelog
|
|
88
|
+
|
|
89
|
+
| Version | Change |
|
|
90
|
+
|---|---|
|
|
91
|
+
| 1.3.0 | Optional `menu.optionGroups` and `items[].optionGroupIds` (Online Commerce C3); `MenuOptionGroup`/`MenuOption` types; `MAX_OPTION_*` constants. `full.json` gains options (hash re-pinned); `minimal.json` unchanged |
|
|
92
|
+
| 1.2.0 | `commerce.provider` becomes an open enum for consumers (`ConsumerContentFile`, `isKnownCommerceProvider`) |
|
|
93
|
+
| 1.1.0 | Optional `commerce` module |
|
|
94
|
+
| 1.0.0 | The locked v1 contract: `siteSettings`, `branding`, `menu` |
|
|
95
|
+
|
|
55
96
|
## The `commerce` module (1.1.0)
|
|
56
97
|
|
|
57
98
|
Online-ordering **configuration**. LamGig does not process orders and stores no provider
|
|
@@ -153,7 +194,8 @@ and the database can never disagree. Widening the locale subset later is additiv
|
|
|
153
194
|
|
|
154
195
|
## Fixtures (`fixtures/v1/`)
|
|
155
196
|
|
|
156
|
-
- `full.json` — realistic restaurant, every optional field populated
|
|
197
|
+
- `full.json` — realistic restaurant, every optional field populated (option groups since
|
|
198
|
+
1.3.0, including an unavailable option).
|
|
157
199
|
- `minimal.json` — empty menu, no optional fields; also the reference shape for the
|
|
158
200
|
fresh-site validity check (a newly provisioned site must gather to this shape).
|
|
159
201
|
- `hashes.json` — checked-in `hashContent` snapshots; a mismatch means serialization or a
|
|
@@ -75,6 +75,29 @@ var makeBrandingSchema = (o) => o({
|
|
|
75
75
|
});
|
|
76
76
|
var brandingSchemaStrict = makeBrandingSchema(strictFactory);
|
|
77
77
|
var brandingSchema = makeBrandingSchema(tolerantFactory);
|
|
78
|
+
var MAX_OPTION_GROUPS_PER_ITEM = 10;
|
|
79
|
+
var MAX_OPTIONS_PER_GROUP = 30;
|
|
80
|
+
var MAX_OPTION_SELECT = 30;
|
|
81
|
+
var MAX_OPTION_PRICE_DELTA = 1e5;
|
|
82
|
+
var makeMenuOptionSchema = (o) => o({
|
|
83
|
+
id: z.uuid(),
|
|
84
|
+
name: z.string().min(1).max(120),
|
|
85
|
+
/** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
|
|
86
|
+
priceDelta: z.number().int().min(0).max(MAX_OPTION_PRICE_DELTA),
|
|
87
|
+
isAvailable: z.boolean(),
|
|
88
|
+
sortOrder: z.number().int().min(0).max(1e5)
|
|
89
|
+
});
|
|
90
|
+
var makeMenuOptionGroupSchema = (o) => o({
|
|
91
|
+
id: z.uuid(),
|
|
92
|
+
name: z.string().min(1).max(120),
|
|
93
|
+
/** Fewest options a customer must choose; ≥ 1 makes the group required. */
|
|
94
|
+
min: z.number().int().min(0).max(MAX_OPTION_SELECT),
|
|
95
|
+
/** Most options a customer may choose. */
|
|
96
|
+
max: z.number().int().min(0).max(MAX_OPTION_SELECT),
|
|
97
|
+
sortOrder: z.number().int().min(0).max(1e5),
|
|
98
|
+
/** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
|
|
99
|
+
options: z.array(makeMenuOptionSchema(o))
|
|
100
|
+
});
|
|
78
101
|
var makeMenuItemSchema = (o) => o({
|
|
79
102
|
/** Stable DB UUID — identical across publishes (phase-2.md T1.3). */
|
|
80
103
|
id: z.uuid(),
|
|
@@ -88,7 +111,9 @@ var makeMenuItemSchema = (o) => o({
|
|
|
88
111
|
imageUrl: z.url().optional(),
|
|
89
112
|
tags: z.array(z.string().min(1)).max(20),
|
|
90
113
|
sortOrder: z.number().int().min(0).max(1e5),
|
|
91
|
-
isAvailable: z.boolean()
|
|
114
|
+
isAvailable: z.boolean(),
|
|
115
|
+
/** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
|
|
116
|
+
optionGroupIds: z.array(z.uuid()).optional()
|
|
92
117
|
});
|
|
93
118
|
var makeMenuCategorySchema = (o) => o({
|
|
94
119
|
id: z.uuid(),
|
|
@@ -99,9 +124,75 @@ var makeMenuCategorySchema = (o) => o({
|
|
|
99
124
|
});
|
|
100
125
|
var makeMenuSchema = (o) => o({
|
|
101
126
|
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
102
|
-
categories: z.array(makeMenuCategorySchema(o))
|
|
127
|
+
categories: z.array(makeMenuCategorySchema(o)),
|
|
128
|
+
/** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
|
|
129
|
+
optionGroups: z.array(makeMenuOptionGroupSchema(o)).optional()
|
|
103
130
|
});
|
|
104
|
-
|
|
131
|
+
function checkMenuOptions(menu, ctx) {
|
|
132
|
+
const issue = (path, message) => ctx.addIssue({ code: "custom", path, message });
|
|
133
|
+
const groups = menu.optionGroups ?? [];
|
|
134
|
+
const groupNames = /* @__PURE__ */ new Map();
|
|
135
|
+
const optionIds = /* @__PURE__ */ new Set();
|
|
136
|
+
groups.forEach((group, g) => {
|
|
137
|
+
const label = `Option group "${group.name}"`;
|
|
138
|
+
if (groupNames.has(group.id)) issue(["optionGroups", g, "id"], `${label} repeats another group's id`);
|
|
139
|
+
groupNames.set(group.id, group.name);
|
|
140
|
+
if (group.min > group.max) {
|
|
141
|
+
issue(["optionGroups", g, "min"], `${label} requires at least ${group.min} but allows at most ${group.max}`);
|
|
142
|
+
}
|
|
143
|
+
if (group.options.length > MAX_OPTIONS_PER_GROUP) {
|
|
144
|
+
issue(
|
|
145
|
+
["optionGroups", g, "options"],
|
|
146
|
+
`${label} has ${group.options.length} options; the limit is ${MAX_OPTIONS_PER_GROUP}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const available = group.options.filter((option) => option.isAvailable).length;
|
|
150
|
+
if (group.min >= 1 && available === 0) {
|
|
151
|
+
issue(["optionGroups", g, "options"], `${label} is required but has no available options`);
|
|
152
|
+
} else if (group.max > available) {
|
|
153
|
+
issue(
|
|
154
|
+
["optionGroups", g, "max"],
|
|
155
|
+
`${label} allows up to ${group.max} choices but has only ${available} available option${available === 1 ? "" : "s"}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
group.options.forEach((option, k) => {
|
|
159
|
+
if (optionIds.has(option.id)) {
|
|
160
|
+
issue(["optionGroups", g, "options", k, "id"], `${label}: option "${option.name}" repeats another option's id`);
|
|
161
|
+
}
|
|
162
|
+
optionIds.add(option.id);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
const firstUse = /* @__PURE__ */ new Map();
|
|
166
|
+
menu.categories.forEach((category, c) => {
|
|
167
|
+
category.items.forEach((item, i) => {
|
|
168
|
+
const ids = item.optionGroupIds ?? [];
|
|
169
|
+
const path = ["categories", c, "items", i, "optionGroupIds"];
|
|
170
|
+
if (ids.length > MAX_OPTION_GROUPS_PER_ITEM) {
|
|
171
|
+
issue(path, `Item "${item.name}" has ${ids.length} option groups; the limit is ${MAX_OPTION_GROUPS_PER_ITEM}`);
|
|
172
|
+
}
|
|
173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
174
|
+
ids.forEach((id, k) => {
|
|
175
|
+
if (seen.has(id)) issue([...path, k], `Item "${item.name}" lists the same option group twice`);
|
|
176
|
+
seen.add(id);
|
|
177
|
+
const groupName = groupNames.get(id);
|
|
178
|
+
if (groupName === void 0) {
|
|
179
|
+
issue([...path, k], `Item "${item.name}" uses an option group that is not published`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const prior = firstUse.get(id);
|
|
183
|
+
if (!prior) {
|
|
184
|
+
firstUse.set(id, { currency: item.price.currency, itemName: item.name });
|
|
185
|
+
} else if (prior.currency !== item.price.currency) {
|
|
186
|
+
issue(
|
|
187
|
+
[...path, k],
|
|
188
|
+
`Option group "${groupName}" is used by items in two currencies: "${prior.itemName}" (${prior.currency}) and "${item.name}" (${item.price.currency})`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
var menuSchemaStrict = makeMenuSchema(strictFactory).superRefine(checkMenuOptions);
|
|
105
196
|
var menuSchema = makeMenuSchema(tolerantFactory);
|
|
106
197
|
var makeCommerceHoursSchema = (o) => z.array(
|
|
107
198
|
o({
|
|
@@ -159,7 +250,7 @@ var makeCommerceSchema = (o, provider) => o({
|
|
|
159
250
|
});
|
|
160
251
|
var commerceSchemaStrict = makeCommerceSchema(strictFactory, commerceProviderStrict);
|
|
161
252
|
var commerceSchema = makeCommerceSchema(tolerantFactory, commerceProviderTolerant);
|
|
162
|
-
var makeContentFileSchema = (o, commerceProvider) => o({
|
|
253
|
+
var makeContentFileSchema = (o, commerceProvider, menu) => o({
|
|
163
254
|
schemaVersion: z.literal(1),
|
|
164
255
|
site: o({ id: z.uuid(), slug: z.string().min(1) }),
|
|
165
256
|
publishedAt: z.iso.datetime(),
|
|
@@ -176,12 +267,20 @@ var makeContentFileSchema = (o, commerceProvider) => o({
|
|
|
176
267
|
modules: o({
|
|
177
268
|
siteSettings: makeSiteSettingsSchema(o),
|
|
178
269
|
branding: makeBrandingSchema(o),
|
|
179
|
-
menu
|
|
270
|
+
menu,
|
|
180
271
|
commerce: makeCommerceSchema(o, commerceProvider).optional()
|
|
181
272
|
})
|
|
182
273
|
});
|
|
183
|
-
var contentFileSchemaStrict = makeContentFileSchema(
|
|
184
|
-
|
|
274
|
+
var contentFileSchemaStrict = makeContentFileSchema(
|
|
275
|
+
strictFactory,
|
|
276
|
+
commerceProviderStrict,
|
|
277
|
+
menuSchemaStrict
|
|
278
|
+
);
|
|
279
|
+
var contentFileSchema = makeContentFileSchema(
|
|
280
|
+
tolerantFactory,
|
|
281
|
+
commerceProviderTolerant,
|
|
282
|
+
menuSchema
|
|
283
|
+
);
|
|
185
284
|
function canonicalize(payload) {
|
|
186
285
|
const out = serialize(payload);
|
|
187
286
|
if (out === void 0) {
|
|
@@ -246,6 +345,10 @@ export {
|
|
|
246
345
|
siteSettingsSchema,
|
|
247
346
|
brandingSchemaStrict,
|
|
248
347
|
brandingSchema,
|
|
348
|
+
MAX_OPTION_GROUPS_PER_ITEM,
|
|
349
|
+
MAX_OPTIONS_PER_GROUP,
|
|
350
|
+
MAX_OPTION_SELECT,
|
|
351
|
+
MAX_OPTION_PRICE_DELTA,
|
|
249
352
|
menuSchemaStrict,
|
|
250
353
|
menuSchema,
|
|
251
354
|
COMMERCE_PROVIDERS,
|
|
@@ -258,4 +361,4 @@ export {
|
|
|
258
361
|
contentFileSchema,
|
|
259
362
|
canonicalize
|
|
260
363
|
};
|
|
261
|
-
//# sourceMappingURL=chunk-
|
|
364
|
+
//# sourceMappingURL=chunk-7B6AENRI.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//\n// OPTION GROUPS (1.3.0, Online Commerce C3): one concept for sizes and add-ons.\n// A size is a group with min 1 / max 1; an add-on list is min 0 / max n. Groups\n// are site-level and reusable — defined once in `optionGroups`, attached to an\n// item by id in `optionGroupIds`, in display order. Both keys are OPTIONAL and\n// ABSENT when empty, so every pre-1.3.0 snapshot (and every site that uses no\n// options) stays valid — ARCHITECTURE.md § 5.\n//\n// Price rule: a line's unit price = item `price.amount` + Σ chosen options'\n// `priceDelta`. Deltas carry no currency of their own: they are integer minor\n// units in the currency of the ITEM they are chosen on.\n//\n// Field bounds below hold in BOTH modes and mirror the DB CHECKs. The rules that\n// relate objects to each other (references resolve, counts, required groups,\n// one currency per group) are the producer's PUBLISH GATE and live only in the\n// strict schema — see `checkMenuOptions`.\n\n/** At most this many option groups on one item (publish-gated, not DB-enforced). */\nexport const MAX_OPTION_GROUPS_PER_ITEM = 10;\n/** At most this many published options in one group (publish-gated). */\nexport const MAX_OPTIONS_PER_GROUP = 30;\n/** Upper bound of a group's `min`/`max` (mirrors the DB CHECK). */\nexport const MAX_OPTION_SELECT = 30;\n/** Upper bound of an option's `priceDelta`, minor units (mirrors the DB CHECK). */\nexport const MAX_OPTION_PRICE_DELTA = 100_000;\n\nconst makeMenuOptionSchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(120),\n /** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */\n priceDelta: z.number().int().min(0).max(MAX_OPTION_PRICE_DELTA),\n isAvailable: z.boolean(),\n sortOrder: z.number().int().min(0).max(100_000),\n });\n\nconst makeMenuOptionGroupSchema = (o: ObjectFactory) =>\n o({\n id: z.uuid(),\n name: z.string().min(1).max(120),\n /** Fewest options a customer must choose; ≥ 1 makes the group required. */\n min: z.number().int().min(0).max(MAX_OPTION_SELECT),\n /** Most options a customer may choose. */\n max: z.number().int().min(0).max(MAX_OPTION_SELECT),\n sortOrder: z.number().int().min(0).max(100_000),\n /** Archived options are absent; an unavailable one is present with `isAvailable: false`. */\n options: z.array(makeMenuOptionSchema(o)),\n });\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 /** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */\n optionGroupIds: z.array(z.uuid()).optional(),\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 /** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */\n optionGroups: z.array(makeMenuOptionGroupSchema(o)).optional(),\n });\n\ntype MenuShape = z.output<ReturnType<typeof makeMenuSchema>>;\n\n/**\n * The menu's PUBLISH GATE for option groups (C3) — strict schema only. A draft\n * may break any of these and still be saved; it cannot be published. Every\n * message names the group or item, because it is shown to the merchant verbatim\n * by the publish screen.\n *\n * Strict-only on purpose: these are rules about what the PRODUCER may emit, and\n * a consumer must not reject a whole payload because a later release relaxed one\n * (say, more groups per item).\n */\nfunction checkMenuOptions(menu: MenuShape, ctx: z.RefinementCtx): void {\n const issue = (path: (string | number)[], message: string) =>\n ctx.addIssue({ code: 'custom', path, message });\n\n const groups = menu.optionGroups ?? [];\n const groupNames = new Map<string, string>();\n const optionIds = new Set<string>();\n groups.forEach((group, g) => {\n const label = `Option group \"${group.name}\"`;\n if (groupNames.has(group.id)) issue(['optionGroups', g, 'id'], `${label} repeats another group's id`);\n groupNames.set(group.id, group.name);\n\n if (group.min > group.max) {\n issue(['optionGroups', g, 'min'], `${label} requires at least ${group.min} but allows at most ${group.max}`);\n }\n if (group.options.length > MAX_OPTIONS_PER_GROUP) {\n issue(\n ['optionGroups', g, 'options'],\n `${label} has ${group.options.length} options; the limit is ${MAX_OPTIONS_PER_GROUP}`,\n );\n }\n const available = group.options.filter((option) => option.isAvailable).length;\n if (group.min >= 1 && available === 0) {\n issue(['optionGroups', g, 'options'], `${label} is required but has no available options`);\n } else if (group.max > available) {\n issue(\n ['optionGroups', g, 'max'],\n `${label} allows up to ${group.max} choices but has only ${available} available option${available === 1 ? '' : 's'}`,\n );\n }\n group.options.forEach((option, k) => {\n if (optionIds.has(option.id)) {\n issue(['optionGroups', g, 'options', k, 'id'], `${label}: option \"${option.name}\" repeats another option's id`);\n }\n optionIds.add(option.id);\n });\n });\n\n // Deltas are in the item's currency, so one group cannot serve two currencies.\n const firstUse = new Map<string, { currency: string; itemName: string }>();\n menu.categories.forEach((category, c) => {\n category.items.forEach((item, i) => {\n const ids = item.optionGroupIds ?? [];\n const path = ['categories', c, 'items', i, 'optionGroupIds'];\n if (ids.length > MAX_OPTION_GROUPS_PER_ITEM) {\n issue(path, `Item \"${item.name}\" has ${ids.length} option groups; the limit is ${MAX_OPTION_GROUPS_PER_ITEM}`);\n }\n const seen = new Set<string>();\n ids.forEach((id, k) => {\n if (seen.has(id)) issue([...path, k], `Item \"${item.name}\" lists the same option group twice`);\n seen.add(id);\n const groupName = groupNames.get(id);\n if (groupName === undefined) {\n issue([...path, k], `Item \"${item.name}\" uses an option group that is not published`);\n return;\n }\n const prior = firstUse.get(id);\n if (!prior) {\n firstUse.set(id, { currency: item.price.currency, itemName: item.name });\n } else if (prior.currency !== item.price.currency) {\n issue(\n [...path, k],\n `Option group \"${groupName}\" is used by items in two currencies: ` +\n `\"${prior.itemName}\" (${prior.currency}) and \"${item.name}\" (${item.price.currency})`,\n );\n }\n });\n });\n });\n}\n\nexport const menuSchemaStrict = makeMenuSchema(strictFactory).superRefine(checkMenuOptions);\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\n// `menu` is passed in rather than built from `o` for the same reason as the\n// provider: the two modes differ beyond the object factory — the strict menu\n// carries the option-group publish gate (`checkMenuOptions`), the tolerant one\n// does not.\nconst makeContentFileSchema = <P extends CommerceProviderSchema, M extends typeof menuSchema>(\n o: ObjectFactory,\n commerceProvider: P,\n menu: M,\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,\n commerce: makeCommerceSchema(o, commerceProvider).optional(),\n }),\n });\n\n/** Producer gate: the dashboard validates its own output against this. */\nexport const contentFileSchemaStrict = makeContentFileSchema(\n strictFactory,\n commerceProviderStrict,\n menuSchemaStrict,\n);\n/** Public consumer schema: tolerant of additive v1 changes. */\nexport const contentFileSchema = makeContentFileSchema(\n tolerantFactory,\n commerceProviderTolerant,\n menuSchema,\n);\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 MenuOptionGroup = NonNullable<Menu['optionGroups']>[number];\nexport type MenuOption = MenuOptionGroup['options'][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
package/dist/index.d.ts
CHANGED
|
@@ -121,6 +121,14 @@ declare const brandingSchema: z.ZodObject<{
|
|
|
121
121
|
body: z.ZodOptional<z.ZodString>;
|
|
122
122
|
}, z.core.$strip>>;
|
|
123
123
|
}, z.core.$strip>;
|
|
124
|
+
/** At most this many option groups on one item (publish-gated, not DB-enforced). */
|
|
125
|
+
declare const MAX_OPTION_GROUPS_PER_ITEM = 10;
|
|
126
|
+
/** At most this many published options in one group (publish-gated). */
|
|
127
|
+
declare const MAX_OPTIONS_PER_GROUP = 30;
|
|
128
|
+
/** Upper bound of a group's `min`/`max` (mirrors the DB CHECK). */
|
|
129
|
+
declare const MAX_OPTION_SELECT = 30;
|
|
130
|
+
/** Upper bound of an option's `priceDelta`, minor units (mirrors the DB CHECK). */
|
|
131
|
+
declare const MAX_OPTION_PRICE_DELTA = 100000;
|
|
124
132
|
declare const menuSchemaStrict: z.ZodObject<{
|
|
125
133
|
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
126
134
|
categories: z.ZodArray<z.ZodObject<{
|
|
@@ -142,8 +150,29 @@ declare const menuSchemaStrict: z.ZodObject<{
|
|
|
142
150
|
tags: z.ZodArray<z.ZodString>;
|
|
143
151
|
sortOrder: z.ZodNumber;
|
|
144
152
|
isAvailable: z.ZodBoolean;
|
|
153
|
+
/** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
|
|
154
|
+
optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
|
|
145
155
|
}, z.core.$strip>>;
|
|
146
156
|
}, z.core.$strip>>;
|
|
157
|
+
/** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
|
|
158
|
+
optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
159
|
+
id: z.ZodUUID;
|
|
160
|
+
name: z.ZodString;
|
|
161
|
+
/** Fewest options a customer must choose; ≥ 1 makes the group required. */
|
|
162
|
+
min: z.ZodNumber;
|
|
163
|
+
/** Most options a customer may choose. */
|
|
164
|
+
max: z.ZodNumber;
|
|
165
|
+
sortOrder: z.ZodNumber;
|
|
166
|
+
/** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
|
|
167
|
+
options: z.ZodArray<z.ZodObject<{
|
|
168
|
+
id: z.ZodUUID;
|
|
169
|
+
name: z.ZodString;
|
|
170
|
+
/** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
|
|
171
|
+
priceDelta: z.ZodNumber;
|
|
172
|
+
isAvailable: z.ZodBoolean;
|
|
173
|
+
sortOrder: z.ZodNumber;
|
|
174
|
+
}, z.core.$strip>>;
|
|
175
|
+
}, z.core.$strip>>>;
|
|
147
176
|
}, z.core.$strip>;
|
|
148
177
|
declare const menuSchema: z.ZodObject<{
|
|
149
178
|
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
@@ -166,8 +195,29 @@ declare const menuSchema: z.ZodObject<{
|
|
|
166
195
|
tags: z.ZodArray<z.ZodString>;
|
|
167
196
|
sortOrder: z.ZodNumber;
|
|
168
197
|
isAvailable: z.ZodBoolean;
|
|
198
|
+
/** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
|
|
199
|
+
optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
|
|
169
200
|
}, z.core.$strip>>;
|
|
170
201
|
}, z.core.$strip>>;
|
|
202
|
+
/** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
|
|
203
|
+
optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
204
|
+
id: z.ZodUUID;
|
|
205
|
+
name: z.ZodString;
|
|
206
|
+
/** Fewest options a customer must choose; ≥ 1 makes the group required. */
|
|
207
|
+
min: z.ZodNumber;
|
|
208
|
+
/** Most options a customer may choose. */
|
|
209
|
+
max: z.ZodNumber;
|
|
210
|
+
sortOrder: z.ZodNumber;
|
|
211
|
+
/** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
|
|
212
|
+
options: z.ZodArray<z.ZodObject<{
|
|
213
|
+
id: z.ZodUUID;
|
|
214
|
+
name: z.ZodString;
|
|
215
|
+
/** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
|
|
216
|
+
priceDelta: z.ZodNumber;
|
|
217
|
+
isAvailable: z.ZodBoolean;
|
|
218
|
+
sortOrder: z.ZodNumber;
|
|
219
|
+
}, z.core.$strip>>;
|
|
220
|
+
}, z.core.$strip>>>;
|
|
171
221
|
}, z.core.$strip>;
|
|
172
222
|
/** Providers the PRODUCER may publish. Grows one member per phase (OC-2). */
|
|
173
223
|
declare const COMMERCE_PROVIDERS: readonly ["none", "other"];
|
|
@@ -311,8 +361,29 @@ declare const contentFileSchemaStrict: z.ZodObject<{
|
|
|
311
361
|
tags: z.ZodArray<z.ZodString>;
|
|
312
362
|
sortOrder: z.ZodNumber;
|
|
313
363
|
isAvailable: z.ZodBoolean;
|
|
364
|
+
/** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
|
|
365
|
+
optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
|
|
314
366
|
}, z.core.$strip>>;
|
|
315
367
|
}, z.core.$strip>>;
|
|
368
|
+
/** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
|
|
369
|
+
optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
370
|
+
id: z.ZodUUID;
|
|
371
|
+
name: z.ZodString;
|
|
372
|
+
/** Fewest options a customer must choose; ≥ 1 makes the group required. */
|
|
373
|
+
min: z.ZodNumber;
|
|
374
|
+
/** Most options a customer may choose. */
|
|
375
|
+
max: z.ZodNumber;
|
|
376
|
+
sortOrder: z.ZodNumber;
|
|
377
|
+
/** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
|
|
378
|
+
options: z.ZodArray<z.ZodObject<{
|
|
379
|
+
id: z.ZodUUID;
|
|
380
|
+
name: z.ZodString;
|
|
381
|
+
/** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
|
|
382
|
+
priceDelta: z.ZodNumber;
|
|
383
|
+
isAvailable: z.ZodBoolean;
|
|
384
|
+
sortOrder: z.ZodNumber;
|
|
385
|
+
}, z.core.$strip>>;
|
|
386
|
+
}, z.core.$strip>>>;
|
|
316
387
|
}, z.core.$strip>;
|
|
317
388
|
commerce: z.ZodOptional<z.ZodObject<{
|
|
318
389
|
provider: z.ZodEnum<{
|
|
@@ -417,8 +488,29 @@ declare const contentFileSchema: z.ZodObject<{
|
|
|
417
488
|
tags: z.ZodArray<z.ZodString>;
|
|
418
489
|
sortOrder: z.ZodNumber;
|
|
419
490
|
isAvailable: z.ZodBoolean;
|
|
491
|
+
/** 1.3.0: ids into `menu.optionGroups`, in display order. Absent ≡ `[]`. */
|
|
492
|
+
optionGroupIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
|
|
420
493
|
}, z.core.$strip>>;
|
|
421
494
|
}, z.core.$strip>>;
|
|
495
|
+
/** 1.3.0: the site's option groups (archived ones absent). Absent ≡ `[]`. */
|
|
496
|
+
optionGroups: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
497
|
+
id: z.ZodUUID;
|
|
498
|
+
name: z.ZodString;
|
|
499
|
+
/** Fewest options a customer must choose; ≥ 1 makes the group required. */
|
|
500
|
+
min: z.ZodNumber;
|
|
501
|
+
/** Most options a customer may choose. */
|
|
502
|
+
max: z.ZodNumber;
|
|
503
|
+
sortOrder: z.ZodNumber;
|
|
504
|
+
/** Archived options are absent; an unavailable one is present with `isAvailable: false`. */
|
|
505
|
+
options: z.ZodArray<z.ZodObject<{
|
|
506
|
+
id: z.ZodUUID;
|
|
507
|
+
name: z.ZodString;
|
|
508
|
+
/** Integer minor units, ≥ 0, in the currency of the item it is chosen on. */
|
|
509
|
+
priceDelta: z.ZodNumber;
|
|
510
|
+
isAvailable: z.ZodBoolean;
|
|
511
|
+
sortOrder: z.ZodNumber;
|
|
512
|
+
}, z.core.$strip>>;
|
|
513
|
+
}, z.core.$strip>>>;
|
|
422
514
|
}, z.core.$strip>;
|
|
423
515
|
commerce: z.ZodOptional<z.ZodObject<{
|
|
424
516
|
provider: z.ZodUnion<readonly [z.ZodEnum<{
|
|
@@ -456,6 +548,8 @@ type Branding = z.infer<typeof brandingSchemaStrict>;
|
|
|
456
548
|
type Menu = z.infer<typeof menuSchemaStrict>;
|
|
457
549
|
type MenuCategory = Menu['categories'][number];
|
|
458
550
|
type MenuItem = MenuCategory['items'][number];
|
|
551
|
+
type MenuOptionGroup = NonNullable<Menu['optionGroups']>[number];
|
|
552
|
+
type MenuOption = MenuOptionGroup['options'][number];
|
|
459
553
|
type Commerce = z.infer<typeof commerceSchemaStrict>;
|
|
460
554
|
type CommerceDay = (typeof COMMERCE_DAYS)[number];
|
|
461
555
|
type CommerceHours = Commerce['hours'];
|
|
@@ -472,4 +566,4 @@ type ConsumerContentFile = z.infer<typeof contentFileSchema>;
|
|
|
472
566
|
type ConsumerCommerce = z.infer<typeof commerceSchema>;
|
|
473
567
|
declare function canonicalize(payload: unknown): string;
|
|
474
568
|
|
|
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 };
|
|
569
|
+
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, MAX_OPTIONS_PER_GROUP, MAX_OPTION_GROUPS_PER_ITEM, MAX_OPTION_PRICE_DELTA, MAX_OPTION_SELECT, type Menu, type MenuCategory, type MenuItem, type MenuOption, type MenuOptionGroup, 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
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
LOCALE_PATTERN,
|
|
13
13
|
LOCALE_PATTERN_SOURCE,
|
|
14
14
|
MAX_CONTENT_BYTES,
|
|
15
|
+
MAX_OPTIONS_PER_GROUP,
|
|
16
|
+
MAX_OPTION_GROUPS_PER_ITEM,
|
|
17
|
+
MAX_OPTION_PRICE_DELTA,
|
|
18
|
+
MAX_OPTION_SELECT,
|
|
15
19
|
SCHEMA_VERSION,
|
|
16
20
|
TIME_24H_PATTERN,
|
|
17
21
|
TIME_24H_PATTERN_SOURCE,
|
|
@@ -28,7 +32,7 @@ import {
|
|
|
28
32
|
menuSchemaStrict,
|
|
29
33
|
siteSettingsSchema,
|
|
30
34
|
siteSettingsSchemaStrict
|
|
31
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-7B6AENRI.js";
|
|
32
36
|
export {
|
|
33
37
|
COMMERCE_DAYS,
|
|
34
38
|
COMMERCE_PROVIDERS,
|
|
@@ -43,6 +47,10 @@ export {
|
|
|
43
47
|
LOCALE_PATTERN,
|
|
44
48
|
LOCALE_PATTERN_SOURCE,
|
|
45
49
|
MAX_CONTENT_BYTES,
|
|
50
|
+
MAX_OPTIONS_PER_GROUP,
|
|
51
|
+
MAX_OPTION_GROUPS_PER_ITEM,
|
|
52
|
+
MAX_OPTION_PRICE_DELTA,
|
|
53
|
+
MAX_OPTION_SELECT,
|
|
46
54
|
SCHEMA_VERSION,
|
|
47
55
|
TIME_24H_PATTERN,
|
|
48
56
|
TIME_24H_PATTERN_SOURCE,
|
package/fixtures/v1/full.json
CHANGED
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"imageUrl": "https://example.supabase.co/storage/v1/object/public/assets/org_01example7fixture0000000000/b7e6a1d2-3c4f-4a5b-9e8d-1f2a3b4c5d6e/fd61a03af4f77d870fc21e05e7e80678095c92d808cfb3b5c279ee04c74aca13.webp",
|
|
68
68
|
"tags": ["signature", "gluten-free"],
|
|
69
69
|
"sortOrder": 0,
|
|
70
|
-
"isAvailable": true
|
|
70
|
+
"isAvailable": true,
|
|
71
|
+
"optionGroupIds": ["c0693af5-7491-4b0b-bf44-b26acc3e2bee", "9c80e14f-f97a-41fc-87dc-ddf624aea9da"]
|
|
71
72
|
},
|
|
72
73
|
{
|
|
73
74
|
"id": "6f5e4d3c-2b1a-4098-a7b6-c5d4e3f2a1b0",
|
|
@@ -79,7 +80,8 @@
|
|
|
79
80
|
},
|
|
80
81
|
"tags": ["gluten-free"],
|
|
81
82
|
"sortOrder": 1,
|
|
82
|
-
"isAvailable": true
|
|
83
|
+
"isAvailable": true,
|
|
84
|
+
"optionGroupIds": ["c0693af5-7491-4b0b-bf44-b26acc3e2bee", "9c80e14f-f97a-41fc-87dc-ddf624aea9da"]
|
|
83
85
|
},
|
|
84
86
|
{
|
|
85
87
|
"id": "7a6b5c4d-3e2f-4109-b8a7-d6c5b4a3f2e1",
|
|
@@ -110,7 +112,8 @@
|
|
|
110
112
|
},
|
|
111
113
|
"tags": ["cold"],
|
|
112
114
|
"sortOrder": 0,
|
|
113
|
-
"isAvailable": true
|
|
115
|
+
"isAvailable": true,
|
|
116
|
+
"optionGroupIds": ["1f0a2da4-8111-47a9-be63-3ba3d8b820c1"]
|
|
114
117
|
},
|
|
115
118
|
{
|
|
116
119
|
"id": "9c8b7a6d-5e4f-4321-90c9-f8e7d6c5b4a3",
|
|
@@ -125,6 +128,84 @@
|
|
|
125
128
|
}
|
|
126
129
|
]
|
|
127
130
|
}
|
|
131
|
+
],
|
|
132
|
+
"optionGroups": [
|
|
133
|
+
{
|
|
134
|
+
"id": "c0693af5-7491-4b0b-bf44-b26acc3e2bee",
|
|
135
|
+
"name": "Size",
|
|
136
|
+
"min": 1,
|
|
137
|
+
"max": 1,
|
|
138
|
+
"sortOrder": 0,
|
|
139
|
+
"options": [
|
|
140
|
+
{
|
|
141
|
+
"id": "f02dd4ed-59bf-4285-928d-2c2f03d78c63",
|
|
142
|
+
"name": "Regular",
|
|
143
|
+
"priceDelta": 0,
|
|
144
|
+
"isAvailable": true,
|
|
145
|
+
"sortOrder": 0
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "a70e5eea-5e19-4ed5-bc86-f0ec60715ea2",
|
|
149
|
+
"name": "Large",
|
|
150
|
+
"priceDelta": 200,
|
|
151
|
+
"isAvailable": true,
|
|
152
|
+
"sortOrder": 1
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "9c80e14f-f97a-41fc-87dc-ddf624aea9da",
|
|
158
|
+
"name": "Extras",
|
|
159
|
+
"min": 0,
|
|
160
|
+
"max": 2,
|
|
161
|
+
"sortOrder": 1,
|
|
162
|
+
"options": [
|
|
163
|
+
{
|
|
164
|
+
"id": "f1c60f21-f159-4f3c-90fa-94fb5c5cb3b6",
|
|
165
|
+
"name": "Extra brisket",
|
|
166
|
+
"priceDelta": 300,
|
|
167
|
+
"isAvailable": true,
|
|
168
|
+
"sortOrder": 0
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"id": "bd120953-6ed4-482c-9764-2a5bbf96eec2",
|
|
172
|
+
"name": "Extra noodles",
|
|
173
|
+
"priceDelta": 150,
|
|
174
|
+
"isAvailable": true,
|
|
175
|
+
"sortOrder": 1
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"id": "8d4fcac1-14f8-494b-83a4-ba64717fc8bb",
|
|
179
|
+
"name": "Poached egg",
|
|
180
|
+
"priceDelta": 100,
|
|
181
|
+
"isAvailable": false,
|
|
182
|
+
"sortOrder": 2
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"id": "1f0a2da4-8111-47a9-be63-3ba3d8b820c1",
|
|
188
|
+
"name": "Milk",
|
|
189
|
+
"min": 0,
|
|
190
|
+
"max": 1,
|
|
191
|
+
"sortOrder": 2,
|
|
192
|
+
"options": [
|
|
193
|
+
{
|
|
194
|
+
"id": "0d2377b3-03b1-467c-a007-b151fb156646",
|
|
195
|
+
"name": "Condensed milk",
|
|
196
|
+
"priceDelta": 0,
|
|
197
|
+
"isAvailable": true,
|
|
198
|
+
"sortOrder": 0
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "4c72ddc8-3416-46a7-b771-77e5f09f15ec",
|
|
202
|
+
"name": "Oat milk",
|
|
203
|
+
"priceDelta": 75,
|
|
204
|
+
"isAvailable": true,
|
|
205
|
+
"sortOrder": 1
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
128
209
|
]
|
|
129
210
|
},
|
|
130
211
|
"commerce": {
|
|
@@ -136,12 +217,36 @@
|
|
|
136
217
|
"acceptsOnlineOrders": true,
|
|
137
218
|
"prepTimeMinutes": 25,
|
|
138
219
|
"hours": [
|
|
139
|
-
{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{
|
|
220
|
+
{
|
|
221
|
+
"day": "mon",
|
|
222
|
+
"opens": "11:00",
|
|
223
|
+
"closes": "21:00"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"day": "tue",
|
|
227
|
+
"opens": "11:00",
|
|
228
|
+
"closes": "21:00"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"day": "wed",
|
|
232
|
+
"opens": "11:00",
|
|
233
|
+
"closes": "21:00"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"day": "thu",
|
|
237
|
+
"opens": "11:00",
|
|
238
|
+
"closes": "21:00"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"day": "fri",
|
|
242
|
+
"opens": "11:00",
|
|
243
|
+
"closes": "01:30"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
"day": "sat",
|
|
247
|
+
"opens": "10:00",
|
|
248
|
+
"closes": "01:30"
|
|
249
|
+
}
|
|
145
250
|
]
|
|
146
251
|
}
|
|
147
252
|
}
|
package/fixtures/v1/hashes.json
CHANGED
|
@@ -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. REGENERATED
|
|
3
|
-
"full.json": "sha256:
|
|
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 TWICE: (1) 2026-08-25, full.json gained the optional `commerce` module (phase-7 Slice 2, signed off in the proposal's Slice 2 line); (2) 2026-10-06, full.json gained the optional `menu.optionGroups` / `optionGroupIds` (content-schemas 1.3.0, Online Commerce C3 T2, signed off with the approved spec's 'fixture and hash updates'; the previous full.json hash is kept in test/contract.test.ts as FULL_1_2_0_HASH — the rollback shape). minimal.json is UNCHANGED and must stay so: commerce and option groups are optional, so the minimal payload never carries them.",
|
|
3
|
+
"full.json": "sha256:b3262a49e000fec59b067f7aec07bc0b31855086f121a16a02af394da616ff4d",
|
|
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.
|
|
3
|
+
"version": "1.3.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/**\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"]}
|