@reblu/site-next 1.7.1 → 1.9.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 CHANGED
@@ -1,5 +1,54 @@
1
1
  # @reblu/site-next
2
2
 
3
+ ## 1.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 16dfaea: Add `createSiteManifestRoute` (REB-777), a new `./site-manifest` subpath.
8
+
9
+ A Route Handler factory for a SITE's capability manifest, same isolation
10
+ pattern as `./route-handlers` (REB-510): its own tsup entry, zero
11
+ dependencies (not even `next/server`), no `'use client'`, so importing it
12
+ from `app/.well-known/reblu-site.json/route.ts` never drags in any
13
+ client-component graph.
14
+
15
+ ```ts
16
+ export const GET = createSiteManifestRoute({
17
+ surfaces: { 'academy.enrollment': { path: '/academia/inscripcion' } },
18
+ })
19
+ ```
20
+
21
+ Serves `{ manifestVersion: 1, sdkVersion?, surfaces }` as JSON with a short
22
+ public `Cache-Control` (this payload holds no tenant secrets). `surfaces`
23
+ keys are typed against `@reblu/site-contracts`'s new
24
+ `SiteCapabilitySurfaceKey` — a typo in a surface key is a compile error, not
25
+ a silent no-op.
26
+
27
+ Purely additive — a new export, nothing existing changed.
28
+
29
+ ### Patch Changes
30
+
31
+ - d4dd66d: Fix `deriveValidationFields` throwing when called from a Server Component.
32
+ `@reblu/site-next/academy` carries a leading `'use client'` directive so
33
+ `AcademyEnrollmentForm` survives the build, which made the whole entry —
34
+ including the pure, dependency-free `deriveValidationFields` helper — look
35
+ client-only to Next's RSC boundary. Add `@reblu/site-next/academy-server`, a
36
+ new server-safe subpath exporting only `deriveValidationFields` and its
37
+ types, for Server Components to import from. `@reblu/site-next/academy`
38
+ still re-exports `deriveValidationFields` unchanged for existing consumers
39
+ and client-side call sites.
40
+ - Updated dependencies [16dfaea]
41
+ - @reblu/site-contracts@1.1.0
42
+
43
+ ## 1.8.0
44
+
45
+ ### Minor Changes
46
+
47
+ - 984f8b5: Add `@reblu/site-next/academy`, exporting `AcademyEnrollmentForm` — a
48
+ copy-injectable, per-tenant customizable enrollment-request form — and
49
+ `deriveValidationFields`, a pure helper that derives its validation fields
50
+ from the course/config data a SITE's academy API already returns.
51
+
3
52
  ## 1.7.1
4
53
 
5
54
  ### Patch Changes
package/README.md CHANGED
@@ -18,6 +18,10 @@ intentionally Next-coupled so the lower SDK layers —
18
18
  5. **reCAPTCHA v3** (`./recaptcha`) — the SITE's half of the contact-form
19
19
  captcha contract: the canonical action, the render gate, and the
20
20
  interaction-mount key. See "reCAPTCHA v3" below.
21
+ 6. **Academy** (`./academy`, `./academy-server`) — `AcademyEnrollmentForm`,
22
+ the anonymous enrollment-request form, plus `deriveValidationFields` to
23
+ build its per-tenant validation fields from course/config data. See
24
+ "Academy" below.
21
25
 
22
26
  ## Install
23
27
 
@@ -149,6 +153,93 @@ export const POST = createProxyRoute('/api/site/auth/login')
149
153
  `REBLU_API_BASE_URL`/`REBLU_API_KEY` env vars — read lazily per request, not at
150
154
  module load.
151
155
 
156
+ ## Academy
157
+
158
+ `@reblu/site-next/academy` exports the anonymous academy enrollment-request
159
+ form (`AcademyEnrollmentForm`) and re-exports the pure helper that derives its
160
+ validation fields from tenant data (`deriveValidationFields`).
161
+ `@reblu/site-next/academy-server` exports ONLY that pure helper.
162
+
163
+ **Which subpath to import from matters.** `./academy` carries a leading
164
+ `'use client'` directive — required so `AcademyEnrollmentForm` survives the
165
+ build — and the whole entry (helper included) is one directive-covered chunk.
166
+ Calling `deriveValidationFields` imported from `./academy` inside a Server
167
+ Component throws at runtime ("Attempted to call deriveValidationFields() from
168
+ the server but deriveValidationFields is on the client"), because Next's RSC
169
+ boundary sees the whole entry as client-only. A course detail page is
170
+ typically an async Server Component computing `validationFields` to pass down
171
+ as a prop — exactly the case that needs the server-safe subpath:
172
+
173
+ ```tsx
174
+ // app/academy/[slug]/page.tsx — an async Server Component
175
+ import { AcademyEnrollmentForm } from '@reblu/site-next/academy'
176
+ import { deriveValidationFields } from '@reblu/site-next/academy-server'
177
+
178
+ // `course` is whatever `GET /api/site/academy/courses/[slug]` (via
179
+ // `@reblu/site-client`'s `academy.bySlug`) already returned — this form never
180
+ // re-derives `salesEnabled` or requests anything extra.
181
+ const validationFields = deriveValidationFields(course)
182
+
183
+ {course.salesEnabled && (
184
+ <AcademyEnrollmentForm
185
+ enrollmentRequestUrl="/api/academy/enrollment-requests"
186
+ courseSlug={course.slug}
187
+ validationFields={validationFields}
188
+ />
189
+ )}
190
+ ```
191
+
192
+ Import `deriveValidationFields` from `./academy` instead only when the call
193
+ site is already inside a `'use client'` boundary (e.g. computing it alongside
194
+ other client-side state) — `./academy` still re-exports it for that case and
195
+ for call sites that prefer one import statement for both.
196
+
197
+ `enrollmentRequestUrl` is a SITE-owned Route Handler path — `/api/academy/
198
+ enrollment-requests` above is illustrative; the SITE chooses its own local
199
+ route. The browser can never call base-api directly (`X-Reblu-Api-Key` is a
200
+ server secret), so that local route proxies to the platform endpoint,
201
+ `POST /api/site/academy/enrollment-requests`. Wire it with `createProxyRoute`
202
+ from `./route-handlers`:
203
+
204
+ ```ts
205
+ // app/api/academy/enrollment-requests/route.ts
206
+ import { createProxyRoute } from '@reblu/site-next/route-handlers'
207
+
208
+ export const POST = createProxyRoute('/api/site/academy/enrollment-requests')
209
+ ```
210
+
211
+ **PER-TENANT, DATA-DRIVEN — not code-driven.** Whether a course asks for extra
212
+ verification, and which fields it asks for, is entirely tenant configuration:
213
+
214
+ - Today, `AcademyCourseDetail` exposes only `requiresValidation` +
215
+ `validationFieldLabel` (a single generic field). `deriveValidationFields`
216
+ falls back to that one field when no richer list is present.
217
+ - A future tenant-configurable list (e.g. `AcademyConfig.validationFields: [{
218
+ key, label, required, maxLength? }]`) is accepted by the SAME helper —
219
+ `AcademyValidationSource` is defined structurally, so a wider real contract
220
+ type is already assignable without an SDK release. Arbitrary keys (a
221
+ colegiado number, a professional college, anything a tenant configures) work
222
+ the moment the data carries them; `AcademyEnrollmentForm` never hardcodes a
223
+ field name — it only ever iterates the `validationFields` array it was given.
224
+ - A tenant with no validation requirement passes `validationFields={[]}` (or
225
+ omits the prop, `[]` is the default) — the form renders only the email
226
+ field.
227
+
228
+ **Never a purchase confirmation.** The upstream route
229
+ (`POST /api/site/academy/enrollment-requests`) replies UNIFORMLY whether the
230
+ email was brand-new (a request row was created) or already known (a sign-in
231
+ link was sent, nothing created) — anti-enumeration by design. The form's
232
+ always-visible copy says the request goes to human review and that no
233
+ automatic charge happens; `onSubmitted` fires on any 2xx and must never be
234
+ read as "a request was recorded".
235
+
236
+ **Field-level errors.** A non-2xx JSON body shaped like `{ error, details:
237
+ { fieldErrors } }` (`ApiErrorBody`/`ApiErrorDetails` from `@reblu/site-client`)
238
+ maps each `fieldErrors` key onto the matching input (`email`, or a
239
+ `validationFields[].key`) and focuses the first one; anything else — no
240
+ `details`, or a `429` — renders as a single form-level error instead. No
241
+ reCAPTCHA: the route is IP rate-limited server-side.
242
+
152
243
  ## Structured data (JSON-LD)
153
244
 
154
245
  `@reblu/site-next/json-ld` (REB-622, server-only, dependency-free) renders one
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/academy/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ deriveValidationFields: () => deriveValidationFields
24
+ });
25
+ module.exports = __toCommonJS(server_exports);
26
+
27
+ // src/academy/derive-validation-fields.ts
28
+ var DEFAULT_GENERIC_VALIDATION_KEY = "validationField";
29
+ var DEFAULT_GENERIC_VALIDATION_LABEL = "Dato de verificaci\xF3n";
30
+ function deriveValidationFields(source, options = {}) {
31
+ if (!source.requiresValidation) return [];
32
+ if (source.validationFields && source.validationFields.length > 0) {
33
+ return source.validationFields.map((field) => ({
34
+ key: field.key,
35
+ label: field.label,
36
+ required: field.required ?? true,
37
+ ...field.maxLength !== void 0 ? { maxLength: field.maxLength } : {}
38
+ }));
39
+ }
40
+ return [
41
+ {
42
+ key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,
43
+ label: source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,
44
+ required: source.validationFieldRequired ?? true
45
+ }
46
+ ];
47
+ }
48
+ // Annotate the CommonJS export names for ESM import in node:
49
+ 0 && (module.exports = {
50
+ deriveValidationFields
51
+ });
52
+ //# sourceMappingURL=academy-server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/academy/server.ts","../src/academy/derive-validation-fields.ts"],"sourcesContent":["/**\n * `@reblu/site-next/academy-server` — the server-safe half of the academy SDK\n * surface (defect fix, follow-up to REB-584/#1095).\n *\n * `deriveValidationFields` is a pure, dependency-free helper (no React, no\n * `@reblu/site-ui`) — exactly the kind of thing a Server Component should be\n * able to call directly, and the README/demo-site usage pattern always called\n * it inline in an async Server Component. But `./academy` (`src/academy/index.ts`)\n * carries its OWN leading `'use client'` directive so `AcademyEnrollmentForm`\n * survives the build (R4 — see `tsup.config.ts`), and with `splitting: false`\n * the whole entry is ONE self-contained chunk sharing that single directive\n * prologue. That made every import from `./academy`, including this pure\n * helper, look like a client-only export to Next's RSC boundary — calling\n * `deriveValidationFields` from a Server Component threw \"Attempted to call\n * deriveValidationFields() from the server but deriveValidationFields is on\n * the client\".\n *\n * This module is its OWN tsup entry (subpath `./academy-server`) with NO\n * React import of any kind, so it must NEVER carry `'use client'` — same\n * direction as `./route-handlers` and `./recaptcha`. Server Components MUST\n * import `deriveValidationFields` from here, not from `./academy`; Client\n * Components (or anywhere already inside a `'use client'` boundary) may keep\n * using either subpath — `./academy` still re-exports it for callers that\n * import it alongside `AcademyEnrollmentForm` in one statement.\n */\nexport {\n deriveValidationFields,\n type AcademyValidationField,\n type AcademyValidationFieldConfig,\n type AcademyValidationSource,\n type DeriveValidationFieldsOptions,\n} from './derive-validation-fields'\n","/**\n * `@reblu/site-next/academy` (also re-exported from the server-safe\n * `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-\n * field derivation (REB-584 follow-up).\n *\n * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks\n * for extra verification, and which fields it asks for, comes entirely from the\n * tenant's academy config / course data. This module never hardcodes a\n * sector-specific field name or label — the SDK does not know what a\n * \"colegiado\" is, and never will.\n *\n * Today the public course-detail contract (`AcademyCourseDetail`) exposes only\n * `requiresValidation` + `validationFieldLabel` — a single generic field, no\n * per-field `required` flag of its own (that flag lives on `AcademyConfig`,\n * server-side only, and isn't in the DTO yet). REB-83 is expected to add a\n * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,\n * label, required, maxLength? }]`) to that contract, which is how the upcoming\n * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code\n * change here.\n *\n * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from\n * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's\n * data actually has today, and remains satisfied once REB-83 lands a wider\n * real type — a course/config object with a `validationFields` array is already\n * assignable to this interface without a SITE-side type change.\n */\n\n/** A single tenant-defined (or derived) validation field the form should render. */\nexport interface AcademyValidationField {\n key: string\n label: string\n required: boolean\n maxLength?: number\n}\n\n/**\n * The shape a tenant-configured validation field list entry may take\n * (REB-83). `required` and `maxLength` are optional on the INPUT — the output\n * `AcademyValidationField` always carries a resolved `required`.\n */\nexport interface AcademyValidationFieldConfig {\n key: string\n label: string\n required?: boolean\n maxLength?: number\n}\n\n/**\n * Structural input — a subset of `AcademyCourseDetail` (today) plus the\n * optional REB-83 fields once the config/course exposes them. Any real DTO\n * that is at least this wide is assignable here without an import.\n */\nexport interface AcademyValidationSource {\n requiresValidation: boolean\n /** Present on `AcademyCourseDetail` today. */\n validationFieldLabel?: string | null\n /**\n * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —\n * accepted here so a SITE that already has it (e.g. from a wider tenant\n * config read) can pass it through; defaults to `true` (erring strict) when\n * absent, matching the existing demo-site precedent.\n */\n validationFieldRequired?: boolean | null\n /** REB-83 — tenant-defined list. When present and non-empty, takes priority\n * over the single-field fallback below. */\n validationFields?: AcademyValidationFieldConfig[] | null\n}\n\nexport interface DeriveValidationFieldsOptions {\n /** @default 'validationField' */\n genericKey?: string\n /** @default 'Dato de verificación' */\n genericLabel?: string\n}\n\nexport const DEFAULT_GENERIC_VALIDATION_KEY = 'validationField'\nexport const DEFAULT_GENERIC_VALIDATION_LABEL = 'Dato de verificación'\n\n/**\n * Derives the list of validation fields an enrollment form should render for\n * a course, from data alone.\n *\n * - `requiresValidation` is false → no fields (the form renders none).\n * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary\n * keys included, each entry's `required` defaulting to `true` when absent.\n * - Otherwise → falls back to the single generic field the SITE already knows\n * today, labeled from `validationFieldLabel` (or the generic fallback\n * label when the tenant configured no label).\n */\nexport function deriveValidationFields(\n source: AcademyValidationSource,\n options: DeriveValidationFieldsOptions = {},\n): AcademyValidationField[] {\n if (!source.requiresValidation) return []\n\n if (source.validationFields && source.validationFields.length > 0) {\n return source.validationFields.map((field) => ({\n key: field.key,\n label: field.label,\n required: field.required ?? true,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n }))\n }\n\n return [\n {\n key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,\n label:\n source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,\n required: source.validationFieldRequired ?? true,\n },\n ]\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC2EO,IAAM,iCAAiC;AACvC,IAAM,mCAAmC;AAazC,SAAS,uBACd,QACA,UAAyC,CAAC,GAChB;AAC1B,MAAI,CAAC,OAAO,mBAAoB,QAAO,CAAC;AAExC,MAAI,OAAO,oBAAoB,OAAO,iBAAiB,SAAS,GAAG;AACjE,WAAO,OAAO,iBAAiB,IAAI,CAAC,WAAW;AAAA,MAC7C,KAAK,MAAM;AAAA,MACX,OAAO,MAAM;AAAA,MACb,UAAU,MAAM,YAAY;AAAA,MAC5B,GAAI,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,IACxE,EAAE;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,MACE,KAAK,QAAQ,cAAc;AAAA,MAC3B,OACE,OAAO,wBAAwB,QAAQ,gBAAgB;AAAA,MACzD,UAAU,OAAO,2BAA2B;AAAA,IAC9C;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `@reblu/site-next/academy` (also re-exported from the server-safe
3
+ * `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-
4
+ * field derivation (REB-584 follow-up).
5
+ *
6
+ * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks
7
+ * for extra verification, and which fields it asks for, comes entirely from the
8
+ * tenant's academy config / course data. This module never hardcodes a
9
+ * sector-specific field name or label — the SDK does not know what a
10
+ * "colegiado" is, and never will.
11
+ *
12
+ * Today the public course-detail contract (`AcademyCourseDetail`) exposes only
13
+ * `requiresValidation` + `validationFieldLabel` — a single generic field, no
14
+ * per-field `required` flag of its own (that flag lives on `AcademyConfig`,
15
+ * server-side only, and isn't in the DTO yet). REB-83 is expected to add a
16
+ * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,
17
+ * label, required, maxLength? }]`) to that contract, which is how the upcoming
18
+ * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code
19
+ * change here.
20
+ *
21
+ * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from
22
+ * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's
23
+ * data actually has today, and remains satisfied once REB-83 lands a wider
24
+ * real type — a course/config object with a `validationFields` array is already
25
+ * assignable to this interface without a SITE-side type change.
26
+ */
27
+ /** A single tenant-defined (or derived) validation field the form should render. */
28
+ interface AcademyValidationField {
29
+ key: string;
30
+ label: string;
31
+ required: boolean;
32
+ maxLength?: number;
33
+ }
34
+ /**
35
+ * The shape a tenant-configured validation field list entry may take
36
+ * (REB-83). `required` and `maxLength` are optional on the INPUT — the output
37
+ * `AcademyValidationField` always carries a resolved `required`.
38
+ */
39
+ interface AcademyValidationFieldConfig {
40
+ key: string;
41
+ label: string;
42
+ required?: boolean;
43
+ maxLength?: number;
44
+ }
45
+ /**
46
+ * Structural input — a subset of `AcademyCourseDetail` (today) plus the
47
+ * optional REB-83 fields once the config/course exposes them. Any real DTO
48
+ * that is at least this wide is assignable here without an import.
49
+ */
50
+ interface AcademyValidationSource {
51
+ requiresValidation: boolean;
52
+ /** Present on `AcademyCourseDetail` today. */
53
+ validationFieldLabel?: string | null;
54
+ /**
55
+ * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —
56
+ * accepted here so a SITE that already has it (e.g. from a wider tenant
57
+ * config read) can pass it through; defaults to `true` (erring strict) when
58
+ * absent, matching the existing demo-site precedent.
59
+ */
60
+ validationFieldRequired?: boolean | null;
61
+ /** REB-83 — tenant-defined list. When present and non-empty, takes priority
62
+ * over the single-field fallback below. */
63
+ validationFields?: AcademyValidationFieldConfig[] | null;
64
+ }
65
+ interface DeriveValidationFieldsOptions {
66
+ /** @default 'validationField' */
67
+ genericKey?: string;
68
+ /** @default 'Dato de verificación' */
69
+ genericLabel?: string;
70
+ }
71
+ /**
72
+ * Derives the list of validation fields an enrollment form should render for
73
+ * a course, from data alone.
74
+ *
75
+ * - `requiresValidation` is false → no fields (the form renders none).
76
+ * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary
77
+ * keys included, each entry's `required` defaulting to `true` when absent.
78
+ * - Otherwise → falls back to the single generic field the SITE already knows
79
+ * today, labeled from `validationFieldLabel` (or the generic fallback
80
+ * label when the tenant configured no label).
81
+ */
82
+ declare function deriveValidationFields(source: AcademyValidationSource, options?: DeriveValidationFieldsOptions): AcademyValidationField[];
83
+
84
+ export { type AcademyValidationField, type AcademyValidationFieldConfig, type AcademyValidationSource, type DeriveValidationFieldsOptions, deriveValidationFields };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `@reblu/site-next/academy` (also re-exported from the server-safe
3
+ * `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-
4
+ * field derivation (REB-584 follow-up).
5
+ *
6
+ * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks
7
+ * for extra verification, and which fields it asks for, comes entirely from the
8
+ * tenant's academy config / course data. This module never hardcodes a
9
+ * sector-specific field name or label — the SDK does not know what a
10
+ * "colegiado" is, and never will.
11
+ *
12
+ * Today the public course-detail contract (`AcademyCourseDetail`) exposes only
13
+ * `requiresValidation` + `validationFieldLabel` — a single generic field, no
14
+ * per-field `required` flag of its own (that flag lives on `AcademyConfig`,
15
+ * server-side only, and isn't in the DTO yet). REB-83 is expected to add a
16
+ * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,
17
+ * label, required, maxLength? }]`) to that contract, which is how the upcoming
18
+ * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code
19
+ * change here.
20
+ *
21
+ * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from
22
+ * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's
23
+ * data actually has today, and remains satisfied once REB-83 lands a wider
24
+ * real type — a course/config object with a `validationFields` array is already
25
+ * assignable to this interface without a SITE-side type change.
26
+ */
27
+ /** A single tenant-defined (or derived) validation field the form should render. */
28
+ interface AcademyValidationField {
29
+ key: string;
30
+ label: string;
31
+ required: boolean;
32
+ maxLength?: number;
33
+ }
34
+ /**
35
+ * The shape a tenant-configured validation field list entry may take
36
+ * (REB-83). `required` and `maxLength` are optional on the INPUT — the output
37
+ * `AcademyValidationField` always carries a resolved `required`.
38
+ */
39
+ interface AcademyValidationFieldConfig {
40
+ key: string;
41
+ label: string;
42
+ required?: boolean;
43
+ maxLength?: number;
44
+ }
45
+ /**
46
+ * Structural input — a subset of `AcademyCourseDetail` (today) plus the
47
+ * optional REB-83 fields once the config/course exposes them. Any real DTO
48
+ * that is at least this wide is assignable here without an import.
49
+ */
50
+ interface AcademyValidationSource {
51
+ requiresValidation: boolean;
52
+ /** Present on `AcademyCourseDetail` today. */
53
+ validationFieldLabel?: string | null;
54
+ /**
55
+ * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —
56
+ * accepted here so a SITE that already has it (e.g. from a wider tenant
57
+ * config read) can pass it through; defaults to `true` (erring strict) when
58
+ * absent, matching the existing demo-site precedent.
59
+ */
60
+ validationFieldRequired?: boolean | null;
61
+ /** REB-83 — tenant-defined list. When present and non-empty, takes priority
62
+ * over the single-field fallback below. */
63
+ validationFields?: AcademyValidationFieldConfig[] | null;
64
+ }
65
+ interface DeriveValidationFieldsOptions {
66
+ /** @default 'validationField' */
67
+ genericKey?: string;
68
+ /** @default 'Dato de verificación' */
69
+ genericLabel?: string;
70
+ }
71
+ /**
72
+ * Derives the list of validation fields an enrollment form should render for
73
+ * a course, from data alone.
74
+ *
75
+ * - `requiresValidation` is false → no fields (the form renders none).
76
+ * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary
77
+ * keys included, each entry's `required` defaulting to `true` when absent.
78
+ * - Otherwise → falls back to the single generic field the SITE already knows
79
+ * today, labeled from `validationFieldLabel` (or the generic fallback
80
+ * label when the tenant configured no label).
81
+ */
82
+ declare function deriveValidationFields(source: AcademyValidationSource, options?: DeriveValidationFieldsOptions): AcademyValidationField[];
83
+
84
+ export { type AcademyValidationField, type AcademyValidationFieldConfig, type AcademyValidationSource, type DeriveValidationFieldsOptions, deriveValidationFields };
@@ -0,0 +1,25 @@
1
+ // src/academy/derive-validation-fields.ts
2
+ var DEFAULT_GENERIC_VALIDATION_KEY = "validationField";
3
+ var DEFAULT_GENERIC_VALIDATION_LABEL = "Dato de verificaci\xF3n";
4
+ function deriveValidationFields(source, options = {}) {
5
+ if (!source.requiresValidation) return [];
6
+ if (source.validationFields && source.validationFields.length > 0) {
7
+ return source.validationFields.map((field) => ({
8
+ key: field.key,
9
+ label: field.label,
10
+ required: field.required ?? true,
11
+ ...field.maxLength !== void 0 ? { maxLength: field.maxLength } : {}
12
+ }));
13
+ }
14
+ return [
15
+ {
16
+ key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,
17
+ label: source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,
18
+ required: source.validationFieldRequired ?? true
19
+ }
20
+ ];
21
+ }
22
+ export {
23
+ deriveValidationFields
24
+ };
25
+ //# sourceMappingURL=academy-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/academy/derive-validation-fields.ts"],"sourcesContent":["/**\n * `@reblu/site-next/academy` (also re-exported from the server-safe\n * `@reblu/site-next/academy-server` subpath — see `./server.ts`) — validation-\n * field derivation (REB-584 follow-up).\n *\n * PER-TENANT, DATA-DRIVEN (not code-driven): whether an enrollment request asks\n * for extra verification, and which fields it asks for, comes entirely from the\n * tenant's academy config / course data. This module never hardcodes a\n * sector-specific field name or label — the SDK does not know what a\n * \"colegiado\" is, and never will.\n *\n * Today the public course-detail contract (`AcademyCourseDetail`) exposes only\n * `requiresValidation` + `validationFieldLabel` — a single generic field, no\n * per-field `required` flag of its own (that flag lives on `AcademyConfig`,\n * server-side only, and isn't in the DTO yet). REB-83 is expected to add a\n * genuinely tenant-configurable list (`AcademyConfig.validationFields: [{ key,\n * label, required, maxLength? }]`) to that contract, which is how the upcoming\n * `collegiateNumber` / `professionalCollege` keys arrive — as DATA, not a code\n * change here.\n *\n * `AcademyValidationSource` is therefore defined STRUCTURALLY, not imported from\n * `@reblu/site-contracts`: it accepts whatever subset of fields the caller's\n * data actually has today, and remains satisfied once REB-83 lands a wider\n * real type — a course/config object with a `validationFields` array is already\n * assignable to this interface without a SITE-side type change.\n */\n\n/** A single tenant-defined (or derived) validation field the form should render. */\nexport interface AcademyValidationField {\n key: string\n label: string\n required: boolean\n maxLength?: number\n}\n\n/**\n * The shape a tenant-configured validation field list entry may take\n * (REB-83). `required` and `maxLength` are optional on the INPUT — the output\n * `AcademyValidationField` always carries a resolved `required`.\n */\nexport interface AcademyValidationFieldConfig {\n key: string\n label: string\n required?: boolean\n maxLength?: number\n}\n\n/**\n * Structural input — a subset of `AcademyCourseDetail` (today) plus the\n * optional REB-83 fields once the config/course exposes them. Any real DTO\n * that is at least this wide is assignable here without an import.\n */\nexport interface AcademyValidationSource {\n requiresValidation: boolean\n /** Present on `AcademyCourseDetail` today. */\n validationFieldLabel?: string | null\n /**\n * NOT yet in `AcademyCourseDetail` (server-only, on `AcademyConfig`) —\n * accepted here so a SITE that already has it (e.g. from a wider tenant\n * config read) can pass it through; defaults to `true` (erring strict) when\n * absent, matching the existing demo-site precedent.\n */\n validationFieldRequired?: boolean | null\n /** REB-83 — tenant-defined list. When present and non-empty, takes priority\n * over the single-field fallback below. */\n validationFields?: AcademyValidationFieldConfig[] | null\n}\n\nexport interface DeriveValidationFieldsOptions {\n /** @default 'validationField' */\n genericKey?: string\n /** @default 'Dato de verificación' */\n genericLabel?: string\n}\n\nexport const DEFAULT_GENERIC_VALIDATION_KEY = 'validationField'\nexport const DEFAULT_GENERIC_VALIDATION_LABEL = 'Dato de verificación'\n\n/**\n * Derives the list of validation fields an enrollment form should render for\n * a course, from data alone.\n *\n * - `requiresValidation` is false → no fields (the form renders none).\n * - A non-empty `validationFields` list (REB-83) → used verbatim, arbitrary\n * keys included, each entry's `required` defaulting to `true` when absent.\n * - Otherwise → falls back to the single generic field the SITE already knows\n * today, labeled from `validationFieldLabel` (or the generic fallback\n * label when the tenant configured no label).\n */\nexport function deriveValidationFields(\n source: AcademyValidationSource,\n options: DeriveValidationFieldsOptions = {},\n): AcademyValidationField[] {\n if (!source.requiresValidation) return []\n\n if (source.validationFields && source.validationFields.length > 0) {\n return source.validationFields.map((field) => ({\n key: field.key,\n label: field.label,\n required: field.required ?? true,\n ...(field.maxLength !== undefined ? { maxLength: field.maxLength } : {}),\n }))\n }\n\n return [\n {\n key: options.genericKey ?? DEFAULT_GENERIC_VALIDATION_KEY,\n label:\n source.validationFieldLabel || options.genericLabel || DEFAULT_GENERIC_VALIDATION_LABEL,\n required: source.validationFieldRequired ?? true,\n },\n ]\n}\n"],"mappings":";AA2EO,IAAM,iCAAiC;AACvC,IAAM,mCAAmC;AAazC,SAAS,uBACd,QACA,UAAyC,CAAC,GAChB;AAC1B,MAAI,CAAC,OAAO,mBAAoB,QAAO,CAAC;AAExC,MAAI,OAAO,oBAAoB,OAAO,iBAAiB,SAAS,GAAG;AACjE,WAAO,OAAO,iBAAiB,IAAI,CAAC,WAAW;AAAA,MAC7C,KAAK,MAAM;AAAA,MACX,OAAO,MAAM;AAAA,MACb,UAAU,MAAM,YAAY;AAAA,MAC5B,GAAI,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,IACxE,EAAE;AAAA,EACJ;AAEA,SAAO;AAAA,IACL;AAAA,MACE,KAAK,QAAQ,cAAc;AAAA,MAC3B,OACE,OAAO,wBAAwB,QAAQ,gBAAgB;AAAA,MACzD,UAAU,OAAO,2BAA2B;AAAA,IAC9C;AAAA,EACF;AACF;","names":[]}