@lamgig/content-schemas 1.0.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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/chunk-KBPOR3WL.js +186 -0
- package/dist/chunk-KBPOR3WL.js.map +1 -0
- package/dist/hash.d.ts +12 -0
- package/dist/hash.js +14 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +318 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/fixtures/v1/full.json +131 -0
- package/fixtures/v1/hashes.json +5 -0
- package/fixtures/v1/minimal.json +24 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LamGig
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @lamgig/content-schemas
|
|
2
|
+
|
|
3
|
+
The **published-JSON contract**: the Zod schemas, canonical serialization, and fixtures that
|
|
4
|
+
define what a LamGig site publishes and what every consumer (websites, apps) may rely on.
|
|
5
|
+
Locked in Phase 2 T1 ([docs/phases/phase-2.md](../../docs/phases/phase-2.md)); changing any
|
|
6
|
+
locked decision is a `schemaVersion` discussion, not an edit.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @lamgig/content-schemas
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Published to npm as a **public** package from this workspace (`npm publish -w
|
|
15
|
+
@lamgig/content-schemas`). It is the contract consumers depend on — websites and apps rendering
|
|
16
|
+
a LamGig site — so treat its version as a public API:
|
|
17
|
+
|
|
18
|
+
| Change | npm bump |
|
|
19
|
+
|---|---|
|
|
20
|
+
| New **optional** field or new **optional** module key | minor |
|
|
21
|
+
| Anything that bumps `SCHEMA_VERSION` (required addition, removal, type change) | major |
|
|
22
|
+
| Docs, fixtures-neutral internals | patch |
|
|
23
|
+
|
|
24
|
+
**Consumers import the tolerant exports** (`contentFileSchema`), never the `*Strict` ones — those
|
|
25
|
+
are the dashboard's gate on its own output. See the compatibility rule below.
|
|
26
|
+
|
|
27
|
+
## Two parse modes, one definition
|
|
28
|
+
|
|
29
|
+
Every schema exists as a pair built from a single definition (an injected object factory —
|
|
30
|
+
zod's `.strip()`/`.passthrough()` do not recurse, so per-tree factories are the only way to
|
|
31
|
+
get uniform strictness):
|
|
32
|
+
|
|
33
|
+
| Export | Mode | Who uses it |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `contentFileSchemaStrict` (+ `*SchemaStrict` per module) | `.strict()` everywhere — unknown keys are **errors** | The dashboard, validating **its own output** before publishing (producer gate) |
|
|
36
|
+
| `contentFileSchema` (+ `*Schema` per module) | tolerant — unknown object keys **stripped**, unknown module keys **ignored** | Shipped consumers |
|
|
37
|
+
|
|
38
|
+
## Compatibility rule (schemaVersion 1)
|
|
39
|
+
|
|
40
|
+
- Within `schemaVersion: 1`, new fields **and** new module keys MUST be optional or defaulted
|
|
41
|
+
for consumers. A consumer built yesterday must accept a payload published tomorrow.
|
|
42
|
+
- Adding any **required** field or required module is breaking — retained/rollback snapshots
|
|
43
|
+
will not contain it — and bumps `schemaVersion`.
|
|
44
|
+
- Consumers MUST use the tolerant schemas and ignore unknown fields. The forward-compat tests
|
|
45
|
+
in [`test/contract.test.ts`](test/contract.test.ts) enforce exactly this: the v1 tolerant
|
|
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.
|
|
49
|
+
|
|
50
|
+
## Canonical serialization
|
|
51
|
+
|
|
52
|
+
`canonicalize(payload)` produces recursively key-sorted JSON with no whitespace:
|
|
53
|
+
|
|
54
|
+
- arrays keep their order; object keys sort by code unit (`Array.prototype.sort`);
|
|
55
|
+
- object properties with value `undefined` are omitted;
|
|
56
|
+
- `undefined` elsewhere, non-finite numbers, bigints, functions, symbols **throw** — nothing
|
|
57
|
+
is silently coerced.
|
|
58
|
+
|
|
59
|
+
`hashContent(payload)` = `"sha256:" + hex(sha256(utf8(canonicalize(payload))))`. The publisher
|
|
60
|
+
persists this as `payload_hash`; consumer tests must use the same functions so hash comparison
|
|
61
|
+
is byte-exact. `hashContent` lives in the node-only subpath **`@lamgig/content-schemas/hash`**
|
|
62
|
+
(it needs `node:crypto`); the main entry stays importable from client bundles. Max canonical
|
|
63
|
+
payload size: `MAX_CONTENT_BYTES` (1 MiB), enforced by the publisher.
|
|
64
|
+
|
|
65
|
+
## Asset URLs: paths in the DB, URLs in the payload
|
|
66
|
+
|
|
67
|
+
Published payloads embed **absolute** asset URLs (`logoUrl`, `faviconUrl`, `imageUrl`) built by
|
|
68
|
+
the dashboard's single URL source, `apps/dashboard/src/lib/content-urls.ts`
|
|
69
|
+
(`storagePublicUrl('assets', path)` — Spike B contract). The database stores **storage paths**,
|
|
70
|
+
and the SQL gather functions emit them under path-named keys (`logoPath`/`faviconPath`/
|
|
71
|
+
`imagePath`) because SQL cannot know the environment-dependent public base; at publish time the
|
|
72
|
+
mapping to URLs happens, and the path keys are stripped, **before** strict validation.
|
|
73
|
+
Decision recorded 2026-07-22 (founder-approved).
|
|
74
|
+
|
|
75
|
+
Since Phase 6 (D28) that mapping belongs to **each module's own `publisher.ts`**, as the optional
|
|
76
|
+
`compose` of the `Publisher` contract — not to `modules/publishing/compose.ts`, which now iterates
|
|
77
|
+
the publisher registry and names no module. A module that stores no storage paths declares no
|
|
78
|
+
`compose`. The producer side of this contract is pinned by
|
|
79
|
+
`apps/dashboard/src/modules/publishing/compose.test.ts`, which feeds gathered PATHS in and requires
|
|
80
|
+
`fixtures/v1/full.json` and its checked-in hash back out, byte for byte.
|
|
81
|
+
|
|
82
|
+
**Dependency direction: this package never imports from the app.** Fixtures contain static
|
|
83
|
+
expected URL strings (base `https://example.supabase.co`); a dashboard-side test asserts the
|
|
84
|
+
URL builder reproduces them exactly.
|
|
85
|
+
|
|
86
|
+
## Shared patterns = DB CHECKs
|
|
87
|
+
|
|
88
|
+
`LOCALE_PATTERN_SOURCE` (`^[a-z]{2}(-[A-Z]{2})?$` — the v1 BCP-47 subset `ll` / `ll-CC`),
|
|
89
|
+
`HEX_COLOR_PATTERN_SOURCE`, and `CURRENCY_PATTERN_SOURCE` are the exact strings used by the
|
|
90
|
+
corresponding DB CHECK constraints (`supabase/migrations/*_content_*.sql`), so the contract
|
|
91
|
+
and the database can never disagree. Widening the locale subset later is additive.
|
|
92
|
+
|
|
93
|
+
## Fixtures (`fixtures/v1/`)
|
|
94
|
+
|
|
95
|
+
- `full.json` — realistic restaurant, every optional field populated.
|
|
96
|
+
- `minimal.json` — empty menu, no optional fields; also the reference shape for the
|
|
97
|
+
fresh-site validity check (a newly provisioned site must gather to this shape).
|
|
98
|
+
- `hashes.json` — checked-in `hashContent` snapshots; a mismatch means serialization or a
|
|
99
|
+
fixture changed, both locked decisions.
|
|
100
|
+
|
|
101
|
+
## Test
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
npm test -w @lamgig/content-schemas # vitest
|
|
105
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var SCHEMA_VERSION = 1;
|
|
4
|
+
var MAX_CONTENT_BYTES = 1048576;
|
|
5
|
+
var LOCALE_PATTERN_SOURCE = "^[a-z]{2}(-[A-Z]{2})?$";
|
|
6
|
+
var LOCALE_PATTERN = new RegExp(LOCALE_PATTERN_SOURCE);
|
|
7
|
+
var HEX_COLOR_PATTERN_SOURCE = "^#[0-9A-Fa-f]{6}$";
|
|
8
|
+
var HEX_COLOR_PATTERN = new RegExp(HEX_COLOR_PATTERN_SOURCE);
|
|
9
|
+
var CURRENCY_PATTERN_SOURCE = "^[A-Z]{3}$";
|
|
10
|
+
var CURRENCY_PATTERN = new RegExp(CURRENCY_PATTERN_SOURCE);
|
|
11
|
+
var COUNTRY_PATTERN_SOURCE = "^[A-Z]{2}$";
|
|
12
|
+
var COUNTRY_PATTERN = new RegExp(COUNTRY_PATTERN_SOURCE);
|
|
13
|
+
function isIanaTimeZone(tz) {
|
|
14
|
+
try {
|
|
15
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var strictFactory = z.strictObject;
|
|
22
|
+
var tolerantFactory = z.object;
|
|
23
|
+
var makeSiteSettingsSchema = (o) => o({
|
|
24
|
+
siteName: z.string().min(1).max(120),
|
|
25
|
+
tagline: z.string().max(200).optional(),
|
|
26
|
+
contact: o({
|
|
27
|
+
email: z.email().optional(),
|
|
28
|
+
phone: z.string().min(1).optional(),
|
|
29
|
+
address: o({
|
|
30
|
+
line1: z.string().min(1),
|
|
31
|
+
line2: z.string().min(1).optional(),
|
|
32
|
+
city: z.string().min(1),
|
|
33
|
+
region: z.string().min(1),
|
|
34
|
+
postalCode: z.string().min(1),
|
|
35
|
+
country: z.string().regex(COUNTRY_PATTERN)
|
|
36
|
+
}).optional()
|
|
37
|
+
}),
|
|
38
|
+
// Explicit known keys (NOT a record): strict rejects unknown platforms,
|
|
39
|
+
// tolerant strips them — adding a platform later is additive.
|
|
40
|
+
socialLinks: o({
|
|
41
|
+
facebook: z.url().optional(),
|
|
42
|
+
instagram: z.url().optional(),
|
|
43
|
+
tiktok: z.url().optional(),
|
|
44
|
+
x: z.url().optional(),
|
|
45
|
+
youtube: z.url().optional(),
|
|
46
|
+
yelp: z.url().optional(),
|
|
47
|
+
google: z.url().optional()
|
|
48
|
+
}).optional(),
|
|
49
|
+
locale: z.string().regex(LOCALE_PATTERN).default("en-US"),
|
|
50
|
+
timezone: z.string().min(1).refine(isIanaTimeZone, "must be an IANA time zone name").default("America/Los_Angeles")
|
|
51
|
+
});
|
|
52
|
+
var siteSettingsSchemaStrict = makeSiteSettingsSchema(strictFactory);
|
|
53
|
+
var siteSettingsSchema = makeSiteSettingsSchema(tolerantFactory);
|
|
54
|
+
var makeBrandingSchema = (o) => o({
|
|
55
|
+
// Absolute URLs produced by the dashboard's storagePublicUrl builder
|
|
56
|
+
// (apps/dashboard/src/lib/content-urls.ts). The DB stores storage PATHS;
|
|
57
|
+
// the publish-time compose maps path -> URL. This package never imports
|
|
58
|
+
// from the app (dependency direction, phase-2.md T1.7).
|
|
59
|
+
logoUrl: z.url().optional(),
|
|
60
|
+
faviconUrl: z.url().optional(),
|
|
61
|
+
colors: o({
|
|
62
|
+
primary: z.string().regex(HEX_COLOR_PATTERN),
|
|
63
|
+
secondary: z.string().regex(HEX_COLOR_PATTERN).optional(),
|
|
64
|
+
accent: z.string().regex(HEX_COLOR_PATTERN).optional(),
|
|
65
|
+
background: z.string().regex(HEX_COLOR_PATTERN).optional(),
|
|
66
|
+
text: z.string().regex(HEX_COLOR_PATTERN).optional()
|
|
67
|
+
}),
|
|
68
|
+
fonts: o({
|
|
69
|
+
heading: z.string().min(1).optional(),
|
|
70
|
+
body: z.string().min(1).optional()
|
|
71
|
+
}).optional()
|
|
72
|
+
});
|
|
73
|
+
var brandingSchemaStrict = makeBrandingSchema(strictFactory);
|
|
74
|
+
var brandingSchema = makeBrandingSchema(tolerantFactory);
|
|
75
|
+
var makeMenuItemSchema = (o) => o({
|
|
76
|
+
/** Stable DB UUID — identical across publishes (phase-2.md T1.3). */
|
|
77
|
+
id: z.uuid(),
|
|
78
|
+
name: z.string().min(1).max(200),
|
|
79
|
+
description: z.string().max(2e3).optional(),
|
|
80
|
+
price: o({
|
|
81
|
+
/** Integer minor units (e.g. cents). */
|
|
82
|
+
amount: z.number().int().min(0),
|
|
83
|
+
currency: z.string().regex(CURRENCY_PATTERN)
|
|
84
|
+
}),
|
|
85
|
+
imageUrl: z.url().optional(),
|
|
86
|
+
tags: z.array(z.string().min(1)).max(20),
|
|
87
|
+
sortOrder: z.number().int().min(0).max(1e5),
|
|
88
|
+
isAvailable: z.boolean()
|
|
89
|
+
});
|
|
90
|
+
var makeMenuCategorySchema = (o) => o({
|
|
91
|
+
id: z.uuid(),
|
|
92
|
+
name: z.string().min(1).max(200),
|
|
93
|
+
description: z.string().max(2e3).optional(),
|
|
94
|
+
sortOrder: z.number().int().min(0).max(1e5),
|
|
95
|
+
items: z.array(makeMenuItemSchema(o))
|
|
96
|
+
});
|
|
97
|
+
var makeMenuSchema = (o) => o({
|
|
98
|
+
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
99
|
+
categories: z.array(makeMenuCategorySchema(o))
|
|
100
|
+
});
|
|
101
|
+
var menuSchemaStrict = makeMenuSchema(strictFactory);
|
|
102
|
+
var menuSchema = makeMenuSchema(tolerantFactory);
|
|
103
|
+
var makeContentFileSchema = (o) => o({
|
|
104
|
+
schemaVersion: z.literal(1),
|
|
105
|
+
site: o({ id: z.uuid(), slug: z.string().min(1) }),
|
|
106
|
+
publishedAt: z.iso.datetime(),
|
|
107
|
+
// All three modules REQUIRED; empty states are valid module payloads
|
|
108
|
+
// (e.g. menu.categories: []), never missing keys. A new required module is
|
|
109
|
+
// breaking (retained/rollback snapshots won't contain it) — bump
|
|
110
|
+
// SCHEMA_VERSION instead.
|
|
111
|
+
modules: o({
|
|
112
|
+
siteSettings: makeSiteSettingsSchema(o),
|
|
113
|
+
branding: makeBrandingSchema(o),
|
|
114
|
+
menu: makeMenuSchema(o)
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
var contentFileSchemaStrict = makeContentFileSchema(strictFactory);
|
|
118
|
+
var contentFileSchema = makeContentFileSchema(tolerantFactory);
|
|
119
|
+
function canonicalize(payload) {
|
|
120
|
+
const out = serialize(payload);
|
|
121
|
+
if (out === void 0) {
|
|
122
|
+
throw new TypeError("canonicalize: payload itself serializes to nothing (undefined)");
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function serialize(value) {
|
|
127
|
+
if (value === void 0) return void 0;
|
|
128
|
+
if (value === null) return "null";
|
|
129
|
+
switch (typeof value) {
|
|
130
|
+
case "string":
|
|
131
|
+
case "boolean":
|
|
132
|
+
return JSON.stringify(value);
|
|
133
|
+
case "number":
|
|
134
|
+
if (!Number.isFinite(value)) {
|
|
135
|
+
throw new TypeError(`canonicalize: non-finite number ${String(value)}`);
|
|
136
|
+
}
|
|
137
|
+
return JSON.stringify(value);
|
|
138
|
+
case "object":
|
|
139
|
+
break;
|
|
140
|
+
default:
|
|
141
|
+
throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
const parts2 = value.map((entry) => {
|
|
145
|
+
const s = serialize(entry);
|
|
146
|
+
if (s === void 0) {
|
|
147
|
+
throw new TypeError("canonicalize: undefined array element");
|
|
148
|
+
}
|
|
149
|
+
return s;
|
|
150
|
+
});
|
|
151
|
+
return `[${parts2.join(",")}]`;
|
|
152
|
+
}
|
|
153
|
+
const record = value;
|
|
154
|
+
const keys = Object.keys(record).sort();
|
|
155
|
+
const parts = [];
|
|
156
|
+
for (const key of keys) {
|
|
157
|
+
const s = serialize(record[key]);
|
|
158
|
+
if (s === void 0) continue;
|
|
159
|
+
parts.push(`${JSON.stringify(key)}:${s}`);
|
|
160
|
+
}
|
|
161
|
+
return `{${parts.join(",")}}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
SCHEMA_VERSION,
|
|
166
|
+
MAX_CONTENT_BYTES,
|
|
167
|
+
LOCALE_PATTERN_SOURCE,
|
|
168
|
+
LOCALE_PATTERN,
|
|
169
|
+
HEX_COLOR_PATTERN_SOURCE,
|
|
170
|
+
HEX_COLOR_PATTERN,
|
|
171
|
+
CURRENCY_PATTERN_SOURCE,
|
|
172
|
+
CURRENCY_PATTERN,
|
|
173
|
+
COUNTRY_PATTERN_SOURCE,
|
|
174
|
+
COUNTRY_PATTERN,
|
|
175
|
+
isIanaTimeZone,
|
|
176
|
+
siteSettingsSchemaStrict,
|
|
177
|
+
siteSettingsSchema,
|
|
178
|
+
brandingSchemaStrict,
|
|
179
|
+
brandingSchema,
|
|
180
|
+
menuSchemaStrict,
|
|
181
|
+
menuSchema,
|
|
182
|
+
contentFileSchemaStrict,
|
|
183
|
+
contentFileSchema,
|
|
184
|
+
canonicalize
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=chunk-KBPOR3WL.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/** 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"]}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sha256:` + hex digest of the canonical UTF-8 bytes. The publisher persists
|
|
3
|
+
* this as `payload_hash`; consumer tests use the SAME function so hash
|
|
4
|
+
* comparisons are always over identical canonical bytes (phase-2.md T1.6).
|
|
5
|
+
*
|
|
6
|
+
* Node-only subpath (`@lamgig/content-schemas/hash`): it needs `node:crypto`
|
|
7
|
+
* for a synchronous digest, and the main entry must stay importable from
|
|
8
|
+
* client bundles (form schemas reuse its patterns).
|
|
9
|
+
*/
|
|
10
|
+
declare function hashContent(payload: unknown): string;
|
|
11
|
+
|
|
12
|
+
export { hashContent };
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalize
|
|
3
|
+
} from "./chunk-KBPOR3WL.js";
|
|
4
|
+
|
|
5
|
+
// src/hash.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
function hashContent(payload) {
|
|
8
|
+
const canonical = canonicalize(payload);
|
|
9
|
+
return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
hashContent
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=hash.js.map
|
package/dist/hash.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hash.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nimport { canonicalize } from './index.js';\n\n/**\n * `sha256:` + hex digest of the canonical UTF-8 bytes. The publisher persists\n * this as `payload_hash`; consumer tests use the SAME function so hash\n * comparisons are always over identical canonical bytes (phase-2.md T1.6).\n *\n * Node-only subpath (`@lamgig/content-schemas/hash`): it needs `node:crypto`\n * for a synchronous digest, and the main entry must stay importable from\n * client bundles (form schemas reuse its patterns).\n */\nexport function hashContent(payload: unknown): string {\n const canonical = canonicalize(payload);\n return `sha256:${createHash('sha256').update(canonical, 'utf8').digest('hex')}`;\n}\n"],"mappings":";;;;;AAAA,SAAS,kBAAkB;AAapB,SAAS,YAAY,SAA0B;AACpD,QAAM,YAAY,aAAa,OAAO;AACtC,SAAO,UAAU,WAAW,QAAQ,EAAE,OAAO,WAAW,MAAM,EAAE,OAAO,KAAK,CAAC;AAC/E;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Version of the published-JSON contract. Additive changes (new OPTIONAL fields
|
|
5
|
+
* and new OPTIONAL module keys) keep this number; any required addition is
|
|
6
|
+
* breaking and bumps it. The full compatibility rule lives in this package's
|
|
7
|
+
* README. See PLAN.md § Published JSON delivery and phase-2.md T1.5.
|
|
8
|
+
*/
|
|
9
|
+
declare const SCHEMA_VERSION = 1;
|
|
10
|
+
/** Publisher-enforced ceiling on the canonical payload size (1 MiB). */
|
|
11
|
+
declare const MAX_CONTENT_BYTES = 1048576;
|
|
12
|
+
declare const LOCALE_PATTERN_SOURCE = "^[a-z]{2}(-[A-Z]{2})?$";
|
|
13
|
+
declare const LOCALE_PATTERN: RegExp;
|
|
14
|
+
declare const HEX_COLOR_PATTERN_SOURCE = "^#[0-9A-Fa-f]{6}$";
|
|
15
|
+
declare const HEX_COLOR_PATTERN: RegExp;
|
|
16
|
+
/** ISO 4217 alpha code shape (mirrored by the `price_currency` DB CHECK). */
|
|
17
|
+
declare const CURRENCY_PATTERN_SOURCE = "^[A-Z]{3}$";
|
|
18
|
+
declare const CURRENCY_PATTERN: RegExp;
|
|
19
|
+
/** ISO 3166-1 alpha-2 country code shape. */
|
|
20
|
+
declare const COUNTRY_PATTERN_SOURCE = "^[A-Z]{2}$";
|
|
21
|
+
declare const COUNTRY_PATTERN: RegExp;
|
|
22
|
+
/** IANA time zone check via the runtime's own tz database. */
|
|
23
|
+
declare function isIanaTimeZone(tz: string): boolean;
|
|
24
|
+
declare const siteSettingsSchemaStrict: z.ZodObject<{
|
|
25
|
+
siteName: z.ZodString;
|
|
26
|
+
tagline: z.ZodOptional<z.ZodString>;
|
|
27
|
+
contact: z.ZodObject<{
|
|
28
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
29
|
+
phone: z.ZodOptional<z.ZodString>;
|
|
30
|
+
address: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
line1: z.ZodString;
|
|
32
|
+
line2: z.ZodOptional<z.ZodString>;
|
|
33
|
+
city: z.ZodString;
|
|
34
|
+
region: z.ZodString;
|
|
35
|
+
postalCode: z.ZodString;
|
|
36
|
+
country: z.ZodString;
|
|
37
|
+
}, z.core.$strip>>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
socialLinks: z.ZodOptional<z.ZodObject<{
|
|
40
|
+
facebook: z.ZodOptional<z.ZodURL>;
|
|
41
|
+
instagram: z.ZodOptional<z.ZodURL>;
|
|
42
|
+
tiktok: z.ZodOptional<z.ZodURL>;
|
|
43
|
+
x: z.ZodOptional<z.ZodURL>;
|
|
44
|
+
youtube: z.ZodOptional<z.ZodURL>;
|
|
45
|
+
yelp: z.ZodOptional<z.ZodURL>;
|
|
46
|
+
google: z.ZodOptional<z.ZodURL>;
|
|
47
|
+
}, z.core.$strip>>;
|
|
48
|
+
locale: z.ZodDefault<z.ZodString>;
|
|
49
|
+
timezone: z.ZodDefault<z.ZodString>;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
declare const siteSettingsSchema: z.ZodObject<{
|
|
52
|
+
siteName: z.ZodString;
|
|
53
|
+
tagline: z.ZodOptional<z.ZodString>;
|
|
54
|
+
contact: z.ZodObject<{
|
|
55
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
56
|
+
phone: z.ZodOptional<z.ZodString>;
|
|
57
|
+
address: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
line1: z.ZodString;
|
|
59
|
+
line2: z.ZodOptional<z.ZodString>;
|
|
60
|
+
city: z.ZodString;
|
|
61
|
+
region: z.ZodString;
|
|
62
|
+
postalCode: z.ZodString;
|
|
63
|
+
country: z.ZodString;
|
|
64
|
+
}, z.core.$strip>>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
socialLinks: z.ZodOptional<z.ZodObject<{
|
|
67
|
+
facebook: z.ZodOptional<z.ZodURL>;
|
|
68
|
+
instagram: z.ZodOptional<z.ZodURL>;
|
|
69
|
+
tiktok: z.ZodOptional<z.ZodURL>;
|
|
70
|
+
x: z.ZodOptional<z.ZodURL>;
|
|
71
|
+
youtube: z.ZodOptional<z.ZodURL>;
|
|
72
|
+
yelp: z.ZodOptional<z.ZodURL>;
|
|
73
|
+
google: z.ZodOptional<z.ZodURL>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
locale: z.ZodDefault<z.ZodString>;
|
|
76
|
+
timezone: z.ZodDefault<z.ZodString>;
|
|
77
|
+
}, z.core.$strip>;
|
|
78
|
+
declare const brandingSchemaStrict: z.ZodObject<{
|
|
79
|
+
logoUrl: z.ZodOptional<z.ZodURL>;
|
|
80
|
+
faviconUrl: z.ZodOptional<z.ZodURL>;
|
|
81
|
+
colors: z.ZodObject<{
|
|
82
|
+
primary: z.ZodString;
|
|
83
|
+
secondary: z.ZodOptional<z.ZodString>;
|
|
84
|
+
accent: z.ZodOptional<z.ZodString>;
|
|
85
|
+
background: z.ZodOptional<z.ZodString>;
|
|
86
|
+
text: z.ZodOptional<z.ZodString>;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
fonts: z.ZodOptional<z.ZodObject<{
|
|
89
|
+
heading: z.ZodOptional<z.ZodString>;
|
|
90
|
+
body: z.ZodOptional<z.ZodString>;
|
|
91
|
+
}, z.core.$strip>>;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
declare const brandingSchema: z.ZodObject<{
|
|
94
|
+
logoUrl: z.ZodOptional<z.ZodURL>;
|
|
95
|
+
faviconUrl: z.ZodOptional<z.ZodURL>;
|
|
96
|
+
colors: z.ZodObject<{
|
|
97
|
+
primary: z.ZodString;
|
|
98
|
+
secondary: z.ZodOptional<z.ZodString>;
|
|
99
|
+
accent: z.ZodOptional<z.ZodString>;
|
|
100
|
+
background: z.ZodOptional<z.ZodString>;
|
|
101
|
+
text: z.ZodOptional<z.ZodString>;
|
|
102
|
+
}, z.core.$strip>;
|
|
103
|
+
fonts: z.ZodOptional<z.ZodObject<{
|
|
104
|
+
heading: z.ZodOptional<z.ZodString>;
|
|
105
|
+
body: z.ZodOptional<z.ZodString>;
|
|
106
|
+
}, z.core.$strip>>;
|
|
107
|
+
}, z.core.$strip>;
|
|
108
|
+
declare const menuSchemaStrict: z.ZodObject<{
|
|
109
|
+
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
110
|
+
categories: z.ZodArray<z.ZodObject<{
|
|
111
|
+
id: z.ZodUUID;
|
|
112
|
+
name: z.ZodString;
|
|
113
|
+
description: z.ZodOptional<z.ZodString>;
|
|
114
|
+
sortOrder: z.ZodNumber;
|
|
115
|
+
items: z.ZodArray<z.ZodObject<{
|
|
116
|
+
/** Stable DB UUID — identical across publishes (phase-2.md T1.3). */
|
|
117
|
+
id: z.ZodUUID;
|
|
118
|
+
name: z.ZodString;
|
|
119
|
+
description: z.ZodOptional<z.ZodString>;
|
|
120
|
+
price: z.ZodObject<{
|
|
121
|
+
/** Integer minor units (e.g. cents). */
|
|
122
|
+
amount: z.ZodNumber;
|
|
123
|
+
currency: z.ZodString;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
imageUrl: z.ZodOptional<z.ZodURL>;
|
|
126
|
+
tags: z.ZodArray<z.ZodString>;
|
|
127
|
+
sortOrder: z.ZodNumber;
|
|
128
|
+
isAvailable: z.ZodBoolean;
|
|
129
|
+
}, z.core.$strip>>;
|
|
130
|
+
}, z.core.$strip>>;
|
|
131
|
+
}, z.core.$strip>;
|
|
132
|
+
declare const menuSchema: z.ZodObject<{
|
|
133
|
+
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
134
|
+
categories: z.ZodArray<z.ZodObject<{
|
|
135
|
+
id: z.ZodUUID;
|
|
136
|
+
name: z.ZodString;
|
|
137
|
+
description: z.ZodOptional<z.ZodString>;
|
|
138
|
+
sortOrder: z.ZodNumber;
|
|
139
|
+
items: z.ZodArray<z.ZodObject<{
|
|
140
|
+
/** Stable DB UUID — identical across publishes (phase-2.md T1.3). */
|
|
141
|
+
id: z.ZodUUID;
|
|
142
|
+
name: z.ZodString;
|
|
143
|
+
description: z.ZodOptional<z.ZodString>;
|
|
144
|
+
price: z.ZodObject<{
|
|
145
|
+
/** Integer minor units (e.g. cents). */
|
|
146
|
+
amount: z.ZodNumber;
|
|
147
|
+
currency: z.ZodString;
|
|
148
|
+
}, z.core.$strip>;
|
|
149
|
+
imageUrl: z.ZodOptional<z.ZodURL>;
|
|
150
|
+
tags: z.ZodArray<z.ZodString>;
|
|
151
|
+
sortOrder: z.ZodNumber;
|
|
152
|
+
isAvailable: z.ZodBoolean;
|
|
153
|
+
}, z.core.$strip>>;
|
|
154
|
+
}, z.core.$strip>>;
|
|
155
|
+
}, z.core.$strip>;
|
|
156
|
+
/** Producer gate: the dashboard validates its own output against this. */
|
|
157
|
+
declare const contentFileSchemaStrict: z.ZodObject<{
|
|
158
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
159
|
+
site: z.ZodObject<{
|
|
160
|
+
id: z.ZodUUID;
|
|
161
|
+
slug: z.ZodString;
|
|
162
|
+
}, z.core.$strip>;
|
|
163
|
+
publishedAt: z.ZodISODateTime;
|
|
164
|
+
modules: z.ZodObject<{
|
|
165
|
+
siteSettings: z.ZodObject<{
|
|
166
|
+
siteName: z.ZodString;
|
|
167
|
+
tagline: z.ZodOptional<z.ZodString>;
|
|
168
|
+
contact: z.ZodObject<{
|
|
169
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
170
|
+
phone: z.ZodOptional<z.ZodString>;
|
|
171
|
+
address: z.ZodOptional<z.ZodObject<{
|
|
172
|
+
line1: z.ZodString;
|
|
173
|
+
line2: z.ZodOptional<z.ZodString>;
|
|
174
|
+
city: z.ZodString;
|
|
175
|
+
region: z.ZodString;
|
|
176
|
+
postalCode: z.ZodString;
|
|
177
|
+
country: z.ZodString;
|
|
178
|
+
}, z.core.$strip>>;
|
|
179
|
+
}, z.core.$strip>;
|
|
180
|
+
socialLinks: z.ZodOptional<z.ZodObject<{
|
|
181
|
+
facebook: z.ZodOptional<z.ZodURL>;
|
|
182
|
+
instagram: z.ZodOptional<z.ZodURL>;
|
|
183
|
+
tiktok: z.ZodOptional<z.ZodURL>;
|
|
184
|
+
x: z.ZodOptional<z.ZodURL>;
|
|
185
|
+
youtube: z.ZodOptional<z.ZodURL>;
|
|
186
|
+
yelp: z.ZodOptional<z.ZodURL>;
|
|
187
|
+
google: z.ZodOptional<z.ZodURL>;
|
|
188
|
+
}, z.core.$strip>>;
|
|
189
|
+
locale: z.ZodDefault<z.ZodString>;
|
|
190
|
+
timezone: z.ZodDefault<z.ZodString>;
|
|
191
|
+
}, z.core.$strip>;
|
|
192
|
+
branding: z.ZodObject<{
|
|
193
|
+
logoUrl: z.ZodOptional<z.ZodURL>;
|
|
194
|
+
faviconUrl: z.ZodOptional<z.ZodURL>;
|
|
195
|
+
colors: z.ZodObject<{
|
|
196
|
+
primary: z.ZodString;
|
|
197
|
+
secondary: z.ZodOptional<z.ZodString>;
|
|
198
|
+
accent: z.ZodOptional<z.ZodString>;
|
|
199
|
+
background: z.ZodOptional<z.ZodString>;
|
|
200
|
+
text: z.ZodOptional<z.ZodString>;
|
|
201
|
+
}, z.core.$strip>;
|
|
202
|
+
fonts: z.ZodOptional<z.ZodObject<{
|
|
203
|
+
heading: z.ZodOptional<z.ZodString>;
|
|
204
|
+
body: z.ZodOptional<z.ZodString>;
|
|
205
|
+
}, z.core.$strip>>;
|
|
206
|
+
}, z.core.$strip>;
|
|
207
|
+
menu: z.ZodObject<{
|
|
208
|
+
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
209
|
+
categories: z.ZodArray<z.ZodObject<{
|
|
210
|
+
id: z.ZodUUID;
|
|
211
|
+
name: z.ZodString;
|
|
212
|
+
description: z.ZodOptional<z.ZodString>;
|
|
213
|
+
sortOrder: z.ZodNumber;
|
|
214
|
+
items: z.ZodArray<z.ZodObject<{
|
|
215
|
+
/** Stable DB UUID — identical across publishes (phase-2.md T1.3). */
|
|
216
|
+
id: z.ZodUUID;
|
|
217
|
+
name: z.ZodString;
|
|
218
|
+
description: z.ZodOptional<z.ZodString>;
|
|
219
|
+
price: z.ZodObject<{
|
|
220
|
+
/** Integer minor units (e.g. cents). */
|
|
221
|
+
amount: z.ZodNumber;
|
|
222
|
+
currency: z.ZodString;
|
|
223
|
+
}, z.core.$strip>;
|
|
224
|
+
imageUrl: z.ZodOptional<z.ZodURL>;
|
|
225
|
+
tags: z.ZodArray<z.ZodString>;
|
|
226
|
+
sortOrder: z.ZodNumber;
|
|
227
|
+
isAvailable: z.ZodBoolean;
|
|
228
|
+
}, z.core.$strip>>;
|
|
229
|
+
}, z.core.$strip>>;
|
|
230
|
+
}, z.core.$strip>;
|
|
231
|
+
}, z.core.$strip>;
|
|
232
|
+
}, z.core.$strip>;
|
|
233
|
+
/** Public consumer schema: tolerant of additive v1 changes. */
|
|
234
|
+
declare const contentFileSchema: z.ZodObject<{
|
|
235
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
236
|
+
site: z.ZodObject<{
|
|
237
|
+
id: z.ZodUUID;
|
|
238
|
+
slug: z.ZodString;
|
|
239
|
+
}, z.core.$strip>;
|
|
240
|
+
publishedAt: z.ZodISODateTime;
|
|
241
|
+
modules: z.ZodObject<{
|
|
242
|
+
siteSettings: z.ZodObject<{
|
|
243
|
+
siteName: z.ZodString;
|
|
244
|
+
tagline: z.ZodOptional<z.ZodString>;
|
|
245
|
+
contact: z.ZodObject<{
|
|
246
|
+
email: z.ZodOptional<z.ZodEmail>;
|
|
247
|
+
phone: z.ZodOptional<z.ZodString>;
|
|
248
|
+
address: z.ZodOptional<z.ZodObject<{
|
|
249
|
+
line1: z.ZodString;
|
|
250
|
+
line2: z.ZodOptional<z.ZodString>;
|
|
251
|
+
city: z.ZodString;
|
|
252
|
+
region: z.ZodString;
|
|
253
|
+
postalCode: z.ZodString;
|
|
254
|
+
country: z.ZodString;
|
|
255
|
+
}, z.core.$strip>>;
|
|
256
|
+
}, z.core.$strip>;
|
|
257
|
+
socialLinks: z.ZodOptional<z.ZodObject<{
|
|
258
|
+
facebook: z.ZodOptional<z.ZodURL>;
|
|
259
|
+
instagram: z.ZodOptional<z.ZodURL>;
|
|
260
|
+
tiktok: z.ZodOptional<z.ZodURL>;
|
|
261
|
+
x: z.ZodOptional<z.ZodURL>;
|
|
262
|
+
youtube: z.ZodOptional<z.ZodURL>;
|
|
263
|
+
yelp: z.ZodOptional<z.ZodURL>;
|
|
264
|
+
google: z.ZodOptional<z.ZodURL>;
|
|
265
|
+
}, z.core.$strip>>;
|
|
266
|
+
locale: z.ZodDefault<z.ZodString>;
|
|
267
|
+
timezone: z.ZodDefault<z.ZodString>;
|
|
268
|
+
}, z.core.$strip>;
|
|
269
|
+
branding: z.ZodObject<{
|
|
270
|
+
logoUrl: z.ZodOptional<z.ZodURL>;
|
|
271
|
+
faviconUrl: z.ZodOptional<z.ZodURL>;
|
|
272
|
+
colors: z.ZodObject<{
|
|
273
|
+
primary: z.ZodString;
|
|
274
|
+
secondary: z.ZodOptional<z.ZodString>;
|
|
275
|
+
accent: z.ZodOptional<z.ZodString>;
|
|
276
|
+
background: z.ZodOptional<z.ZodString>;
|
|
277
|
+
text: z.ZodOptional<z.ZodString>;
|
|
278
|
+
}, z.core.$strip>;
|
|
279
|
+
fonts: z.ZodOptional<z.ZodObject<{
|
|
280
|
+
heading: z.ZodOptional<z.ZodString>;
|
|
281
|
+
body: z.ZodOptional<z.ZodString>;
|
|
282
|
+
}, z.core.$strip>>;
|
|
283
|
+
}, z.core.$strip>;
|
|
284
|
+
menu: z.ZodObject<{
|
|
285
|
+
/** Archived/deleted entities are simply ABSENT; empty menu = `[]`, never a missing key. */
|
|
286
|
+
categories: z.ZodArray<z.ZodObject<{
|
|
287
|
+
id: z.ZodUUID;
|
|
288
|
+
name: z.ZodString;
|
|
289
|
+
description: z.ZodOptional<z.ZodString>;
|
|
290
|
+
sortOrder: z.ZodNumber;
|
|
291
|
+
items: z.ZodArray<z.ZodObject<{
|
|
292
|
+
/** Stable DB UUID — identical across publishes (phase-2.md T1.3). */
|
|
293
|
+
id: z.ZodUUID;
|
|
294
|
+
name: z.ZodString;
|
|
295
|
+
description: z.ZodOptional<z.ZodString>;
|
|
296
|
+
price: z.ZodObject<{
|
|
297
|
+
/** Integer minor units (e.g. cents). */
|
|
298
|
+
amount: z.ZodNumber;
|
|
299
|
+
currency: z.ZodString;
|
|
300
|
+
}, z.core.$strip>;
|
|
301
|
+
imageUrl: z.ZodOptional<z.ZodURL>;
|
|
302
|
+
tags: z.ZodArray<z.ZodString>;
|
|
303
|
+
sortOrder: z.ZodNumber;
|
|
304
|
+
isAvailable: z.ZodBoolean;
|
|
305
|
+
}, z.core.$strip>>;
|
|
306
|
+
}, z.core.$strip>>;
|
|
307
|
+
}, z.core.$strip>;
|
|
308
|
+
}, z.core.$strip>;
|
|
309
|
+
}, z.core.$strip>;
|
|
310
|
+
type SiteSettings = z.infer<typeof siteSettingsSchemaStrict>;
|
|
311
|
+
type Branding = z.infer<typeof brandingSchemaStrict>;
|
|
312
|
+
type Menu = z.infer<typeof menuSchemaStrict>;
|
|
313
|
+
type MenuCategory = Menu['categories'][number];
|
|
314
|
+
type MenuItem = MenuCategory['items'][number];
|
|
315
|
+
type ContentFile = z.infer<typeof contentFileSchemaStrict>;
|
|
316
|
+
declare function canonicalize(payload: unknown): string;
|
|
317
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COUNTRY_PATTERN,
|
|
3
|
+
COUNTRY_PATTERN_SOURCE,
|
|
4
|
+
CURRENCY_PATTERN,
|
|
5
|
+
CURRENCY_PATTERN_SOURCE,
|
|
6
|
+
HEX_COLOR_PATTERN,
|
|
7
|
+
HEX_COLOR_PATTERN_SOURCE,
|
|
8
|
+
LOCALE_PATTERN,
|
|
9
|
+
LOCALE_PATTERN_SOURCE,
|
|
10
|
+
MAX_CONTENT_BYTES,
|
|
11
|
+
SCHEMA_VERSION,
|
|
12
|
+
brandingSchema,
|
|
13
|
+
brandingSchemaStrict,
|
|
14
|
+
canonicalize,
|
|
15
|
+
contentFileSchema,
|
|
16
|
+
contentFileSchemaStrict,
|
|
17
|
+
isIanaTimeZone,
|
|
18
|
+
menuSchema,
|
|
19
|
+
menuSchemaStrict,
|
|
20
|
+
siteSettingsSchema,
|
|
21
|
+
siteSettingsSchemaStrict
|
|
22
|
+
} from "./chunk-KBPOR3WL.js";
|
|
23
|
+
export {
|
|
24
|
+
COUNTRY_PATTERN,
|
|
25
|
+
COUNTRY_PATTERN_SOURCE,
|
|
26
|
+
CURRENCY_PATTERN,
|
|
27
|
+
CURRENCY_PATTERN_SOURCE,
|
|
28
|
+
HEX_COLOR_PATTERN,
|
|
29
|
+
HEX_COLOR_PATTERN_SOURCE,
|
|
30
|
+
LOCALE_PATTERN,
|
|
31
|
+
LOCALE_PATTERN_SOURCE,
|
|
32
|
+
MAX_CONTENT_BYTES,
|
|
33
|
+
SCHEMA_VERSION,
|
|
34
|
+
brandingSchema,
|
|
35
|
+
brandingSchemaStrict,
|
|
36
|
+
canonicalize,
|
|
37
|
+
contentFileSchema,
|
|
38
|
+
contentFileSchemaStrict,
|
|
39
|
+
isIanaTimeZone,
|
|
40
|
+
menuSchema,
|
|
41
|
+
menuSchemaStrict,
|
|
42
|
+
siteSettingsSchema,
|
|
43
|
+
siteSettingsSchemaStrict
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"site": {
|
|
4
|
+
"id": "b7e6a1d2-3c4f-4a5b-9e8d-1f2a3b4c5d6e",
|
|
5
|
+
"slug": "main"
|
|
6
|
+
},
|
|
7
|
+
"publishedAt": "2026-07-22T12:00:00.000Z",
|
|
8
|
+
"modules": {
|
|
9
|
+
"siteSettings": {
|
|
10
|
+
"siteName": "Pho Palace",
|
|
11
|
+
"tagline": "Northern Vietnamese noodle soups, made daily",
|
|
12
|
+
"contact": {
|
|
13
|
+
"email": "hello@phopalace.example",
|
|
14
|
+
"phone": "+1 704-555-0132",
|
|
15
|
+
"address": {
|
|
16
|
+
"line1": "214 Tryon St",
|
|
17
|
+
"line2": "Suite 120",
|
|
18
|
+
"city": "Charlotte",
|
|
19
|
+
"region": "NC",
|
|
20
|
+
"postalCode": "28202",
|
|
21
|
+
"country": "US"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"socialLinks": {
|
|
25
|
+
"facebook": "https://facebook.com/phopalace",
|
|
26
|
+
"instagram": "https://instagram.com/phopalace",
|
|
27
|
+
"tiktok": "https://www.tiktok.com/@phopalace",
|
|
28
|
+
"x": "https://x.com/phopalace",
|
|
29
|
+
"youtube": "https://youtube.com/@phopalace",
|
|
30
|
+
"yelp": "https://www.yelp.com/biz/pho-palace-charlotte",
|
|
31
|
+
"google": "https://maps.google.com/?cid=5069577489237150720"
|
|
32
|
+
},
|
|
33
|
+
"locale": "en-US",
|
|
34
|
+
"timezone": "America/New_York"
|
|
35
|
+
},
|
|
36
|
+
"branding": {
|
|
37
|
+
"logoUrl": "https://example.supabase.co/storage/v1/object/public/assets/org_01example7fixture0000000000/b7e6a1d2-3c4f-4a5b-9e8d-1f2a3b4c5d6e/9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08.png",
|
|
38
|
+
"faviconUrl": "https://example.supabase.co/storage/v1/object/public/assets/org_01example7fixture0000000000/b7e6a1d2-3c4f-4a5b-9e8d-1f2a3b4c5d6e/60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752.png",
|
|
39
|
+
"colors": {
|
|
40
|
+
"primary": "#7A1F1F",
|
|
41
|
+
"secondary": "#F4E9DC",
|
|
42
|
+
"accent": "#C8A24B",
|
|
43
|
+
"background": "#FFFFFF",
|
|
44
|
+
"text": "#1A1A1A"
|
|
45
|
+
},
|
|
46
|
+
"fonts": {
|
|
47
|
+
"heading": "Plus Jakarta Sans",
|
|
48
|
+
"body": "Inter"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"menu": {
|
|
52
|
+
"categories": [
|
|
53
|
+
{
|
|
54
|
+
"id": "0d9a2c66-5b1e-4c9a-8f3d-7e6b5a4c3d2e",
|
|
55
|
+
"name": "Pho",
|
|
56
|
+
"description": "Slow-simmered broth, rice noodles, fresh herbs.",
|
|
57
|
+
"sortOrder": 0,
|
|
58
|
+
"items": [
|
|
59
|
+
{
|
|
60
|
+
"id": "5e4d3c2b-1a09-4f8e-b7c6-d5e4f3a2b1c0",
|
|
61
|
+
"name": "Pho Bo",
|
|
62
|
+
"description": "Beef pho with brisket and rare steak.",
|
|
63
|
+
"price": {
|
|
64
|
+
"amount": 1450,
|
|
65
|
+
"currency": "USD"
|
|
66
|
+
},
|
|
67
|
+
"imageUrl": "https://example.supabase.co/storage/v1/object/public/assets/org_01example7fixture0000000000/b7e6a1d2-3c4f-4a5b-9e8d-1f2a3b4c5d6e/fd61a03af4f77d870fc21e05e7e80678095c92d808cfb3b5c279ee04c74aca13.webp",
|
|
68
|
+
"tags": ["signature", "gluten-free"],
|
|
69
|
+
"sortOrder": 0,
|
|
70
|
+
"isAvailable": true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "6f5e4d3c-2b1a-4098-a7b6-c5d4e3f2a1b0",
|
|
74
|
+
"name": "Pho Ga",
|
|
75
|
+
"description": "Chicken pho with poached free-range chicken.",
|
|
76
|
+
"price": {
|
|
77
|
+
"amount": 1350,
|
|
78
|
+
"currency": "USD"
|
|
79
|
+
},
|
|
80
|
+
"tags": ["gluten-free"],
|
|
81
|
+
"sortOrder": 1,
|
|
82
|
+
"isAvailable": true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "7a6b5c4d-3e2f-4109-b8a7-d6c5b4a3f2e1",
|
|
86
|
+
"name": "Pho Chay",
|
|
87
|
+
"description": "Vegetarian pho with shiitake broth and tofu.",
|
|
88
|
+
"price": {
|
|
89
|
+
"amount": 1250,
|
|
90
|
+
"currency": "USD"
|
|
91
|
+
},
|
|
92
|
+
"tags": ["vegetarian", "vegan-option"],
|
|
93
|
+
"sortOrder": 2,
|
|
94
|
+
"isAvailable": false
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"id": "1e8b3d77-6c2f-4d0b-9a4e-8f7c6b5d4e3f",
|
|
100
|
+
"name": "Drinks",
|
|
101
|
+
"sortOrder": 1,
|
|
102
|
+
"items": [
|
|
103
|
+
{
|
|
104
|
+
"id": "8b7a6c5d-4f3e-4210-a9b8-e7d6c5b4a3f2",
|
|
105
|
+
"name": "Vietnamese Iced Coffee",
|
|
106
|
+
"description": "Ca phe sua da — robusta over condensed milk.",
|
|
107
|
+
"price": {
|
|
108
|
+
"amount": 550,
|
|
109
|
+
"currency": "USD"
|
|
110
|
+
},
|
|
111
|
+
"tags": ["cold"],
|
|
112
|
+
"sortOrder": 0,
|
|
113
|
+
"isAvailable": true
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "9c8b7a6d-5e4f-4321-90c9-f8e7d6c5b4a3",
|
|
117
|
+
"name": "Jasmine Tea",
|
|
118
|
+
"price": {
|
|
119
|
+
"amount": 300,
|
|
120
|
+
"currency": "USD"
|
|
121
|
+
},
|
|
122
|
+
"tags": [],
|
|
123
|
+
"sortOrder": 1,
|
|
124
|
+
"isAvailable": true
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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",
|
|
4
|
+
"minimal.json": "sha256:7b4ebc88bf2c17e1182a7e89fe994c3a38f70e0e638b37a0caa662f0c7e8db22"
|
|
5
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"site": {
|
|
4
|
+
"id": "b7e6a1d2-3c4f-4a5b-9e8d-1f2a3b4c5d6e",
|
|
5
|
+
"slug": "main"
|
|
6
|
+
},
|
|
7
|
+
"publishedAt": "2026-07-22T12:00:00.000Z",
|
|
8
|
+
"modules": {
|
|
9
|
+
"siteSettings": {
|
|
10
|
+
"siteName": "Pho Palace",
|
|
11
|
+
"contact": {},
|
|
12
|
+
"locale": "en-US",
|
|
13
|
+
"timezone": "America/Los_Angeles"
|
|
14
|
+
},
|
|
15
|
+
"branding": {
|
|
16
|
+
"colors": {
|
|
17
|
+
"primary": "#1A1A1A"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"menu": {
|
|
21
|
+
"categories": []
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lamgig/content-schemas",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The LamGig published-JSON contract: Zod schemas, canonical serialization, and fixtures for consuming content published by the LamGig platform.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lamgig",
|
|
7
|
+
"cms",
|
|
8
|
+
"content",
|
|
9
|
+
"zod",
|
|
10
|
+
"schema",
|
|
11
|
+
"canonical-json"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "LamGig",
|
|
15
|
+
"homepage": "https://github.com/LamGig/lamgig-platform/tree/main/packages/content-schemas#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/LamGig/lamgig-platform.git",
|
|
19
|
+
"directory": "packages/content-schemas"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/LamGig/lamgig-platform/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./hash": {
|
|
34
|
+
"types": "./dist/hash.d.ts",
|
|
35
|
+
"import": "./dist/hash.js"
|
|
36
|
+
},
|
|
37
|
+
"./fixtures/*": "./fixtures/*"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"fixtures",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"dev": "tsup --watch",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"prepublishOnly": "npm run build && npm run typecheck && npm run test"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"zod": "^4.4.3"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"tsup": "^8.5.1",
|
|
59
|
+
"typescript": "^5",
|
|
60
|
+
"vitest": "^4.1.10"
|
|
61
|
+
}
|
|
62
|
+
}
|