@reblu/site-contracts 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,330 @@
1
+ # @reblu/site-contracts
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 16dfaea: Add the SITE capability manifest types (REB-777).
8
+
9
+ `SiteManifest`, `SiteManifestSurfaceValue`, and `SiteCapabilitySurfaceKey` are
10
+ the SDK-facing types for `/.well-known/reblu-site.json` — the manifest a SITE
11
+ serves (via `@reblu/site-next`'s new `createSiteManifestRoute`) declaring
12
+ which capability surfaces it has implemented (`academy.enrollment`,
13
+ `academy.catalogue`, `auth.login`, `consent.banner`, `legal.cookies`), so
14
+ nexus can warn a tenant OWNER when a sold module has no matching page on
15
+ their SITE yet.
16
+
17
+ This is the one contract group in this package a SITE produces and base-api
18
+ consumes — the opposite direction of every other export here — but the
19
+ package stays exactly as dependency-free as before: these are `export type`
20
+ only, flowing through the same `@reblu/base-api-contracts` type-only alias
21
+ every other export uses. No runtime Zod, no new dependency, no change to any
22
+ existing export.
23
+
24
+ Purely additive. Not REB-744 (canonical route keys for per-route redirect
25
+ generation, separately tracked, still unimplemented) — that is a different,
26
+ coarser vocabulary (whole-module redirect targets) for a different purpose
27
+ (generating a URL, not declaring a capability).
28
+
29
+ ## 1.0.1
30
+
31
+ ### Patch Changes
32
+
33
+ - 9f54c58: REB-774 — ship `CHANGELOG.md` inside the published npm tarball for every
34
+ `@reblu/site-*` package (verified via `pnpm pack`: it was previously absent,
35
+ since `"files": ["dist"]` limited the tarball to `dist/` and npm only
36
+ auto-includes `README.md`, `LICENSE` and `package.json`). Consumers can now
37
+ read the changelog for the version they installed straight from
38
+ `node_modules/@reblu/<pkg>/CHANGELOG.md`.
39
+
40
+ ## 1.0.0
41
+
42
+ ### Major Changes
43
+
44
+ - 340db30: REB-659 — phone fields on the SITE API are now E.164, and two public boundaries
45
+ changed behaviour. A consuming SITE should read this before upgrading.
46
+
47
+ **Type change.** `ShopAddressInput.phone` and `ShopCheckoutInput.customerPhone`
48
+ widen from `string | undefined` to `string | null | undefined`. `null` is now a
49
+ real state meaning "clear this phone", which the server accepts and the previous
50
+ type could not express. Existing code that only ever assigns a string keeps
51
+ compiling.
52
+
53
+ **Behaviour change — `POST /api/site/contact-form` and `POST /api/site/leads`
54
+ (more permissive).** These used to store whatever string arrived. They now
55
+ resolve it to E.164 against the tenant's configured country
56
+ (`SiteSettings.addressCountry` in nexus). A form that posts what a person typed
57
+ — `922 123 456`, `(922) 123-456`, `0034 922 123 456` — keeps working and the
58
+ value is stored as `+34922123456`.
59
+
60
+ On `/api/site/leads` the phone stays optional: an absent key, `''` and
61
+ whitespace all store no phone, exactly as before.
62
+
63
+ A **non-blank** value that cannot be stored now returns an error instead of an
64
+ undialable string. There are two branches, and they answer differently:
65
+ - **shape** — shorter than 6 or longer than 32 characters, or containing
66
+ anything outside digits, spaces, `+`, dashes, parentheses and dots. An
67
+ extension always lands here, whichever way it is written (`x22`, `ext. 22`,
68
+ `;ext=22`, `#22`, `,22`), because every marker for one is outside that
69
+ allowlist — E.164 cannot represent an extension, so it is refused rather than
70
+ silently truncated to the switchboard. `/api/site/contact-form` answers 400;
71
+ `/api/site/leads` answers **422**, its long-standing status for a malformed
72
+ payload on any field. On that 422 the actionable message is in
73
+ `details.fieldErrors.phone` — the top-level `error` is the generic
74
+ `Validation error`.
75
+ - **resolution** — looks like a phone but cannot be resolved: the library does
76
+ not recognise it, or it lacks an international prefix while the tenant has no
77
+ country configured. **400** on both endpoints, with the message in `error` and
78
+ in `details.fieldErrors.phone`.
79
+
80
+ So: read `details.fieldErrors.phone` rather than branching on the status, and
81
+ do not assume the two endpoints answer with the same code for a bad shape.
82
+
83
+ On both endpoints the resolution prefers the LOCAL reading for a prefix-less
84
+ number over the international one, because the caller is typing (or posting)
85
+ into one specific tenant's form. The residual: a prefix-less number that is
86
+ also a valid national number of the tenant's own country is normalised into
87
+ that country rather than refused — for a Spanish tenant, Myanmar digits
88
+ `951234567` become `+34951234567`. The alternative — preferring the
89
+ international reading — reopens a different and worse class this file's own
90
+ header documents (a US number silently reading as Egyptian), so this is a
91
+ chosen trade, not an oversight. Quantifying how often the residual is actually
92
+ hit is a follow-up issue.
93
+
94
+ **Read-path change — `GET /api/site/config` (same shape, different VALUE).**
95
+ No field was added or removed here, but the VALUE of `address.telephone`
96
+ changes once the phone backfill runs against a tenant: today it can read
97
+ `34922123456` (the legacy shape stored before this issue); after the backfill
98
+ it reads `+34922123456`. Any SITE that pattern-matches or otherwise assumes the
99
+ prefix-less shape when rendering this field (a `tel:`/`wa.me` link builder,
100
+ for instance) should read this before the backfill runs against its tenant.
101
+
102
+ **Behaviour change — `POST /api/site/shop/checkout` (stricter).**
103
+ `customerPhone` and `shippingAddress.phone` must now be E.164 (`+34…`) or
104
+ absent; a bare local number is rejected. Unlike the contact form this payload is
105
+ assembled by the SITE from a cart rather than typed by a person at that moment,
106
+ so there is no "typed locally for this tenant" provenance to resolve against.
107
+
108
+ **Why the SDK does not build E.164 for you.** Every SITE would construct it
109
+ differently and each would be wrong in its own way — which is the defect this
110
+ issue fixes one layer down. The platform owns the rule because the platform is
111
+ the layer that knows the tenant's country. A shared formatter helper for SITEs
112
+ is REB-703, and it comes after this.
113
+
114
+ ***
115
+
116
+ **This release is `1.0.0`, and that is deliberate — see REB-755.**
117
+
118
+ `@reblu/site-contracts` has been at `0.x` while `@reblu/site-client` reached
119
+ 4.0.1. That ordering is hard to defend, and it had a concrete cost: consumers
120
+ pin `^0.5.0`, and **a caret on `0.x` does not cross the minor** (verified with
121
+ `semver@7`: `^0.5.0` admits `0.5.3`, not `0.6.0`). So every change — additive or
122
+ breaking — cost every consuming SITE repo a hand edit. The careful work of
123
+ deciding whether a change breaks anything bought nobody anything.
124
+
125
+ This release is breaking regardless, so the adoption is already owed. Shipping
126
+ `1.0.0` here means the toll and the breaking change are **one edit rather than
127
+ two** — a bare `1.0.0` later would make this a `2.0.0` and cost an extra
128
+ adoption for nothing.
129
+
130
+ From here, `^1.0.0` carries every additive change automatically. REB-651
131
+ (SEO fields, additive by design) lands as `1.1.0` and reaches consumers with no
132
+ edit. REB-665 reshapes `Config` and will be `2.0.0`.
133
+
134
+ `1.0.0` is not a claim that the contract is finished. It is a claim that
135
+ breakage will be signalled through the major — which we were doing anyway, into
136
+ a version range where it had no effect.
137
+
138
+ ## 0.5.0
139
+
140
+ ### Minor Changes
141
+
142
+ - 7dcfb4f: `Config` gains a `fiscal` block — the tenant's tax identifier, registered name
143
+ and official registry entry — plus the types `ConfigFiscal`,
144
+ `ConfigFiscalRegistry` and `ConfigTaxIdKind`.
145
+
146
+ A Spanish commercial site is legally required to publish its tax identifier, and
147
+ until now no tenant could enter one: `SiteSettings` had no `cif`, no `nif`, no
148
+ `taxId` and no registered name distinct from the trading name. Every SITE that
149
+ needed one therefore hardcoded it — the Dermamedicin footer carries
150
+ `B-XXXXXXXX` as a literal in its own repo, which is wrong for every other tenant
151
+ by construction. REB-73 (downloadable PDF invoice) was silently blocked on the
152
+ same gap: an invoice without the issuer's CIF is not a valid invoice.
153
+
154
+ ## Shape
155
+
156
+ ```ts
157
+ type ConfigFiscal = {
158
+ taxId: string | null
159
+ taxIdKind: 'NIF' | 'CIF' | null
160
+ legalName: string | null
161
+ registry: { name: string; number: string } | null
162
+ }
163
+
164
+ // on Config:
165
+ fiscal: ConfigFiscal | null
166
+ ```
167
+
168
+ ## `taxIdKind` — why the SITE is told which document it is holding
169
+
170
+ A tenant holds exactly one tax identifier, but it is not one document. In Spain
171
+ an autónomo files a **NIF** (8 digits + a mod-23 control letter, or a NIE for a
172
+ foreign resident) and every other legal form files a **CIF** (organisation
173
+ letter + 7 digits + a control character that is a digit for some organisation
174
+ letters and a letter for others). Which applies is derivable only from
175
+ `businessType`.
176
+
177
+ So `taxId` alone would force every SITE to re-implement that derivation, and a
178
+ SITE that hardcoded `CIF:` would print the wrong word next to an autónomo's
179
+ number. `taxIdKind` carries the platform's single derivation, and a SITE renders
180
+ the label verbatim:
181
+
182
+ ```tsx
183
+ {
184
+ config.fiscal?.taxId && (
185
+ <span>
186
+ {config.fiscal.taxIdKind}: {config.fiscal.taxId}
187
+ </span>
188
+ )
189
+ }
190
+ ```
191
+
192
+ `taxId` is stored normalised (uppercase, separators stripped), so it is safe to
193
+ render as-is.
194
+
195
+ ## `legalName` carries no fallback, on purpose
196
+
197
+ `legalName` is the registered name / razón social, which routinely differs from
198
+ the trading name in `business.name` ("Dermamedicin" vs "Dermamedicin Canarias,
199
+ S.L."). It is `null` when unset and does **not** silently fall back to the
200
+ trading name: a legal footer and an invoice need the registered one, and quietly
201
+ substituting the other would publish the wrong name on a document with legal
202
+ weight. A SITE that wants a fallback writes it itself, seeing that it is doing
203
+ so.
204
+
205
+ ## `registry` is sector-agnostic, and both-or-nothing
206
+
207
+ `registry` is a free-text `name` plus a `number`, not a `healthRegistryNumber`.
208
+ Every regulated sector is entered in one of these and only the registry's name
209
+ differs: Registro Sanitario for a clinic, RGSEAA for a restaurant, Registro de
210
+ Turismo for a hotel, a colegiado number for a self-employed professional, a
211
+ Registro Mercantil entry for a company. Naming the field after the first
212
+ tenant's sector would put medical vocabulary into a niche-agnostic contract,
213
+ which is the defect REB-628 exists to undo.
214
+
215
+ The object is `null` unless **both** members were filled in. A bare number with
216
+ no registry named is unreadable in a footer; a registry named with no number is
217
+ decorative.
218
+
219
+ ## Absence
220
+
221
+ `fiscal` is `null` when the tenant has entered nothing at all — the same shape
222
+ `address` and `social` already use. That is the signal to render **no** legal
223
+ block, rather than a block of empty labels. The underlying columns are nullable
224
+ with no default and are never backfilled, so "empty" here means "the owner has
225
+ never filled this in", never "filled in blank".
226
+
227
+ Additive: no existing field changed and no existing consumer breaks.
228
+
229
+ ## 0.4.0
230
+
231
+ ### Minor Changes
232
+
233
+ - eef13bf: Expose typed professional-profile fields on `Page`/`pages.list`, widen the list filter beyond the legacy DB page type, and add the pagination/sort params `pages.list` was missing.
234
+
235
+ `pageType='team'` pages carry a structured, sector-agnostic professional
236
+ profile (role, structured CV, social links) — previously trapped in
237
+ `Page.metadata`, which never reached the wire. `pageListItemSchema` and
238
+ `pageDetailSchema` now expose it as a typed `professional` field (`null` for
239
+ every other pageType).
240
+ - `@reblu/site-contracts` (minor, additive-only) publishes `ProfessionalSummary`,
241
+ `ProfessionalProfile`, `ProfessionalCv`, `ProfessionalCvEntry`,
242
+ `ProfessionalLanguage`, `ProfessionalLanguageLevel`, `ProfessionalSocialLink`,
243
+ `ProfessionalSocialPlatform`, and `PagesListSort`.
244
+ - `@reblu/site-client` (major — see below) `pages.list({ type })` is widened
245
+ from `LegacyPageType` (SERVICE|CUSTOM) to the flexible `PageType`
246
+ (service|custom|treatments|specialties|team|generic) — the value space the
247
+ route actually filters on. A typed SITE can now request `type: 'team'`
248
+ without escaping its own types. `type` also accepts an array for a
249
+ multi-type filter (repeated `?type=`).
250
+ - `pages.list` also gains `page`/`pageSize`/`sort` — `pagesListParamsSchema`
251
+ already accepted them and the route already returns a populated `meta`
252
+ (total/page/limit/pages), but `PagesListParams` exposed none of it, so a
253
+ typed caller had no way to request page 2 or change sort. Note the param
254
+ is `pageSize`, not `limit` — pages and blog genuinely differ (this route
255
+ reads `pageSize`; blog's reads `limit`), while the response `meta.limit`
256
+ key is the same word for a different request param on this endpoint.
257
+
258
+ **Breaking (major, not minor):** `PagesListParams.type`'s value space changes
259
+ from `LegacyPageType` to `PageType` — a caller passing `'SERVICE'`/`'CUSTOM'`
260
+ literally will no longer typecheck. No runtime behavior changes for existing
261
+ callers: the route was already silently dropping those values, since it only
262
+ ever validated the lowercase flexible set — but the type change itself is a
263
+ real breaking change for compilation, so this ships as major rather than
264
+ minor even though nothing behaves differently at runtime.
265
+
266
+ ## 0.3.0
267
+
268
+ ### Minor Changes
269
+
270
+ - 495d32d: Relax the contact-form input contract and add a `NOT_CONFIGURED` contact-form mode.
271
+
272
+ `ContactFormInput` now marks `surname` and `businessName` OPTIONAL. The contract
273
+ had required both from every caller, which made it a B2B lead-capture shape: a
274
+ clinic patient, a diner or a shopper has no company name, and a payload without
275
+ one died with a 400 before the captcha was ever consulted. Purely a relaxation —
276
+ a caller that already sends both keeps typechecking unchanged.
277
+
278
+ `ContactFormMode` gains a fifth member, `NOT_CONFIGURED`. No tenant was ever
279
+ provisioned with a `site_integration_config` row, and every read site coalesced
280
+ the absent row to `DISABLED` — so a brand-new tenant's SITE was told its owner had
281
+ switched the contact form off, a decision nobody had made. `NOT_CONFIGURED` is
282
+ that missing state named honestly: nobody has chosen yet. It is never selectable
283
+ by an owner; only provisioning writes it.
284
+
285
+ **Consumers doing an exhaustive `switch` on `ContactFormMode` must handle the new
286
+ member.** Treat it as non-operational — like `DISABLED`, it accepts no
287
+ submissions. The two differ only in what they say about _why_, which is the point:
288
+ a SITE can reasonably prompt the owner to finish setup on `NOT_CONFIGURED` while
289
+ staying quiet on `DISABLED`.
290
+
291
+ Note this is a minor rather than a patch because of that enum widening. The
292
+ package is pre-1.0, so a `switch` without a default is the realistic break.
293
+
294
+ ## 0.2.0
295
+
296
+ ### Minor Changes
297
+
298
+ - d081f12: Expose the academy course catalogue through the SDK.
299
+
300
+ The headless academy endpoints shipped in REB-528/REB-534, but their contract
301
+ file was never re-exported from base-api's contracts barrel, so the types could
302
+ not reach `@reblu/site-contracts` and SITEs had no typed way to call them.
303
+ - `@reblu/site-contracts` publishes the catalogue DTOs: `AcademyCourseSummary`,
304
+ `AcademyCourseDetail`, `AcademyCategoryPublic`, `AcademyModulePublic`,
305
+ `AcademyLessonPublic`, `AcademyInstructorPublic`, and the `AcademyCourseSort`
306
+ query union.
307
+ - `@reblu/site-client` adds the `academy` namespace: `academy.courses(params?)`
308
+ (filtered + paginated), `academy.bySlug(slug)`, and `academy.categories()`.
309
+ `sort` is typed as a union because the server silently ignores an unknown key,
310
+ so a typo is now a compile error instead of a no-op falling back to default
311
+ order.
312
+
313
+ Additive only — no existing export changes shape.
314
+
315
+ ## 0.1.2
316
+
317
+ ### Patch Changes
318
+
319
+ - 356e04d: Republish with `dist/` included and shop surface exposed.
320
+
321
+ `0.1.0` was published broken: the bootstrap publish ran without a build, so the
322
+ tarball shipped only `LICENSE`, `README.md`, and `package.json` — `dist/` was
323
+ absent and `main`/`module`/`types` pointed at files that did not exist, making
324
+ both packages unusable when installed.
325
+ - Add a `prepack` lifecycle script to both packages so `pnpm pack` / `npm pack`
326
+ / `npm publish` always run the build and emit `dist/` into the tarball. Build
327
+ before publish is now enforced by the package itself, not only by CI.
328
+ - `@reblu/site-client` ships the `shop` domain (`domains/shop.ts`) and
329
+ `@reblu/site-contracts` exports the matching shop types, so the SITE API shop
330
+ surface is available to external consumers.
package/dist/index.d.cts CHANGED
@@ -173,6 +173,12 @@ export type ShopTaxonomyTermRef = { taxonomyKey: string; taxonomyLabel: st
173
173
 
174
174
  export type ShopValidateCartInput = { items: { productId: string; quantity: number; variantId?: string | null | undefined; }[];}
175
175
 
176
+ export type SiteCapabilitySurfaceKey = "academy.enrollment" | "academy.catalogue" | "auth.login" | "consent.banner" | "legal.cookies"
177
+
178
+ export type SiteManifest = { manifestVersion: 1; surfaces: Record<string, true | { path?: string | undefined; }>; sdkVersion?: string | undefined;}
179
+
180
+ export type SiteManifestSurfaceValue = true | { path?: string | undefined;}
181
+
176
182
  export type TaxType = "IVA" | "IGIC" | "RETENTION" | "OTHER"
177
183
 
178
184
  export type TrustBadge = { id: string; name: string; logoUrl: string | null; url: string | null; type: "PARTNER" | "INSURANCE" | "SUBVENTION" | "CERTIFICATION" | "AWARD"; sortOrder: number;}
package/dist/index.d.ts CHANGED
@@ -173,6 +173,12 @@ export type ShopTaxonomyTermRef = { taxonomyKey: string; taxonomyLabel: st
173
173
 
174
174
  export type ShopValidateCartInput = { items: { productId: string; quantity: number; variantId?: string | null | undefined; }[];}
175
175
 
176
+ export type SiteCapabilitySurfaceKey = "academy.enrollment" | "academy.catalogue" | "auth.login" | "consent.banner" | "legal.cookies"
177
+
178
+ export type SiteManifest = { manifestVersion: 1; surfaces: Record<string, true | { path?: string | undefined; }>; sdkVersion?: string | undefined;}
179
+
180
+ export type SiteManifestSurfaceValue = true | { path?: string | undefined;}
181
+
176
182
  export type TaxType = "IVA" | "IGIC" | "RETENTION" | "OTHER"
177
183
 
178
184
  export type TrustBadge = { id: string; name: string; logoUrl: string | null; url: string | null; type: "PARTNER" | "INSURANCE" | "SUBVENTION" | "CERTIFICATION" | "AWARD"; sortOrder: number;}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reblu/site-contracts",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Public, dependency-free TypeScript types for the Reblu SITE API (types only — no Zod, no Prisma at runtime).",
6
6
  "license": "Apache-2.0",
@@ -43,7 +43,8 @@
43
43
  }
44
44
  },
45
45
  "files": [
46
- "dist"
46
+ "dist",
47
+ "CHANGELOG.md"
47
48
  ],
48
49
  "devDependencies": {
49
50
  "@size-limit/preset-small-lib": "^11.1.6",