@asteby/storefront-react 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -2
- package/dist/account-server.cjs +128 -19
- package/dist/account-server.cjs.map +1 -1
- package/dist/account-server.d.cts +67 -4
- package/dist/account-server.d.ts +67 -4
- package/dist/account-server.js +125 -20
- package/dist/account-server.js.map +1 -1
- package/dist/account.cjs +252 -7
- package/dist/account.cjs.map +1 -1
- package/dist/account.css +91 -0
- package/dist/account.d.cts +130 -6
- package/dist/account.d.ts +130 -6
- package/dist/account.js +247 -8
- package/dist/account.js.map +1 -1
- package/dist/checkout.cjs +162 -8
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +104 -4
- package/dist/checkout.d.ts +104 -4
- package/dist/checkout.js +158 -10
- package/dist/checkout.js.map +1 -1
- package/dist/chunk-Z4C4LKXF.js +25 -0
- package/dist/chunk-Z4C4LKXF.js.map +1 -0
- package/dist/fit-key-B9sYQVwG.d.cts +49 -0
- package/dist/fit-key-B9sYQVwG.d.ts +49 -0
- package/dist/fitment.cjs +54 -9
- package/dist/fitment.cjs.map +1 -1
- package/dist/fitment.d.cts +20 -2
- package/dist/fitment.d.ts +20 -2
- package/dist/fitment.js +54 -10
- package/dist/fitment.js.map +1 -1
- package/dist/settings-types-jxFQFFqP.d.cts +77 -0
- package/dist/settings-types-jxFQFFqP.d.ts +77 -0
- package/dist/subjects-server.cjs +248 -0
- package/dist/subjects-server.cjs.map +1 -0
- package/dist/subjects-server.d.cts +130 -0
- package/dist/subjects-server.d.ts +130 -0
- package/dist/subjects-server.js +217 -0
- package/dist/subjects-server.js.map +1 -0
- package/dist/subjects.cjs +501 -0
- package/dist/subjects.cjs.map +1 -0
- package/dist/subjects.css +166 -0
- package/dist/subjects.d.cts +178 -0
- package/dist/subjects.d.ts +178 -0
- package/dist/subjects.js +465 -0
- package/dist/subjects.js.map +1 -0
- package/package.json +23 -2
- package/dist/types-D7pVK0GK.d.cts +0 -39
- package/dist/types-D7pVK0GK.d.ts +0 -39
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/subjects/fit-key.ts","../src/subjects/server.ts"],"names":["resolveSubject","sameSite","isStorefrontError"],"mappings":"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","file":"subjects-server.cjs","sourcesContent":["// The compatibility key of a saved item, read backwards. The guest keeps only `fit_key` in a cookie (never a plate or\n// anything private); to show its label and to filter by it, it is resolved against the kind's catalog again.\nimport { resolveSubject, type ResolvedSubject, type SubjectKind } from '@asteby/storefront'\n\n/** Shape of a key: what `fit_key` can hold (catalog ids and slugs joined by the template's separators). */\nconst KEY_RE = /^[a-z0-9._+/-]{1,100}$/\n\nconst escape = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n\n/**\n * Reads a key back into the item it names: the values are taken from the kind's template and validated against the\n * catalog like a saved item. `null` for a key that is not well formed, does not fit the template or does not resolve\n * (stale catalog, tampered cookie). Free (non-catalog) fields come back as their slug, which is all the key kept.\n */\nexport function subjectFromFitKey(kind: SubjectKind, fitKey: string | null | undefined): ResolvedSubject | null {\n if (!fitKey || !KEY_RE.test(fitKey) || !kind.fit_key_template) return null\n const names: string[] = []\n const pattern = kind.fit_key_template\n .split(/(\\{[a-z][a-z0-9_]*\\})/)\n .map((part) => {\n const m = /^\\{([a-z][a-z0-9_]*)\\}$/.exec(part)\n if (!m) return escape(part)\n names.push(m[1]!)\n return '([^/]+)'\n })\n .join('')\n const match = new RegExp(`^${pattern}$`).exec(fitKey)\n if (!match) return null\n const values: Record<string, string> = {}\n names.forEach((n, i) => (values[n] = match[i + 1]!))\n const r = resolveSubject(kind, values)\n // The key must be exactly the one the server would compute for those values.\n return r.ok && r.subject.fit_key === fitKey ? r.subject : null\n}\n","/**\n * Server side of «My items» (`@asteby/storefront-react/subjects/server`): the guest's cookie, the cache rule, the loader\n * (`loadSubjectsContext`, never throws) and the action (`createSubjectAction`). No React, no router: Web `Request` /\n * `Response` only, so any framework mounts it. The browser never talks to OPS.\n *\n * Privacy: the cookie holds ONLY the compatibility key (`fit_key`), and `serializeSubjectCookie` refuses a key that does\n * not resolve against the kind's catalog, so a licence plate or any private value can never be written to it. Nothing in\n * this file logs a value, and a private value is never echoed back (`SubjectActionData.values` carries public ones only).\n */\nimport {\n isStorefrontError,\n type StorefrontClient,\n type Subject,\n type SubjectInput,\n type SubjectKind,\n type SubjectPatch,\n type SubjectProblemCode,\n} from '@asteby/storefront'\nimport { subjectFromFitKey } from './fit-key.js'\nimport type { SubjectActionData, SubjectErrorCode, SubjectIntent } from './types.js'\n\nexport type { SubjectActionData, SubjectErrorCode, SubjectIntent } from './types.js'\nexport { subjectFromFitKey } from './fit-key.js'\n\n// ---- the guest's cookie ------------------------------------------------------------------------------------------------\n\nexport const SUBJECT_COOKIE_PREFIX = 'sf_subject_'\nconst KIND_KEY_RE = /^[a-z][a-z0-9_]{0,31}$/\n\n/** Cookie name of a kind: `sf_subject_<kind>`. */\nexport function subjectCookieName(kind: Pick<SubjectKind, 'key'> | string): string {\n const key = typeof kind === 'string' ? kind : kind.key\n if (!KIND_KEY_RE.test(key)) throw new TypeError('subjectCookieName: not a kind key')\n return SUBJECT_COOKIE_PREFIX + key\n}\n\n/** The key the guest chose, validated against the kind's catalog; `null` when absent, tampered or stale. */\nexport function readSubjectFitKey(cookieHeader: string | null | undefined, kind: SubjectKind): string | null {\n if (!cookieHeader) return null\n const name = subjectCookieName(kind)\n for (const part of cookieHeader.split(';')) {\n const eq = part.indexOf('=')\n if (eq < 0 || part.slice(0, eq).trim() !== name) continue\n let raw = part.slice(eq + 1).trim().replace(/^\"(.*)\"$/, '$1')\n try {\n raw = decodeURIComponent(raw)\n } catch {\n return null\n }\n return subjectFromFitKey(kind, raw) ? raw : null\n }\n return null\n}\n\nexport type SubjectCookieOptions = {\n /** Lifetime in days (default 365). */\n maxAgeDays?: number\n path?: string\n domain?: string\n sameSite?: 'Lax' | 'Strict' | 'None'\n /** Defaults to true when `sameSite` is `None`; set it on https. */\n secure?: boolean\n}\n\n/**\n * `Set-Cookie` value that remembers the guest's item: `HttpOnly`, `SameSite=Lax`, one year. Pass `null` to clear it.\n * It throws `TypeError` for a key that does not resolve against the kind: only a real `fit_key` can reach the cookie.\n */\nexport function serializeSubjectCookie(kind: SubjectKind, fitKey: string | null, options: SubjectCookieOptions = {}): string {\n if (fitKey !== null && !subjectFromFitKey(kind, fitKey)) throw new TypeError('serializeSubjectCookie: not a compatibility key of this kind')\n const parts = [`${subjectCookieName(kind)}=${fitKey ? encodeURIComponent(fitKey) : ''}`, `Path=${options.path ?? '/'}`]\n parts.push(`Max-Age=${fitKey ? Math.round((options.maxAgeDays ?? 365) * 86_400) : 0}`)\n if (options.domain) parts.push(`Domain=${options.domain}`)\n const sameSite = options.sameSite ?? 'Lax'\n parts.push(`SameSite=${sameSite}`, 'HttpOnly')\n if (options.secure ?? sameSite === 'None') parts.push('Secure')\n return parts.join('; ')\n}\n\n// ---- cache --------------------------------------------------------------------------------------------------------------\n\n/** Is there a remembered item (of any kind) in the request? Then the page depends on the buyer. */\nexport function hasActiveSubject(cookieHeader: string | null | undefined): boolean {\n return !!cookieHeader && cookieHeader.split(';').some((p) => p.trim().startsWith(SUBJECT_COOKIE_PREFIX) && p.indexOf('=') > 0 && !/=\\s*$/.test(p))\n}\n\n/**\n * Cache headers of a page that changes with the buyer's item: with one remembered it is the buyer's (`private, no-store`);\n * without, the store's public policy applies. Pass the store's own `Cache-Control` for the public case. Both answers carry\n * `Vary: Cookie`: the public copy is only right for a request WITHOUT the cookie, and a shared cache (CDN) that stored it\n * must not hand it to a guest who has one (their «only what fits my vehicle» listing would come back unfiltered).\n */\nexport function subjectCacheHeaders(cookieHeader: string | null | undefined, publicCacheControl: string, opts: { signedIn?: boolean } = {}): Record<string, string> {\n if (opts.signedIn || hasActiveSubject(cookieHeader)) return { 'Cache-Control': 'private, no-store', Vary: 'Cookie' }\n return { 'Cache-Control': publicCacheControl, Vary: 'Cookie' }\n}\n\n// ---- loader -------------------------------------------------------------------------------------------------------------\n\nexport type ActiveSubject = {\n kind: string\n label: string\n fit_key: string\n /** `account`: the buyer's active saved item; `cookie`: what the guest remembered. */\n source: 'account' | 'cookie'\n /** The saved item (`account` only). */\n subject?: Subject\n}\n\nexport type SubjectsContext = {\n kinds: SubjectKind[]\n /** The buyer's saved items (empty for a guest or when they could not be read). */\n subjects: Subject[]\n /** Per kind key: the item that applies (the buyer's active one, else the guest's cookie). */\n active: Record<string, ActiveSubject>\n /** Something could not be read (the rest is still true): show «We could not load this · Try again» for that part. */\n degraded: { kinds: boolean; subjects: boolean }\n}\n\n/** What is assumed when nothing can be asked: no kinds, so the section simply does not exist. */\nexport const NO_SUBJECTS: Readonly<SubjectsContext> = Object.freeze({ kinds: [], subjects: [], active: {}, degraded: { kinds: true, subjects: false } })\n\nexport type LoadSubjectsContextOptions = {\n /** The buyer-scoped client (`withCustomerToken`); omit for a guest. */\n customer?: StorefrontClient | null\n cookieHeader?: string | null\n signal?: AbortSignal\n}\n\n/**\n * The kinds the store declares, the buyer's saved items and the one that applies to each kind. It NEVER throws: an\n * organization that declares nothing (or a server that predates it) gives `kinds: []` and the section does not exist; a\n * failure reading the items keeps the kinds and flags `degraded.subjects`. Use it as the loader of any page that\n * lists parts or the buyer's garage.\n */\nexport async function loadSubjectsContext(sf: StorefrontClient, options: LoadSubjectsContextOptions = {}): Promise<SubjectsContext> {\n const opt = options.signal ? { signal: options.signal } : undefined\n let kinds: SubjectKind[]\n try {\n kinds = await sf.content.subjectKinds(opt)\n } catch {\n return NO_SUBJECTS\n }\n if (kinds.length === 0) return { kinds: [], subjects: [], active: {}, degraded: { kinds: false, subjects: false } }\n let subjects: Subject[] = []\n let degraded = false\n if (options.customer) {\n try {\n subjects = await options.customer.customer.subjects.list(undefined, opt)\n } catch {\n degraded = true\n }\n }\n const active: Record<string, ActiveSubject> = {}\n for (const kind of kinds) {\n const mine = subjects.filter((s) => s.kind === kind.key)\n const chosen = mine.find((s) => s.is_default) ?? mine[0]\n if (chosen) {\n active[kind.key] = { kind: kind.key, label: chosen.label, fit_key: chosen.fit_key, source: 'account', subject: chosen }\n continue\n }\n const key = readSubjectFitKey(options.cookieHeader, kind)\n const guest = key ? subjectFromFitKey(kind, key) : null\n if (key && guest) active[kind.key] = { kind: kind.key, label: guest.label, fit_key: key, source: 'cookie' }\n }\n return { kinds, subjects, active, degraded: { kinds: false, subjects: degraded } }\n}\n\n// ---- action -------------------------------------------------------------------------------------------------------------\n\nexport type ParsedSubjectForm = {\n intent: SubjectIntent | null\n id: string\n kind: string\n /** Public and private values typed (empty ones left out; a private one flagged to clear is `null`). */\n attributes: Record<string, string | null>\n /** Public values only, to refill the form. */\n echo: Record<string, string>\n nickname: string | null | undefined\n isDefault: boolean\n}\n\nconst text = (v: FormDataEntryValue | null): string => (typeof v === 'string' ? v.trim() : '')\n\n/**\n * Reads the form the components post: `intent`, `id`, `kind`, `<prefix>.<field>` for each field (`<prefix>.<field>.clear`\n * to remove a stored private value), `nickname`, `is_default`. `privateKeys` are the kind's private fields: they are never\n * part of `echo`.\n */\nexport function parseSubjectForm(form: FormData, privateKeys: readonly string[] = [], prefix = 'subject'): ParsedSubjectForm {\n const attributes: Record<string, string | null> = {}\n const echo: Record<string, string> = {}\n const head = `${prefix}.`\n for (const [name, value] of form.entries()) {\n if (!name.startsWith(head) || name.endsWith('.clear')) continue\n const key = name.slice(head.length)\n const v = text(value)\n if (v) {\n attributes[key] = v\n if (!privateKeys.includes(key)) echo[key] = v\n }\n }\n for (const key of privateKeys) if (form.get(`${head}${key}.clear`)) attributes[key] = null\n // The picker posts a hidden `intent=subject-save` AND, without JavaScript, the «Continue» button's own\n // `intent=subject-refine` (the submitter's entry comes after it): refine wins, so «Continue» never saves.\n const intents = form.getAll('intent').map(text)\n const intent = intents.includes('subject-refine') ? 'subject-refine' : (intents[0] ?? '')\n const nick = form.has('nickname') ? text(form.get('nickname')) : undefined\n return {\n intent: intent === 'subject-save' || intent === 'subject-delete' || intent === 'subject-default' || intent === 'subject-refine' ? intent : null,\n id: text(form.get('id')),\n kind: text(form.get('kind')),\n attributes,\n echo,\n nickname: nick === undefined ? undefined : nick || null,\n isDefault: !!form.get('is_default'),\n }\n}\n\nconst fail = (intent: SubjectIntent | undefined, code: SubjectErrorCode, extra: Partial<SubjectActionData> = {}): SubjectActionData => ({\n ok: false,\n ...(intent ? { intent } : {}),\n ...extra,\n error: { code, ...(extra.error?.field ? { field: extra.error.field } : {}) },\n})\n\n/** `attributes.make` → `make`. */\nconst fieldOf = (field: string | undefined): string | undefined => (field ? field.replace(/^attributes\\./, '') : undefined)\n\n/**\n * Why a field was refused, from the server's message. The real server writes «attributes.make: is not in the catalog»;\n * the mock writes «make: not_in_catalog». Both map to one code; the value is never part of either message.\n */\nfunction problemOf(message: string | undefined): SubjectProblemCode | 'invalid' {\n const m = (message ?? '').toLowerCase()\n const coded = /:\\s*(required|unknown|invalid|not_in_catalog|year_range|catalog_unavailable)\\b/.exec(m)?.[1]\n if (coded) return coded as SubjectProblemCode\n if (/:\\s*required\\b/.test(m)) return 'required'\n if (m.includes('unknown field')) return 'unknown'\n if (m.includes('not in the catalog')) return 'not_in_catalog'\n if (m.includes('outside the years')) return 'year_range'\n if (m.includes('catalog is not available')) return 'catalog_unavailable'\n return 'invalid'\n}\n\nfunction toFailure(err: unknown, intent: SubjectIntent, values: Record<string, string>, id?: string): SubjectActionData {\n const carry = { values, ...(id ? { id } : {}) }\n if (!isStorefrontError(err)) return fail(intent, 'unavailable', carry)\n if (err.code === 'insufficient_scope') return fail(intent, 'store_misconfigured', carry)\n switch (err.status) {\n case 401:\n return fail(intent, 'unauthorized', carry)\n case 404:\n return fail(intent, 'not_found', carry)\n case 409:\n return fail(intent, err.field === 'kind' ? 'limit' : 'duplicate', carry)\n case 429:\n return fail(intent, 'rate_limited', carry)\n case 403:\n return fail(intent, 'unsupported', carry)\n case 400:\n case 422: {\n const field = fieldOf(err.field)\n const why = problemOf(err.message)\n return fail(intent, err.field === 'kind' ? 'unsupported' : 'validation', {\n ...carry,\n ...(field ? { fieldErrors: { [field]: why }, error: { code: 'validation', field } } : {}),\n })\n }\n default:\n return fail(intent, 'unavailable', carry)\n }\n}\n\nfunction sameSite(request: Request): boolean {\n const site = request.headers.get('sec-fetch-site')\n if (site && site !== 'same-origin' && site !== 'same-site' && site !== 'none') return false\n const origin = request.headers.get('origin')\n if (!origin) return true\n try {\n const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? new URL(request.url).host\n return new URL(origin).host === host.split(',')[0]!.trim()\n } catch {\n return false\n }\n}\n\nexport type CreateSubjectActionOptions = {\n storefront: StorefrontClient\n /** The buyer's session token from the store's own cookie (`readCustomerToken`) or a cookie object with `parse`. */\n readCustomerToken?: (request: Request) => string | null | undefined | Promise<string | null | undefined>\n customerCookie?: { parse(header: string | null): unknown | Promise<unknown> }\n clientIp?: (request: Request) => string | null | undefined\n /** Field-name prefix of the form (default `subject`). */\n namePrefix?: string\n /** Refuse a cross-site POST (default true). */\n sameSiteOnly?: boolean\n}\n\n/**\n * The action of the «My items» forms: one `POST` with an `intent` field.\n *\n * | intent | fields | does |\n * | ----------------- | ------------------------------------------------------------ | --------------------------------------------- |\n * | `subject-save` | kind, `subject.<field>`…, nickname?, is_default?, id? (edit) | creates, or updates when `id` is set |\n * | `subject-delete` | id | `customer.subjects.delete` (physical) |\n * | `subject-default` | id | makes it the active item of its kind |\n * | `subject-refine` | `subject.<field>`… | saves nothing: echoes the values (no-JS cascade) |\n *\n * It never redirects: it returns a `SubjectActionData` for the page to render, with 422s mapped to the field\n * (`fieldErrors.make`). An empty private field on edit keeps the stored value; `subject.<field>.clear` removes it.\n * Nothing is logged and no private value is echoed.\n */\nexport function createSubjectAction(options: CreateSubjectActionOptions) {\n const { storefront: sf } = options\n const prefix = options.namePrefix ?? 'subject'\n async function token(request: Request): Promise<string | null> {\n if (options.readCustomerToken) return (await options.readCustomerToken(request)) || null\n if (!options.customerCookie) return null\n const v = await options.customerCookie.parse(request.headers.get('cookie'))\n return typeof v === 'string' && v ? v : null\n }\n return async function subjectAction({ request }: { request: Request }): Promise<SubjectActionData> {\n if (options.sameSiteOnly !== false && !sameSite(request)) return fail(undefined, 'unsupported')\n const form = await request.formData().catch(() => null)\n if (!form) return fail(undefined, 'validation')\n const tok = await token(request)\n const ip = options.clientIp?.(request)\n const w = { ...(ip ? { clientIp: ip } : {}), ...(tok ? { customerToken: tok } : {}) }\n // The kind is known to the store, not trusted from the form: which fields are private comes from the data.\n const rawKind = text(form.get('kind'))\n let privateKeys: string[] = []\n try {\n const kinds = await sf.content.subjectKinds()\n privateKeys = kinds.find((k) => k.key === rawKind)?.fields.filter((f) => f.private).map((f) => f.key) ?? []\n } catch {\n /* the write below answers for itself */\n }\n const p = parseSubjectForm(form, privateKeys, prefix)\n const intent = p.intent\n if (!intent) return fail(undefined, 'validation')\n // Without JavaScript the cascade advances by posting: nothing is saved, the form is drawn again with these values.\n if (intent === 'subject-refine') return { ok: false, intent, values: p.echo, ...(p.id ? { id: p.id } : {}) }\n if (!tok) return fail(intent, 'unauthorized', { values: p.echo, ...(p.id ? { id: p.id } : {}) })\n try {\n switch (intent) {\n case 'subject-save': {\n if (!p.kind && !p.id) return fail(intent, 'validation', { values: p.echo })\n const saved = p.id\n ? await sf.customer.subjects.update(p.id, { attributes: p.attributes, ...(p.nickname !== undefined ? { nickname: p.nickname } : {}), ...(p.isDefault ? { is_default: true } : {}) } satisfies SubjectPatch, w)\n : await sf.customer.subjects.create({ kind: p.kind, attributes: p.attributes, ...(p.nickname !== undefined ? { nickname: p.nickname } : {}), ...(p.isDefault ? { is_default: true } : {}) } satisfies SubjectInput, w)\n return { ok: true, intent, subject: saved }\n }\n case 'subject-delete': {\n if (!p.id) return fail(intent, 'validation')\n await sf.customer.subjects.delete(p.id, w)\n return { ok: true, intent }\n }\n case 'subject-default': {\n if (!p.id) return fail(intent, 'validation')\n return { ok: true, intent, subject: await sf.customer.subjects.update(p.id, { is_default: true }, w) }\n }\n }\n } catch (err) {\n // Never log the error: a message can carry the field, and the request holds the buyer's items.\n return toFailure(err, intent, p.echo, p.id)\n }\n }\n}\n\nexport type SubjectAction = ReturnType<typeof createSubjectAction>\n"]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Subject, StorefrontClient, SubjectKind } from '@asteby/storefront';
|
|
2
|
+
import { b as SubjectIntent, S as SubjectActionData } from './fit-key-B9sYQVwG.cjs';
|
|
3
|
+
export { a as SubjectErrorCode, s as subjectFromFitKey } from './fit-key-B9sYQVwG.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server side of «My items» (`@asteby/storefront-react/subjects/server`): the guest's cookie, the cache rule, the loader
|
|
7
|
+
* (`loadSubjectsContext`, never throws) and the action (`createSubjectAction`). No React, no router: Web `Request` /
|
|
8
|
+
* `Response` only, so any framework mounts it. The browser never talks to OPS.
|
|
9
|
+
*
|
|
10
|
+
* Privacy: the cookie holds ONLY the compatibility key (`fit_key`), and `serializeSubjectCookie` refuses a key that does
|
|
11
|
+
* not resolve against the kind's catalog, so a licence plate or any private value can never be written to it. Nothing in
|
|
12
|
+
* this file logs a value, and a private value is never echoed back (`SubjectActionData.values` carries public ones only).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare const SUBJECT_COOKIE_PREFIX = "sf_subject_";
|
|
16
|
+
/** Cookie name of a kind: `sf_subject_<kind>`. */
|
|
17
|
+
declare function subjectCookieName(kind: Pick<SubjectKind, 'key'> | string): string;
|
|
18
|
+
/** The key the guest chose, validated against the kind's catalog; `null` when absent, tampered or stale. */
|
|
19
|
+
declare function readSubjectFitKey(cookieHeader: string | null | undefined, kind: SubjectKind): string | null;
|
|
20
|
+
type SubjectCookieOptions = {
|
|
21
|
+
/** Lifetime in days (default 365). */
|
|
22
|
+
maxAgeDays?: number;
|
|
23
|
+
path?: string;
|
|
24
|
+
domain?: string;
|
|
25
|
+
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
26
|
+
/** Defaults to true when `sameSite` is `None`; set it on https. */
|
|
27
|
+
secure?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* `Set-Cookie` value that remembers the guest's item: `HttpOnly`, `SameSite=Lax`, one year. Pass `null` to clear it.
|
|
31
|
+
* It throws `TypeError` for a key that does not resolve against the kind: only a real `fit_key` can reach the cookie.
|
|
32
|
+
*/
|
|
33
|
+
declare function serializeSubjectCookie(kind: SubjectKind, fitKey: string | null, options?: SubjectCookieOptions): string;
|
|
34
|
+
/** Is there a remembered item (of any kind) in the request? Then the page depends on the buyer. */
|
|
35
|
+
declare function hasActiveSubject(cookieHeader: string | null | undefined): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Cache headers of a page that changes with the buyer's item: with one remembered it is the buyer's (`private, no-store`);
|
|
38
|
+
* without, the store's public policy applies. Pass the store's own `Cache-Control` for the public case. Both answers carry
|
|
39
|
+
* `Vary: Cookie`: the public copy is only right for a request WITHOUT the cookie, and a shared cache (CDN) that stored it
|
|
40
|
+
* must not hand it to a guest who has one (their «only what fits my vehicle» listing would come back unfiltered).
|
|
41
|
+
*/
|
|
42
|
+
declare function subjectCacheHeaders(cookieHeader: string | null | undefined, publicCacheControl: string, opts?: {
|
|
43
|
+
signedIn?: boolean;
|
|
44
|
+
}): Record<string, string>;
|
|
45
|
+
type ActiveSubject = {
|
|
46
|
+
kind: string;
|
|
47
|
+
label: string;
|
|
48
|
+
fit_key: string;
|
|
49
|
+
/** `account`: the buyer's active saved item; `cookie`: what the guest remembered. */
|
|
50
|
+
source: 'account' | 'cookie';
|
|
51
|
+
/** The saved item (`account` only). */
|
|
52
|
+
subject?: Subject;
|
|
53
|
+
};
|
|
54
|
+
type SubjectsContext = {
|
|
55
|
+
kinds: SubjectKind[];
|
|
56
|
+
/** The buyer's saved items (empty for a guest or when they could not be read). */
|
|
57
|
+
subjects: Subject[];
|
|
58
|
+
/** Per kind key: the item that applies (the buyer's active one, else the guest's cookie). */
|
|
59
|
+
active: Record<string, ActiveSubject>;
|
|
60
|
+
/** Something could not be read (the rest is still true): show «We could not load this · Try again» for that part. */
|
|
61
|
+
degraded: {
|
|
62
|
+
kinds: boolean;
|
|
63
|
+
subjects: boolean;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/** What is assumed when nothing can be asked: no kinds, so the section simply does not exist. */
|
|
67
|
+
declare const NO_SUBJECTS: Readonly<SubjectsContext>;
|
|
68
|
+
type LoadSubjectsContextOptions = {
|
|
69
|
+
/** The buyer-scoped client (`withCustomerToken`); omit for a guest. */
|
|
70
|
+
customer?: StorefrontClient | null;
|
|
71
|
+
cookieHeader?: string | null;
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The kinds the store declares, the buyer's saved items and the one that applies to each kind. It NEVER throws: an
|
|
76
|
+
* organization that declares nothing (or a server that predates it) gives `kinds: []` and the section does not exist; a
|
|
77
|
+
* failure reading the items keeps the kinds and flags `degraded.subjects`. Use it as the loader of any page that
|
|
78
|
+
* lists parts or the buyer's garage.
|
|
79
|
+
*/
|
|
80
|
+
declare function loadSubjectsContext(sf: StorefrontClient, options?: LoadSubjectsContextOptions): Promise<SubjectsContext>;
|
|
81
|
+
type ParsedSubjectForm = {
|
|
82
|
+
intent: SubjectIntent | null;
|
|
83
|
+
id: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
/** Public and private values typed (empty ones left out; a private one flagged to clear is `null`). */
|
|
86
|
+
attributes: Record<string, string | null>;
|
|
87
|
+
/** Public values only, to refill the form. */
|
|
88
|
+
echo: Record<string, string>;
|
|
89
|
+
nickname: string | null | undefined;
|
|
90
|
+
isDefault: boolean;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Reads the form the components post: `intent`, `id`, `kind`, `<prefix>.<field>` for each field (`<prefix>.<field>.clear`
|
|
94
|
+
* to remove a stored private value), `nickname`, `is_default`. `privateKeys` are the kind's private fields: they are never
|
|
95
|
+
* part of `echo`.
|
|
96
|
+
*/
|
|
97
|
+
declare function parseSubjectForm(form: FormData, privateKeys?: readonly string[], prefix?: string): ParsedSubjectForm;
|
|
98
|
+
type CreateSubjectActionOptions = {
|
|
99
|
+
storefront: StorefrontClient;
|
|
100
|
+
/** The buyer's session token from the store's own cookie (`readCustomerToken`) or a cookie object with `parse`. */
|
|
101
|
+
readCustomerToken?: (request: Request) => string | null | undefined | Promise<string | null | undefined>;
|
|
102
|
+
customerCookie?: {
|
|
103
|
+
parse(header: string | null): unknown | Promise<unknown>;
|
|
104
|
+
};
|
|
105
|
+
clientIp?: (request: Request) => string | null | undefined;
|
|
106
|
+
/** Field-name prefix of the form (default `subject`). */
|
|
107
|
+
namePrefix?: string;
|
|
108
|
+
/** Refuse a cross-site POST (default true). */
|
|
109
|
+
sameSiteOnly?: boolean;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The action of the «My items» forms: one `POST` with an `intent` field.
|
|
113
|
+
*
|
|
114
|
+
* | intent | fields | does |
|
|
115
|
+
* | ----------------- | ------------------------------------------------------------ | --------------------------------------------- |
|
|
116
|
+
* | `subject-save` | kind, `subject.<field>`…, nickname?, is_default?, id? (edit) | creates, or updates when `id` is set |
|
|
117
|
+
* | `subject-delete` | id | `customer.subjects.delete` (physical) |
|
|
118
|
+
* | `subject-default` | id | makes it the active item of its kind |
|
|
119
|
+
* | `subject-refine` | `subject.<field>`… | saves nothing: echoes the values (no-JS cascade) |
|
|
120
|
+
*
|
|
121
|
+
* It never redirects: it returns a `SubjectActionData` for the page to render, with 422s mapped to the field
|
|
122
|
+
* (`fieldErrors.make`). An empty private field on edit keeps the stored value; `subject.<field>.clear` removes it.
|
|
123
|
+
* Nothing is logged and no private value is echoed.
|
|
124
|
+
*/
|
|
125
|
+
declare function createSubjectAction(options: CreateSubjectActionOptions): ({ request }: {
|
|
126
|
+
request: Request;
|
|
127
|
+
}) => Promise<SubjectActionData>;
|
|
128
|
+
type SubjectAction = ReturnType<typeof createSubjectAction>;
|
|
129
|
+
|
|
130
|
+
export { type ActiveSubject, type CreateSubjectActionOptions, type LoadSubjectsContextOptions, NO_SUBJECTS, type ParsedSubjectForm, SUBJECT_COOKIE_PREFIX, type SubjectAction, SubjectActionData, type SubjectCookieOptions, SubjectIntent, type SubjectsContext, createSubjectAction, hasActiveSubject, loadSubjectsContext, parseSubjectForm, readSubjectFitKey, serializeSubjectCookie, subjectCacheHeaders, subjectCookieName };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Subject, StorefrontClient, SubjectKind } from '@asteby/storefront';
|
|
2
|
+
import { b as SubjectIntent, S as SubjectActionData } from './fit-key-B9sYQVwG.js';
|
|
3
|
+
export { a as SubjectErrorCode, s as subjectFromFitKey } from './fit-key-B9sYQVwG.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Server side of «My items» (`@asteby/storefront-react/subjects/server`): the guest's cookie, the cache rule, the loader
|
|
7
|
+
* (`loadSubjectsContext`, never throws) and the action (`createSubjectAction`). No React, no router: Web `Request` /
|
|
8
|
+
* `Response` only, so any framework mounts it. The browser never talks to OPS.
|
|
9
|
+
*
|
|
10
|
+
* Privacy: the cookie holds ONLY the compatibility key (`fit_key`), and `serializeSubjectCookie` refuses a key that does
|
|
11
|
+
* not resolve against the kind's catalog, so a licence plate or any private value can never be written to it. Nothing in
|
|
12
|
+
* this file logs a value, and a private value is never echoed back (`SubjectActionData.values` carries public ones only).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
declare const SUBJECT_COOKIE_PREFIX = "sf_subject_";
|
|
16
|
+
/** Cookie name of a kind: `sf_subject_<kind>`. */
|
|
17
|
+
declare function subjectCookieName(kind: Pick<SubjectKind, 'key'> | string): string;
|
|
18
|
+
/** The key the guest chose, validated against the kind's catalog; `null` when absent, tampered or stale. */
|
|
19
|
+
declare function readSubjectFitKey(cookieHeader: string | null | undefined, kind: SubjectKind): string | null;
|
|
20
|
+
type SubjectCookieOptions = {
|
|
21
|
+
/** Lifetime in days (default 365). */
|
|
22
|
+
maxAgeDays?: number;
|
|
23
|
+
path?: string;
|
|
24
|
+
domain?: string;
|
|
25
|
+
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
26
|
+
/** Defaults to true when `sameSite` is `None`; set it on https. */
|
|
27
|
+
secure?: boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* `Set-Cookie` value that remembers the guest's item: `HttpOnly`, `SameSite=Lax`, one year. Pass `null` to clear it.
|
|
31
|
+
* It throws `TypeError` for a key that does not resolve against the kind: only a real `fit_key` can reach the cookie.
|
|
32
|
+
*/
|
|
33
|
+
declare function serializeSubjectCookie(kind: SubjectKind, fitKey: string | null, options?: SubjectCookieOptions): string;
|
|
34
|
+
/** Is there a remembered item (of any kind) in the request? Then the page depends on the buyer. */
|
|
35
|
+
declare function hasActiveSubject(cookieHeader: string | null | undefined): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Cache headers of a page that changes with the buyer's item: with one remembered it is the buyer's (`private, no-store`);
|
|
38
|
+
* without, the store's public policy applies. Pass the store's own `Cache-Control` for the public case. Both answers carry
|
|
39
|
+
* `Vary: Cookie`: the public copy is only right for a request WITHOUT the cookie, and a shared cache (CDN) that stored it
|
|
40
|
+
* must not hand it to a guest who has one (their «only what fits my vehicle» listing would come back unfiltered).
|
|
41
|
+
*/
|
|
42
|
+
declare function subjectCacheHeaders(cookieHeader: string | null | undefined, publicCacheControl: string, opts?: {
|
|
43
|
+
signedIn?: boolean;
|
|
44
|
+
}): Record<string, string>;
|
|
45
|
+
type ActiveSubject = {
|
|
46
|
+
kind: string;
|
|
47
|
+
label: string;
|
|
48
|
+
fit_key: string;
|
|
49
|
+
/** `account`: the buyer's active saved item; `cookie`: what the guest remembered. */
|
|
50
|
+
source: 'account' | 'cookie';
|
|
51
|
+
/** The saved item (`account` only). */
|
|
52
|
+
subject?: Subject;
|
|
53
|
+
};
|
|
54
|
+
type SubjectsContext = {
|
|
55
|
+
kinds: SubjectKind[];
|
|
56
|
+
/** The buyer's saved items (empty for a guest or when they could not be read). */
|
|
57
|
+
subjects: Subject[];
|
|
58
|
+
/** Per kind key: the item that applies (the buyer's active one, else the guest's cookie). */
|
|
59
|
+
active: Record<string, ActiveSubject>;
|
|
60
|
+
/** Something could not be read (the rest is still true): show «We could not load this · Try again» for that part. */
|
|
61
|
+
degraded: {
|
|
62
|
+
kinds: boolean;
|
|
63
|
+
subjects: boolean;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/** What is assumed when nothing can be asked: no kinds, so the section simply does not exist. */
|
|
67
|
+
declare const NO_SUBJECTS: Readonly<SubjectsContext>;
|
|
68
|
+
type LoadSubjectsContextOptions = {
|
|
69
|
+
/** The buyer-scoped client (`withCustomerToken`); omit for a guest. */
|
|
70
|
+
customer?: StorefrontClient | null;
|
|
71
|
+
cookieHeader?: string | null;
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The kinds the store declares, the buyer's saved items and the one that applies to each kind. It NEVER throws: an
|
|
76
|
+
* organization that declares nothing (or a server that predates it) gives `kinds: []` and the section does not exist; a
|
|
77
|
+
* failure reading the items keeps the kinds and flags `degraded.subjects`. Use it as the loader of any page that
|
|
78
|
+
* lists parts or the buyer's garage.
|
|
79
|
+
*/
|
|
80
|
+
declare function loadSubjectsContext(sf: StorefrontClient, options?: LoadSubjectsContextOptions): Promise<SubjectsContext>;
|
|
81
|
+
type ParsedSubjectForm = {
|
|
82
|
+
intent: SubjectIntent | null;
|
|
83
|
+
id: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
/** Public and private values typed (empty ones left out; a private one flagged to clear is `null`). */
|
|
86
|
+
attributes: Record<string, string | null>;
|
|
87
|
+
/** Public values only, to refill the form. */
|
|
88
|
+
echo: Record<string, string>;
|
|
89
|
+
nickname: string | null | undefined;
|
|
90
|
+
isDefault: boolean;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Reads the form the components post: `intent`, `id`, `kind`, `<prefix>.<field>` for each field (`<prefix>.<field>.clear`
|
|
94
|
+
* to remove a stored private value), `nickname`, `is_default`. `privateKeys` are the kind's private fields: they are never
|
|
95
|
+
* part of `echo`.
|
|
96
|
+
*/
|
|
97
|
+
declare function parseSubjectForm(form: FormData, privateKeys?: readonly string[], prefix?: string): ParsedSubjectForm;
|
|
98
|
+
type CreateSubjectActionOptions = {
|
|
99
|
+
storefront: StorefrontClient;
|
|
100
|
+
/** The buyer's session token from the store's own cookie (`readCustomerToken`) or a cookie object with `parse`. */
|
|
101
|
+
readCustomerToken?: (request: Request) => string | null | undefined | Promise<string | null | undefined>;
|
|
102
|
+
customerCookie?: {
|
|
103
|
+
parse(header: string | null): unknown | Promise<unknown>;
|
|
104
|
+
};
|
|
105
|
+
clientIp?: (request: Request) => string | null | undefined;
|
|
106
|
+
/** Field-name prefix of the form (default `subject`). */
|
|
107
|
+
namePrefix?: string;
|
|
108
|
+
/** Refuse a cross-site POST (default true). */
|
|
109
|
+
sameSiteOnly?: boolean;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The action of the «My items» forms: one `POST` with an `intent` field.
|
|
113
|
+
*
|
|
114
|
+
* | intent | fields | does |
|
|
115
|
+
* | ----------------- | ------------------------------------------------------------ | --------------------------------------------- |
|
|
116
|
+
* | `subject-save` | kind, `subject.<field>`…, nickname?, is_default?, id? (edit) | creates, or updates when `id` is set |
|
|
117
|
+
* | `subject-delete` | id | `customer.subjects.delete` (physical) |
|
|
118
|
+
* | `subject-default` | id | makes it the active item of its kind |
|
|
119
|
+
* | `subject-refine` | `subject.<field>`… | saves nothing: echoes the values (no-JS cascade) |
|
|
120
|
+
*
|
|
121
|
+
* It never redirects: it returns a `SubjectActionData` for the page to render, with 422s mapped to the field
|
|
122
|
+
* (`fieldErrors.make`). An empty private field on edit keeps the stored value; `subject.<field>.clear` removes it.
|
|
123
|
+
* Nothing is logged and no private value is echoed.
|
|
124
|
+
*/
|
|
125
|
+
declare function createSubjectAction(options: CreateSubjectActionOptions): ({ request }: {
|
|
126
|
+
request: Request;
|
|
127
|
+
}) => Promise<SubjectActionData>;
|
|
128
|
+
type SubjectAction = ReturnType<typeof createSubjectAction>;
|
|
129
|
+
|
|
130
|
+
export { type ActiveSubject, type CreateSubjectActionOptions, type LoadSubjectsContextOptions, NO_SUBJECTS, type ParsedSubjectForm, SUBJECT_COOKIE_PREFIX, type SubjectAction, SubjectActionData, type SubjectCookieOptions, SubjectIntent, type SubjectsContext, createSubjectAction, hasActiveSubject, loadSubjectsContext, parseSubjectForm, readSubjectFitKey, serializeSubjectCookie, subjectCacheHeaders, subjectCookieName };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { subjectFromFitKey } from './chunk-Z4C4LKXF.js';
|
|
2
|
+
export { subjectFromFitKey } from './chunk-Z4C4LKXF.js';
|
|
3
|
+
import { isStorefrontError } from '@asteby/storefront';
|
|
4
|
+
|
|
5
|
+
var SUBJECT_COOKIE_PREFIX = "sf_subject_";
|
|
6
|
+
var KIND_KEY_RE = /^[a-z][a-z0-9_]{0,31}$/;
|
|
7
|
+
function subjectCookieName(kind) {
|
|
8
|
+
const key = typeof kind === "string" ? kind : kind.key;
|
|
9
|
+
if (!KIND_KEY_RE.test(key)) throw new TypeError("subjectCookieName: not a kind key");
|
|
10
|
+
return SUBJECT_COOKIE_PREFIX + key;
|
|
11
|
+
}
|
|
12
|
+
function readSubjectFitKey(cookieHeader, kind) {
|
|
13
|
+
if (!cookieHeader) return null;
|
|
14
|
+
const name = subjectCookieName(kind);
|
|
15
|
+
for (const part of cookieHeader.split(";")) {
|
|
16
|
+
const eq = part.indexOf("=");
|
|
17
|
+
if (eq < 0 || part.slice(0, eq).trim() !== name) continue;
|
|
18
|
+
let raw = part.slice(eq + 1).trim().replace(/^"(.*)"$/, "$1");
|
|
19
|
+
try {
|
|
20
|
+
raw = decodeURIComponent(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return subjectFromFitKey(kind, raw) ? raw : null;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function serializeSubjectCookie(kind, fitKey, options = {}) {
|
|
29
|
+
if (fitKey !== null && !subjectFromFitKey(kind, fitKey)) throw new TypeError("serializeSubjectCookie: not a compatibility key of this kind");
|
|
30
|
+
const parts = [`${subjectCookieName(kind)}=${fitKey ? encodeURIComponent(fitKey) : ""}`, `Path=${options.path ?? "/"}`];
|
|
31
|
+
parts.push(`Max-Age=${fitKey ? Math.round((options.maxAgeDays ?? 365) * 86400) : 0}`);
|
|
32
|
+
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
33
|
+
const sameSite2 = options.sameSite ?? "Lax";
|
|
34
|
+
parts.push(`SameSite=${sameSite2}`, "HttpOnly");
|
|
35
|
+
if (options.secure ?? sameSite2 === "None") parts.push("Secure");
|
|
36
|
+
return parts.join("; ");
|
|
37
|
+
}
|
|
38
|
+
function hasActiveSubject(cookieHeader) {
|
|
39
|
+
return !!cookieHeader && cookieHeader.split(";").some((p) => p.trim().startsWith(SUBJECT_COOKIE_PREFIX) && p.indexOf("=") > 0 && !/=\s*$/.test(p));
|
|
40
|
+
}
|
|
41
|
+
function subjectCacheHeaders(cookieHeader, publicCacheControl, opts = {}) {
|
|
42
|
+
if (opts.signedIn || hasActiveSubject(cookieHeader)) return { "Cache-Control": "private, no-store", Vary: "Cookie" };
|
|
43
|
+
return { "Cache-Control": publicCacheControl, Vary: "Cookie" };
|
|
44
|
+
}
|
|
45
|
+
var NO_SUBJECTS = Object.freeze({ kinds: [], subjects: [], active: {}, degraded: { kinds: true, subjects: false } });
|
|
46
|
+
async function loadSubjectsContext(sf, options = {}) {
|
|
47
|
+
const opt = options.signal ? { signal: options.signal } : void 0;
|
|
48
|
+
let kinds;
|
|
49
|
+
try {
|
|
50
|
+
kinds = await sf.content.subjectKinds(opt);
|
|
51
|
+
} catch {
|
|
52
|
+
return NO_SUBJECTS;
|
|
53
|
+
}
|
|
54
|
+
if (kinds.length === 0) return { kinds: [], subjects: [], active: {}, degraded: { kinds: false, subjects: false } };
|
|
55
|
+
let subjects = [];
|
|
56
|
+
let degraded = false;
|
|
57
|
+
if (options.customer) {
|
|
58
|
+
try {
|
|
59
|
+
subjects = await options.customer.customer.subjects.list(void 0, opt);
|
|
60
|
+
} catch {
|
|
61
|
+
degraded = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const active = {};
|
|
65
|
+
for (const kind of kinds) {
|
|
66
|
+
const mine = subjects.filter((s) => s.kind === kind.key);
|
|
67
|
+
const chosen = mine.find((s) => s.is_default) ?? mine[0];
|
|
68
|
+
if (chosen) {
|
|
69
|
+
active[kind.key] = { kind: kind.key, label: chosen.label, fit_key: chosen.fit_key, source: "account", subject: chosen };
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const key = readSubjectFitKey(options.cookieHeader, kind);
|
|
73
|
+
const guest = key ? subjectFromFitKey(kind, key) : null;
|
|
74
|
+
if (key && guest) active[kind.key] = { kind: kind.key, label: guest.label, fit_key: key, source: "cookie" };
|
|
75
|
+
}
|
|
76
|
+
return { kinds, subjects, active, degraded: { kinds: false, subjects: degraded } };
|
|
77
|
+
}
|
|
78
|
+
var text = (v) => typeof v === "string" ? v.trim() : "";
|
|
79
|
+
function parseSubjectForm(form, privateKeys = [], prefix = "subject") {
|
|
80
|
+
const attributes = {};
|
|
81
|
+
const echo = {};
|
|
82
|
+
const head = `${prefix}.`;
|
|
83
|
+
for (const [name, value] of form.entries()) {
|
|
84
|
+
if (!name.startsWith(head) || name.endsWith(".clear")) continue;
|
|
85
|
+
const key = name.slice(head.length);
|
|
86
|
+
const v = text(value);
|
|
87
|
+
if (v) {
|
|
88
|
+
attributes[key] = v;
|
|
89
|
+
if (!privateKeys.includes(key)) echo[key] = v;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const key of privateKeys) if (form.get(`${head}${key}.clear`)) attributes[key] = null;
|
|
93
|
+
const intents = form.getAll("intent").map(text);
|
|
94
|
+
const intent = intents.includes("subject-refine") ? "subject-refine" : intents[0] ?? "";
|
|
95
|
+
const nick = form.has("nickname") ? text(form.get("nickname")) : void 0;
|
|
96
|
+
return {
|
|
97
|
+
intent: intent === "subject-save" || intent === "subject-delete" || intent === "subject-default" || intent === "subject-refine" ? intent : null,
|
|
98
|
+
id: text(form.get("id")),
|
|
99
|
+
kind: text(form.get("kind")),
|
|
100
|
+
attributes,
|
|
101
|
+
echo,
|
|
102
|
+
nickname: nick === void 0 ? void 0 : nick || null,
|
|
103
|
+
isDefault: !!form.get("is_default")
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
var fail = (intent, code, extra = {}) => ({
|
|
107
|
+
ok: false,
|
|
108
|
+
...intent ? { intent } : {},
|
|
109
|
+
...extra,
|
|
110
|
+
error: { code, ...extra.error?.field ? { field: extra.error.field } : {} }
|
|
111
|
+
});
|
|
112
|
+
var fieldOf = (field) => field ? field.replace(/^attributes\./, "") : void 0;
|
|
113
|
+
function problemOf(message) {
|
|
114
|
+
const m = (message ?? "").toLowerCase();
|
|
115
|
+
const coded = /:\s*(required|unknown|invalid|not_in_catalog|year_range|catalog_unavailable)\b/.exec(m)?.[1];
|
|
116
|
+
if (coded) return coded;
|
|
117
|
+
if (/:\s*required\b/.test(m)) return "required";
|
|
118
|
+
if (m.includes("unknown field")) return "unknown";
|
|
119
|
+
if (m.includes("not in the catalog")) return "not_in_catalog";
|
|
120
|
+
if (m.includes("outside the years")) return "year_range";
|
|
121
|
+
if (m.includes("catalog is not available")) return "catalog_unavailable";
|
|
122
|
+
return "invalid";
|
|
123
|
+
}
|
|
124
|
+
function toFailure(err, intent, values, id) {
|
|
125
|
+
const carry = { values, ...id ? { id } : {} };
|
|
126
|
+
if (!isStorefrontError(err)) return fail(intent, "unavailable", carry);
|
|
127
|
+
if (err.code === "insufficient_scope") return fail(intent, "store_misconfigured", carry);
|
|
128
|
+
switch (err.status) {
|
|
129
|
+
case 401:
|
|
130
|
+
return fail(intent, "unauthorized", carry);
|
|
131
|
+
case 404:
|
|
132
|
+
return fail(intent, "not_found", carry);
|
|
133
|
+
case 409:
|
|
134
|
+
return fail(intent, err.field === "kind" ? "limit" : "duplicate", carry);
|
|
135
|
+
case 429:
|
|
136
|
+
return fail(intent, "rate_limited", carry);
|
|
137
|
+
case 403:
|
|
138
|
+
return fail(intent, "unsupported", carry);
|
|
139
|
+
case 400:
|
|
140
|
+
case 422: {
|
|
141
|
+
const field = fieldOf(err.field);
|
|
142
|
+
const why = problemOf(err.message);
|
|
143
|
+
return fail(intent, err.field === "kind" ? "unsupported" : "validation", {
|
|
144
|
+
...carry,
|
|
145
|
+
...field ? { fieldErrors: { [field]: why }, error: { code: "validation", field } } : {}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
return fail(intent, "unavailable", carry);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function sameSite(request) {
|
|
153
|
+
const site = request.headers.get("sec-fetch-site");
|
|
154
|
+
if (site && site !== "same-origin" && site !== "same-site" && site !== "none") return false;
|
|
155
|
+
const origin = request.headers.get("origin");
|
|
156
|
+
if (!origin) return true;
|
|
157
|
+
try {
|
|
158
|
+
const host = request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? new URL(request.url).host;
|
|
159
|
+
return new URL(origin).host === host.split(",")[0].trim();
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function createSubjectAction(options) {
|
|
165
|
+
const { storefront: sf } = options;
|
|
166
|
+
const prefix = options.namePrefix ?? "subject";
|
|
167
|
+
async function token(request) {
|
|
168
|
+
if (options.readCustomerToken) return await options.readCustomerToken(request) || null;
|
|
169
|
+
if (!options.customerCookie) return null;
|
|
170
|
+
const v = await options.customerCookie.parse(request.headers.get("cookie"));
|
|
171
|
+
return typeof v === "string" && v ? v : null;
|
|
172
|
+
}
|
|
173
|
+
return async function subjectAction({ request }) {
|
|
174
|
+
if (options.sameSiteOnly !== false && !sameSite(request)) return fail(void 0, "unsupported");
|
|
175
|
+
const form = await request.formData().catch(() => null);
|
|
176
|
+
if (!form) return fail(void 0, "validation");
|
|
177
|
+
const tok = await token(request);
|
|
178
|
+
const ip = options.clientIp?.(request);
|
|
179
|
+
const w = { ...ip ? { clientIp: ip } : {}, ...tok ? { customerToken: tok } : {} };
|
|
180
|
+
const rawKind = text(form.get("kind"));
|
|
181
|
+
let privateKeys = [];
|
|
182
|
+
try {
|
|
183
|
+
const kinds = await sf.content.subjectKinds();
|
|
184
|
+
privateKeys = kinds.find((k) => k.key === rawKind)?.fields.filter((f) => f.private).map((f) => f.key) ?? [];
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
const p = parseSubjectForm(form, privateKeys, prefix);
|
|
188
|
+
const intent = p.intent;
|
|
189
|
+
if (!intent) return fail(void 0, "validation");
|
|
190
|
+
if (intent === "subject-refine") return { ok: false, intent, values: p.echo, ...p.id ? { id: p.id } : {} };
|
|
191
|
+
if (!tok) return fail(intent, "unauthorized", { values: p.echo, ...p.id ? { id: p.id } : {} });
|
|
192
|
+
try {
|
|
193
|
+
switch (intent) {
|
|
194
|
+
case "subject-save": {
|
|
195
|
+
if (!p.kind && !p.id) return fail(intent, "validation", { values: p.echo });
|
|
196
|
+
const saved = p.id ? await sf.customer.subjects.update(p.id, { attributes: p.attributes, ...p.nickname !== void 0 ? { nickname: p.nickname } : {}, ...p.isDefault ? { is_default: true } : {} }, w) : await sf.customer.subjects.create({ kind: p.kind, attributes: p.attributes, ...p.nickname !== void 0 ? { nickname: p.nickname } : {}, ...p.isDefault ? { is_default: true } : {} }, w);
|
|
197
|
+
return { ok: true, intent, subject: saved };
|
|
198
|
+
}
|
|
199
|
+
case "subject-delete": {
|
|
200
|
+
if (!p.id) return fail(intent, "validation");
|
|
201
|
+
await sf.customer.subjects.delete(p.id, w);
|
|
202
|
+
return { ok: true, intent };
|
|
203
|
+
}
|
|
204
|
+
case "subject-default": {
|
|
205
|
+
if (!p.id) return fail(intent, "validation");
|
|
206
|
+
return { ok: true, intent, subject: await sf.customer.subjects.update(p.id, { is_default: true }, w) };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return toFailure(err, intent, p.echo, p.id);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export { NO_SUBJECTS, SUBJECT_COOKIE_PREFIX, createSubjectAction, hasActiveSubject, loadSubjectsContext, parseSubjectForm, readSubjectFitKey, serializeSubjectCookie, subjectCacheHeaders, subjectCookieName };
|
|
216
|
+
//# sourceMappingURL=subjects-server.js.map
|
|
217
|
+
//# sourceMappingURL=subjects-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/subjects/server.ts"],"names":["sameSite"],"mappings":"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","file":"subjects-server.js","sourcesContent":["/**\n * Server side of «My items» (`@asteby/storefront-react/subjects/server`): the guest's cookie, the cache rule, the loader\n * (`loadSubjectsContext`, never throws) and the action (`createSubjectAction`). No React, no router: Web `Request` /\n * `Response` only, so any framework mounts it. The browser never talks to OPS.\n *\n * Privacy: the cookie holds ONLY the compatibility key (`fit_key`), and `serializeSubjectCookie` refuses a key that does\n * not resolve against the kind's catalog, so a licence plate or any private value can never be written to it. Nothing in\n * this file logs a value, and a private value is never echoed back (`SubjectActionData.values` carries public ones only).\n */\nimport {\n isStorefrontError,\n type StorefrontClient,\n type Subject,\n type SubjectInput,\n type SubjectKind,\n type SubjectPatch,\n type SubjectProblemCode,\n} from '@asteby/storefront'\nimport { subjectFromFitKey } from './fit-key.js'\nimport type { SubjectActionData, SubjectErrorCode, SubjectIntent } from './types.js'\n\nexport type { SubjectActionData, SubjectErrorCode, SubjectIntent } from './types.js'\nexport { subjectFromFitKey } from './fit-key.js'\n\n// ---- the guest's cookie ------------------------------------------------------------------------------------------------\n\nexport const SUBJECT_COOKIE_PREFIX = 'sf_subject_'\nconst KIND_KEY_RE = /^[a-z][a-z0-9_]{0,31}$/\n\n/** Cookie name of a kind: `sf_subject_<kind>`. */\nexport function subjectCookieName(kind: Pick<SubjectKind, 'key'> | string): string {\n const key = typeof kind === 'string' ? kind : kind.key\n if (!KIND_KEY_RE.test(key)) throw new TypeError('subjectCookieName: not a kind key')\n return SUBJECT_COOKIE_PREFIX + key\n}\n\n/** The key the guest chose, validated against the kind's catalog; `null` when absent, tampered or stale. */\nexport function readSubjectFitKey(cookieHeader: string | null | undefined, kind: SubjectKind): string | null {\n if (!cookieHeader) return null\n const name = subjectCookieName(kind)\n for (const part of cookieHeader.split(';')) {\n const eq = part.indexOf('=')\n if (eq < 0 || part.slice(0, eq).trim() !== name) continue\n let raw = part.slice(eq + 1).trim().replace(/^\"(.*)\"$/, '$1')\n try {\n raw = decodeURIComponent(raw)\n } catch {\n return null\n }\n return subjectFromFitKey(kind, raw) ? raw : null\n }\n return null\n}\n\nexport type SubjectCookieOptions = {\n /** Lifetime in days (default 365). */\n maxAgeDays?: number\n path?: string\n domain?: string\n sameSite?: 'Lax' | 'Strict' | 'None'\n /** Defaults to true when `sameSite` is `None`; set it on https. */\n secure?: boolean\n}\n\n/**\n * `Set-Cookie` value that remembers the guest's item: `HttpOnly`, `SameSite=Lax`, one year. Pass `null` to clear it.\n * It throws `TypeError` for a key that does not resolve against the kind: only a real `fit_key` can reach the cookie.\n */\nexport function serializeSubjectCookie(kind: SubjectKind, fitKey: string | null, options: SubjectCookieOptions = {}): string {\n if (fitKey !== null && !subjectFromFitKey(kind, fitKey)) throw new TypeError('serializeSubjectCookie: not a compatibility key of this kind')\n const parts = [`${subjectCookieName(kind)}=${fitKey ? encodeURIComponent(fitKey) : ''}`, `Path=${options.path ?? '/'}`]\n parts.push(`Max-Age=${fitKey ? Math.round((options.maxAgeDays ?? 365) * 86_400) : 0}`)\n if (options.domain) parts.push(`Domain=${options.domain}`)\n const sameSite = options.sameSite ?? 'Lax'\n parts.push(`SameSite=${sameSite}`, 'HttpOnly')\n if (options.secure ?? sameSite === 'None') parts.push('Secure')\n return parts.join('; ')\n}\n\n// ---- cache --------------------------------------------------------------------------------------------------------------\n\n/** Is there a remembered item (of any kind) in the request? Then the page depends on the buyer. */\nexport function hasActiveSubject(cookieHeader: string | null | undefined): boolean {\n return !!cookieHeader && cookieHeader.split(';').some((p) => p.trim().startsWith(SUBJECT_COOKIE_PREFIX) && p.indexOf('=') > 0 && !/=\\s*$/.test(p))\n}\n\n/**\n * Cache headers of a page that changes with the buyer's item: with one remembered it is the buyer's (`private, no-store`);\n * without, the store's public policy applies. Pass the store's own `Cache-Control` for the public case. Both answers carry\n * `Vary: Cookie`: the public copy is only right for a request WITHOUT the cookie, and a shared cache (CDN) that stored it\n * must not hand it to a guest who has one (their «only what fits my vehicle» listing would come back unfiltered).\n */\nexport function subjectCacheHeaders(cookieHeader: string | null | undefined, publicCacheControl: string, opts: { signedIn?: boolean } = {}): Record<string, string> {\n if (opts.signedIn || hasActiveSubject(cookieHeader)) return { 'Cache-Control': 'private, no-store', Vary: 'Cookie' }\n return { 'Cache-Control': publicCacheControl, Vary: 'Cookie' }\n}\n\n// ---- loader -------------------------------------------------------------------------------------------------------------\n\nexport type ActiveSubject = {\n kind: string\n label: string\n fit_key: string\n /** `account`: the buyer's active saved item; `cookie`: what the guest remembered. */\n source: 'account' | 'cookie'\n /** The saved item (`account` only). */\n subject?: Subject\n}\n\nexport type SubjectsContext = {\n kinds: SubjectKind[]\n /** The buyer's saved items (empty for a guest or when they could not be read). */\n subjects: Subject[]\n /** Per kind key: the item that applies (the buyer's active one, else the guest's cookie). */\n active: Record<string, ActiveSubject>\n /** Something could not be read (the rest is still true): show «We could not load this · Try again» for that part. */\n degraded: { kinds: boolean; subjects: boolean }\n}\n\n/** What is assumed when nothing can be asked: no kinds, so the section simply does not exist. */\nexport const NO_SUBJECTS: Readonly<SubjectsContext> = Object.freeze({ kinds: [], subjects: [], active: {}, degraded: { kinds: true, subjects: false } })\n\nexport type LoadSubjectsContextOptions = {\n /** The buyer-scoped client (`withCustomerToken`); omit for a guest. */\n customer?: StorefrontClient | null\n cookieHeader?: string | null\n signal?: AbortSignal\n}\n\n/**\n * The kinds the store declares, the buyer's saved items and the one that applies to each kind. It NEVER throws: an\n * organization that declares nothing (or a server that predates it) gives `kinds: []` and the section does not exist; a\n * failure reading the items keeps the kinds and flags `degraded.subjects`. Use it as the loader of any page that\n * lists parts or the buyer's garage.\n */\nexport async function loadSubjectsContext(sf: StorefrontClient, options: LoadSubjectsContextOptions = {}): Promise<SubjectsContext> {\n const opt = options.signal ? { signal: options.signal } : undefined\n let kinds: SubjectKind[]\n try {\n kinds = await sf.content.subjectKinds(opt)\n } catch {\n return NO_SUBJECTS\n }\n if (kinds.length === 0) return { kinds: [], subjects: [], active: {}, degraded: { kinds: false, subjects: false } }\n let subjects: Subject[] = []\n let degraded = false\n if (options.customer) {\n try {\n subjects = await options.customer.customer.subjects.list(undefined, opt)\n } catch {\n degraded = true\n }\n }\n const active: Record<string, ActiveSubject> = {}\n for (const kind of kinds) {\n const mine = subjects.filter((s) => s.kind === kind.key)\n const chosen = mine.find((s) => s.is_default) ?? mine[0]\n if (chosen) {\n active[kind.key] = { kind: kind.key, label: chosen.label, fit_key: chosen.fit_key, source: 'account', subject: chosen }\n continue\n }\n const key = readSubjectFitKey(options.cookieHeader, kind)\n const guest = key ? subjectFromFitKey(kind, key) : null\n if (key && guest) active[kind.key] = { kind: kind.key, label: guest.label, fit_key: key, source: 'cookie' }\n }\n return { kinds, subjects, active, degraded: { kinds: false, subjects: degraded } }\n}\n\n// ---- action -------------------------------------------------------------------------------------------------------------\n\nexport type ParsedSubjectForm = {\n intent: SubjectIntent | null\n id: string\n kind: string\n /** Public and private values typed (empty ones left out; a private one flagged to clear is `null`). */\n attributes: Record<string, string | null>\n /** Public values only, to refill the form. */\n echo: Record<string, string>\n nickname: string | null | undefined\n isDefault: boolean\n}\n\nconst text = (v: FormDataEntryValue | null): string => (typeof v === 'string' ? v.trim() : '')\n\n/**\n * Reads the form the components post: `intent`, `id`, `kind`, `<prefix>.<field>` for each field (`<prefix>.<field>.clear`\n * to remove a stored private value), `nickname`, `is_default`. `privateKeys` are the kind's private fields: they are never\n * part of `echo`.\n */\nexport function parseSubjectForm(form: FormData, privateKeys: readonly string[] = [], prefix = 'subject'): ParsedSubjectForm {\n const attributes: Record<string, string | null> = {}\n const echo: Record<string, string> = {}\n const head = `${prefix}.`\n for (const [name, value] of form.entries()) {\n if (!name.startsWith(head) || name.endsWith('.clear')) continue\n const key = name.slice(head.length)\n const v = text(value)\n if (v) {\n attributes[key] = v\n if (!privateKeys.includes(key)) echo[key] = v\n }\n }\n for (const key of privateKeys) if (form.get(`${head}${key}.clear`)) attributes[key] = null\n // The picker posts a hidden `intent=subject-save` AND, without JavaScript, the «Continue» button's own\n // `intent=subject-refine` (the submitter's entry comes after it): refine wins, so «Continue» never saves.\n const intents = form.getAll('intent').map(text)\n const intent = intents.includes('subject-refine') ? 'subject-refine' : (intents[0] ?? '')\n const nick = form.has('nickname') ? text(form.get('nickname')) : undefined\n return {\n intent: intent === 'subject-save' || intent === 'subject-delete' || intent === 'subject-default' || intent === 'subject-refine' ? intent : null,\n id: text(form.get('id')),\n kind: text(form.get('kind')),\n attributes,\n echo,\n nickname: nick === undefined ? undefined : nick || null,\n isDefault: !!form.get('is_default'),\n }\n}\n\nconst fail = (intent: SubjectIntent | undefined, code: SubjectErrorCode, extra: Partial<SubjectActionData> = {}): SubjectActionData => ({\n ok: false,\n ...(intent ? { intent } : {}),\n ...extra,\n error: { code, ...(extra.error?.field ? { field: extra.error.field } : {}) },\n})\n\n/** `attributes.make` → `make`. */\nconst fieldOf = (field: string | undefined): string | undefined => (field ? field.replace(/^attributes\\./, '') : undefined)\n\n/**\n * Why a field was refused, from the server's message. The real server writes «attributes.make: is not in the catalog»;\n * the mock writes «make: not_in_catalog». Both map to one code; the value is never part of either message.\n */\nfunction problemOf(message: string | undefined): SubjectProblemCode | 'invalid' {\n const m = (message ?? '').toLowerCase()\n const coded = /:\\s*(required|unknown|invalid|not_in_catalog|year_range|catalog_unavailable)\\b/.exec(m)?.[1]\n if (coded) return coded as SubjectProblemCode\n if (/:\\s*required\\b/.test(m)) return 'required'\n if (m.includes('unknown field')) return 'unknown'\n if (m.includes('not in the catalog')) return 'not_in_catalog'\n if (m.includes('outside the years')) return 'year_range'\n if (m.includes('catalog is not available')) return 'catalog_unavailable'\n return 'invalid'\n}\n\nfunction toFailure(err: unknown, intent: SubjectIntent, values: Record<string, string>, id?: string): SubjectActionData {\n const carry = { values, ...(id ? { id } : {}) }\n if (!isStorefrontError(err)) return fail(intent, 'unavailable', carry)\n if (err.code === 'insufficient_scope') return fail(intent, 'store_misconfigured', carry)\n switch (err.status) {\n case 401:\n return fail(intent, 'unauthorized', carry)\n case 404:\n return fail(intent, 'not_found', carry)\n case 409:\n return fail(intent, err.field === 'kind' ? 'limit' : 'duplicate', carry)\n case 429:\n return fail(intent, 'rate_limited', carry)\n case 403:\n return fail(intent, 'unsupported', carry)\n case 400:\n case 422: {\n const field = fieldOf(err.field)\n const why = problemOf(err.message)\n return fail(intent, err.field === 'kind' ? 'unsupported' : 'validation', {\n ...carry,\n ...(field ? { fieldErrors: { [field]: why }, error: { code: 'validation', field } } : {}),\n })\n }\n default:\n return fail(intent, 'unavailable', carry)\n }\n}\n\nfunction sameSite(request: Request): boolean {\n const site = request.headers.get('sec-fetch-site')\n if (site && site !== 'same-origin' && site !== 'same-site' && site !== 'none') return false\n const origin = request.headers.get('origin')\n if (!origin) return true\n try {\n const host = request.headers.get('x-forwarded-host') ?? request.headers.get('host') ?? new URL(request.url).host\n return new URL(origin).host === host.split(',')[0]!.trim()\n } catch {\n return false\n }\n}\n\nexport type CreateSubjectActionOptions = {\n storefront: StorefrontClient\n /** The buyer's session token from the store's own cookie (`readCustomerToken`) or a cookie object with `parse`. */\n readCustomerToken?: (request: Request) => string | null | undefined | Promise<string | null | undefined>\n customerCookie?: { parse(header: string | null): unknown | Promise<unknown> }\n clientIp?: (request: Request) => string | null | undefined\n /** Field-name prefix of the form (default `subject`). */\n namePrefix?: string\n /** Refuse a cross-site POST (default true). */\n sameSiteOnly?: boolean\n}\n\n/**\n * The action of the «My items» forms: one `POST` with an `intent` field.\n *\n * | intent | fields | does |\n * | ----------------- | ------------------------------------------------------------ | --------------------------------------------- |\n * | `subject-save` | kind, `subject.<field>`…, nickname?, is_default?, id? (edit) | creates, or updates when `id` is set |\n * | `subject-delete` | id | `customer.subjects.delete` (physical) |\n * | `subject-default` | id | makes it the active item of its kind |\n * | `subject-refine` | `subject.<field>`… | saves nothing: echoes the values (no-JS cascade) |\n *\n * It never redirects: it returns a `SubjectActionData` for the page to render, with 422s mapped to the field\n * (`fieldErrors.make`). An empty private field on edit keeps the stored value; `subject.<field>.clear` removes it.\n * Nothing is logged and no private value is echoed.\n */\nexport function createSubjectAction(options: CreateSubjectActionOptions) {\n const { storefront: sf } = options\n const prefix = options.namePrefix ?? 'subject'\n async function token(request: Request): Promise<string | null> {\n if (options.readCustomerToken) return (await options.readCustomerToken(request)) || null\n if (!options.customerCookie) return null\n const v = await options.customerCookie.parse(request.headers.get('cookie'))\n return typeof v === 'string' && v ? v : null\n }\n return async function subjectAction({ request }: { request: Request }): Promise<SubjectActionData> {\n if (options.sameSiteOnly !== false && !sameSite(request)) return fail(undefined, 'unsupported')\n const form = await request.formData().catch(() => null)\n if (!form) return fail(undefined, 'validation')\n const tok = await token(request)\n const ip = options.clientIp?.(request)\n const w = { ...(ip ? { clientIp: ip } : {}), ...(tok ? { customerToken: tok } : {}) }\n // The kind is known to the store, not trusted from the form: which fields are private comes from the data.\n const rawKind = text(form.get('kind'))\n let privateKeys: string[] = []\n try {\n const kinds = await sf.content.subjectKinds()\n privateKeys = kinds.find((k) => k.key === rawKind)?.fields.filter((f) => f.private).map((f) => f.key) ?? []\n } catch {\n /* the write below answers for itself */\n }\n const p = parseSubjectForm(form, privateKeys, prefix)\n const intent = p.intent\n if (!intent) return fail(undefined, 'validation')\n // Without JavaScript the cascade advances by posting: nothing is saved, the form is drawn again with these values.\n if (intent === 'subject-refine') return { ok: false, intent, values: p.echo, ...(p.id ? { id: p.id } : {}) }\n if (!tok) return fail(intent, 'unauthorized', { values: p.echo, ...(p.id ? { id: p.id } : {}) })\n try {\n switch (intent) {\n case 'subject-save': {\n if (!p.kind && !p.id) return fail(intent, 'validation', { values: p.echo })\n const saved = p.id\n ? await sf.customer.subjects.update(p.id, { attributes: p.attributes, ...(p.nickname !== undefined ? { nickname: p.nickname } : {}), ...(p.isDefault ? { is_default: true } : {}) } satisfies SubjectPatch, w)\n : await sf.customer.subjects.create({ kind: p.kind, attributes: p.attributes, ...(p.nickname !== undefined ? { nickname: p.nickname } : {}), ...(p.isDefault ? { is_default: true } : {}) } satisfies SubjectInput, w)\n return { ok: true, intent, subject: saved }\n }\n case 'subject-delete': {\n if (!p.id) return fail(intent, 'validation')\n await sf.customer.subjects.delete(p.id, w)\n return { ok: true, intent }\n }\n case 'subject-default': {\n if (!p.id) return fail(intent, 'validation')\n return { ok: true, intent, subject: await sf.customer.subjects.update(p.id, { is_default: true }, w) }\n }\n }\n } catch (err) {\n // Never log the error: a message can carry the field, and the request holds the buyer's items.\n return toFailure(err, intent, p.echo, p.id)\n }\n }\n}\n\nexport type SubjectAction = ReturnType<typeof createSubjectAction>\n"]}
|