@agence-zef/sdk 0.2.0 → 0.4.1

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { ConfigCollections, Registry, ZefConfig } from "@agence-zef/schema";
2
- import type { ApiItem, ApiLayout, ApiLink, ApiList, ApiPage, ApiPageEntry, ApiPreviewDocument, ApiRedirect, ApiSection, ApiSite, ApiSyncResult } from "./types.js";
1
+ import type { ConfigCollections, CustomValuesOf, Registry, ZefConfig } from "@agence-zef/schema";
2
+ import type { ApiAccess, ApiAccount, ApiAccountSession, ApiAccountUpdate, ApiCart, ApiCheckout, ApiCheckoutInput, ApiGiftCardInput, ApiItem, ApiLayout, ApiLink, ApiList, ApiOrder, ApiPage, ApiPageEntry, ApiPlayback, ApiPreviewDocument, ApiRedirect, ApiRegisterInput, ApiSection, ApiServicePoint, ApiShippingOption, ApiSite, ApiSubscription, ApiSyncResult } from "./types.js";
3
3
  export type * from "./types.js";
4
4
  /** Erreur renvoyée par l'API (clé invalide, schéma refusé…). */
5
5
  export declare class ZefApiError extends Error {
@@ -27,13 +27,23 @@ export declare function createClient<C extends ZefConfig = ZefConfig>(options: C
27
27
  site: () => Promise<ApiSite>;
28
28
  /** Toutes les pages publiées et leurs adresses par langue. */
29
29
  pages: () => Promise<ApiPageEntry[]>;
30
- /** Page par adresse (`/la-cave`, `/en/the-cellar`) ; `null` si elle n'existe pas. */
31
- page: (url: string) => Promise<ApiPage | null>;
30
+ /**
31
+ * Page par adresse (`/la-cave`, `/en/the-cellar`) ; `null` si elle n'existe pas. Contenu
32
+ * réservé : avec la session du compte (`visitor`), les sections s'il en a le droit.
33
+ */
34
+ page: (url: string, visitor?: Visitor) => Promise<ApiPage | null>;
35
+ /**
36
+ * Page ou élément par adresse, avec la session du compte (contenu réservé rendu à la
37
+ * demande) ; `null` si rien n'est à cette adresse.
38
+ */
39
+ document: (url: string, visitor?: Visitor) => Promise<ApiPreviewDocument | null>;
32
40
  collection: <K extends keyof Collections<C> & string>(name: K, opts?: LocaleOption & {
33
41
  limit?: number;
34
42
  offset?: number;
35
43
  }) => Promise<ApiList<Collections<C>[K]>>;
36
- item: <K extends keyof Collections<C> & string>(name: K, slug: string, opts?: LocaleOption) => Promise<Collections<C>[K] | null>;
44
+ item: <K extends keyof Collections<C> & string>(name: K, slug: string, opts?: LocaleOption & {
45
+ visitor?: Visitor;
46
+ }) => Promise<Collections<C>[K] | null>;
37
47
  /** Header et footer du site (sections partagées par toutes les pages). */
38
48
  layout: (opts?: LocaleOption) => Promise<ApiLayout>;
39
49
  /** Aperçu : header et footer en brouillon, champs marqués pour l'overlay. */
@@ -55,7 +65,89 @@ export declare function createClient<C extends ZefConfig = ZefConfig>(options: C
55
65
  }) => Promise<ApiSection>;
56
66
  /** Vérifie la clé et enregistre le lien du dépôt (`zef link`). */
57
67
  link: () => Promise<ApiLink>;
68
+ /**
69
+ * Module Comptes : comptes des personnes du site. `session` est le jeton gardé dans le cookie
70
+ * du site ; `ip` l'adresse du visiteur (limites de débit, anti-robot).
71
+ */
72
+ accounts: {
73
+ /** Inscription : ouvre aussi la session et envoie le lien de vérification. */
74
+ register: (input: ApiRegisterInput<CustomValuesOf<C, "accounts.account">>, visitor?: Visitor) => Promise<ApiAccountSession<CustomValuesOf<C, "accounts.account">>>;
75
+ /** Lien de vérification reçu par email. */
76
+ verify: (token: string) => Promise<{
77
+ account: ApiAccount<CustomValuesOf<C, "accounts.account">>;
78
+ }>;
79
+ resendVerification: (visitor: Visitor, verifyUrl?: string) => Promise<Record<string, never>>;
80
+ /** Connexion : `ZefApiError` `invalid_credentials` (401) ou `locked` (429) en cas d'échec. */
81
+ signIn: (email: string, password: string, visitor?: Visitor) => Promise<ApiAccountSession<CustomValuesOf<C, "accounts.account">>>;
82
+ signOut: (visitor: Visitor) => Promise<Record<string, never>>;
83
+ /** Envoie le lien de mot de passe oublié s'il existe un compte (réponse identique sinon). */
84
+ requestPasswordReset: (email: string, resetUrl?: string, visitor?: Visitor) => Promise<Record<string, never>>;
85
+ resetPassword: (token: string, password: string, visitor?: Visitor) => Promise<Record<string, never>>;
86
+ /** Compte de la session, ou `null` sans session valable. */
87
+ me: (visitor: Visitor) => Promise<ApiAccount<CustomValuesOf<C, "accounts.account">> | null>;
88
+ updateMe: (patch: ApiAccountUpdate<CustomValuesOf<C, "accounts.account">>, visitor: Visitor) => Promise<{
89
+ account: ApiAccount<CustomValuesOf<C, "accounts.account">>;
90
+ }>;
91
+ deleteMe: (password: string, visitor: Visitor) => Promise<Record<string, never>>;
92
+ };
93
+ /** Boutique (module commerce) : panier, paiement et commandes du compte connecté. */
94
+ shop: {
95
+ /**
96
+ * Panier ; avec `promoCode`, la remise ou la raison du refus (`promoError`) ; avec `giftCard`,
97
+ * le solde disponible de la carte (`giftCardError` sinon).
98
+ */
99
+ cart: (visitor: Visitor, locale?: string, codes?: {
100
+ promoCode?: string | null;
101
+ giftCard?: string | null;
102
+ }) => Promise<ApiCart>;
103
+ /** Ajoute une quantité ; `out_of_stock` (409) au-delà du stock disponible. */
104
+ addToCart: (variantId: string, quantity: number, visitor: Visitor, locale?: string) => Promise<ApiCart>;
105
+ /** Fixe la quantité d'une ligne (0 la retire). */
106
+ setCartLine: (variantId: string, quantity: number, visitor: Visitor, locale?: string) => Promise<ApiCart>;
107
+ /** Ajoute une carte cadeau (montant et destinataire). */
108
+ addGiftCard: (input: ApiGiftCardInput, visitor: Visitor, locale?: string) => Promise<ApiCart>;
109
+ removeGiftCard: (lineId: string, visitor: Visitor, locale?: string) => Promise<ApiCart>;
110
+ clearCart: (visitor: Visitor) => Promise<ApiCart>;
111
+ /** Modes de livraison proposés pour le panier, selon le pays (`FR` par défaut). */
112
+ shippingOptions: (visitor: Visitor, country?: string, promoCode?: string | null) => Promise<ApiShippingOption[]>;
113
+ /** Points relais proches pour un mode à point relais. */
114
+ servicePoints: (input: {
115
+ option: string;
116
+ country: string;
117
+ postalCode: string;
118
+ }, visitor: Visitor) => Promise<ApiServicePoint[]>;
119
+ /** Lance le paiement : commande en attente, stock réservé, facture Stripe. */
120
+ checkout: (input: ApiCheckoutInput, visitor: Visitor, locale?: string) => Promise<ApiCheckout>;
121
+ /** Accès du compte aux vidéos. */
122
+ access: (visitor: Visitor) => Promise<ApiAccess[]>;
123
+ /**
124
+ * Liens de lecture d'une vidéo (403 `no_access` sans droit). Une location pas encore
125
+ * commencée démarre ici : à n'appeler que sur une demande explicite du visiteur.
126
+ */
127
+ play: (videoId: string, visitor: Visitor) => Promise<ApiPlayback>;
128
+ /** Abonnements du compte (en cours et terminés). */
129
+ subscriptions: (visitor: Visitor) => Promise<ApiSubscription[]>;
130
+ /** Résilie à la fin de la période payée (accès gardés jusque-là). */
131
+ cancelSubscription: (id: string, visitor: Visitor) => Promise<{
132
+ endsAt: string | null;
133
+ }>;
134
+ /**
135
+ * « Changer de carte » : adresse du portail Stripe du client, qui revient sur `returnUrl`
136
+ * (`url: null` en paiement simulé).
137
+ */
138
+ paymentMethod: (id: string, returnUrl: string, visitor: Visitor) => Promise<{
139
+ url: string | null;
140
+ }>;
141
+ orders: (visitor: Visitor) => Promise<ApiOrder[]>;
142
+ order: (id: string, visitor: Visitor) => Promise<ApiOrder>;
143
+ /** Paiement simulé (`provider: "simulated"`, développement local seulement). */
144
+ simulatePayment: (orderId: string, visitor: Visitor) => Promise<Record<string, never>>;
145
+ };
58
146
  /** Envoie le registre du site (`zef sync`). */
59
147
  sync: (registry: Registry) => Promise<ApiSyncResult>;
60
148
  };
61
149
  export type ZefClient<C extends ZefConfig = ZefConfig> = ReturnType<typeof createClient<C>>;
150
+ export type Visitor = {
151
+ session?: string | null;
152
+ ip?: string | null;
153
+ };
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ export function createClient(options) {
28
28
  const res = await doFetch(`${base}${path}${qs}`, {
29
29
  method: init.method ?? "GET",
30
30
  headers: {
31
+ ...init.headers,
31
32
  authorization: `Bearer ${options.apiKey}`,
32
33
  ...(init.body === undefined ? {} : { "content-type": "application/json" }),
33
34
  },
@@ -55,12 +56,23 @@ export function createClient(options) {
55
56
  site: async () => required(await request("/site"), "Site"),
56
57
  /** Toutes les pages publiées et leurs adresses par langue. */
57
58
  pages: async () => required(await request("/pages"), "Pages").pages,
58
- /** Page par adresse (`/la-cave`, `/en/the-cellar`) ; `null` si elle n'existe pas. */
59
- page: (url) => request("/pages", { query: { url } }),
59
+ /**
60
+ * Page par adresse (`/la-cave`, `/en/the-cellar`) ; `null` si elle n'existe pas. Contenu
61
+ * réservé : avec la session du compte (`visitor`), les sections s'il en a le droit.
62
+ */
63
+ page: (url, visitor) => request("/pages", { query: { url }, headers: visitorHeaders(visitor) }),
64
+ /**
65
+ * Page ou élément par adresse, avec la session du compte (contenu réservé rendu à la
66
+ * demande) ; `null` si rien n'est à cette adresse.
67
+ */
68
+ document: (url, visitor) => request("/documents", {
69
+ query: { url },
70
+ headers: visitorHeaders(visitor),
71
+ }),
60
72
  collection: async (name, opts = {}) => required(await request(`/collections/${encodeURIComponent(name)}`, {
61
73
  query: opts,
62
74
  }), `Collection « ${name} »`),
63
- item: (name, slug, opts = {}) => request(`/collections/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`, { query: opts }),
75
+ item: (name, slug, opts = {}) => request(`/collections/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`, { query: { locale: opts.locale }, headers: visitorHeaders(opts.visitor) }),
64
76
  /** Header et footer du site (sections partagées par toutes les pages). */
65
77
  layout: async (opts = {}) => required(await request("/layout", { query: opts }), "Header et footer"),
66
78
  /** Aperçu : header et footer en brouillon, champs marqués pour l'overlay. */
@@ -83,7 +95,112 @@ export function createClient(options) {
83
95
  },
84
96
  /** Vérifie la clé et enregistre le lien du dépôt (`zef link`). */
85
97
  link: async () => required(await request("/link", { method: "POST" }), "Lien"),
98
+ /**
99
+ * Module Comptes : comptes des personnes du site. `session` est le jeton gardé dans le cookie
100
+ * du site ; `ip` l'adresse du visiteur (limites de débit, anti-robot).
101
+ */
102
+ accounts: accounts(request, required),
103
+ /** Boutique (module commerce) : panier, paiement et commandes du compte connecté. */
104
+ shop: shop(request, required),
86
105
  /** Envoie le registre du site (`zef sync`). */
87
106
  sync: async (registry) => required(await request("/schema", { method: "POST", body: registry }), "Registre"),
88
107
  };
89
108
  }
109
+ const visitorHeaders = ({ session, ip } = {}) => ({
110
+ ...(session ? { "x-zef-account": session } : {}),
111
+ ...(ip ? { "x-zef-client-ip": ip } : {}),
112
+ });
113
+ function accounts(request, required) {
114
+ const post = async (path, body, visitor, method = "POST") => required(await request(path, { method, body, headers: visitorHeaders(visitor) }), "Réponse");
115
+ return {
116
+ /** Inscription : ouvre aussi la session et envoie le lien de vérification. */
117
+ register: (input, visitor) => post("/accounts", input, visitor),
118
+ /** Lien de vérification reçu par email. */
119
+ verify: (token) => post("/accounts/verify", { token }),
120
+ resendVerification: (visitor, verifyUrl) => post("/accounts/verify/resend", { verifyUrl }, visitor),
121
+ /** Connexion : `ZefApiError` `invalid_credentials` (401) ou `locked` (429) en cas d'échec. */
122
+ signIn: (email, password, visitor) => post("/sessions", { email, password }, visitor),
123
+ signOut: (visitor) => post("/sessions", undefined, visitor, "DELETE"),
124
+ /** Envoie le lien de mot de passe oublié s'il existe un compte (réponse identique sinon). */
125
+ requestPasswordReset: (email, resetUrl, visitor) => post("/password-reset", { email, resetUrl }, visitor),
126
+ resetPassword: (token, password, visitor) => post("/password-reset/confirm", { token, password }, visitor),
127
+ /** Compte de la session, ou `null` sans session valable. */
128
+ me: async (visitor) => {
129
+ if (!visitor.session)
130
+ return null;
131
+ try {
132
+ return (await post("/me", undefined, visitor, "GET")).account;
133
+ }
134
+ catch (error) {
135
+ if (error instanceof ZefApiError && error.status === 401)
136
+ return null;
137
+ throw error;
138
+ }
139
+ },
140
+ updateMe: (patch, visitor) => post("/me", patch, visitor, "PATCH"),
141
+ deleteMe: (password, visitor) => post("/me", { password }, visitor, "DELETE"),
142
+ };
143
+ }
144
+ function shop(request, required) {
145
+ const call = async (path, visitor, method = "GET", body, locale) => required(await request(path, {
146
+ method,
147
+ body,
148
+ query: locale ? { locale } : undefined,
149
+ headers: visitorHeaders(visitor),
150
+ }), "Réponse");
151
+ return {
152
+ /**
153
+ * Panier ; avec `promoCode`, la remise ou la raison du refus (`promoError`) ; avec `giftCard`,
154
+ * le solde disponible de la carte (`giftCardError` sinon).
155
+ */
156
+ cart: async (visitor, locale, codes = {}) => required(await request("/shop/cart", {
157
+ query: {
158
+ locale,
159
+ promoCode: codes.promoCode || undefined,
160
+ giftCard: codes.giftCard || undefined,
161
+ },
162
+ headers: visitorHeaders(visitor),
163
+ }), "Réponse").cart,
164
+ /** Ajoute une quantité ; `out_of_stock` (409) au-delà du stock disponible. */
165
+ addToCart: async (variantId, quantity, visitor, locale) => (await call("/shop/cart", visitor, "POST", { variantId, quantity }, locale)).cart,
166
+ /** Fixe la quantité d'une ligne (0 la retire). */
167
+ setCartLine: async (variantId, quantity, visitor, locale) => (await call("/shop/cart", visitor, "PATCH", { variantId, quantity }, locale)).cart,
168
+ /** Ajoute une carte cadeau (montant et destinataire). */
169
+ addGiftCard: async (input, visitor, locale) => (await call("/shop/cart/gift-cards", visitor, "POST", input, locale)).cart,
170
+ removeGiftCard: async (lineId, visitor, locale) => (await call(`/shop/cart/gift-cards/${encodeURIComponent(lineId)}`, visitor, "DELETE", undefined, locale)).cart,
171
+ clearCart: async (visitor) => (await call("/shop/cart", visitor, "DELETE")).cart,
172
+ /** Modes de livraison proposés pour le panier, selon le pays (`FR` par défaut). */
173
+ shippingOptions: async (visitor, country = "FR", promoCode) => required(await request("/shop/shipping-options", {
174
+ query: { country, promoCode: promoCode || undefined },
175
+ headers: visitorHeaders(visitor),
176
+ }), "Réponse").options,
177
+ /** Points relais proches pour un mode à point relais. */
178
+ servicePoints: async (input, visitor) => required(await request("/shop/service-points", {
179
+ query: input,
180
+ headers: visitorHeaders(visitor),
181
+ }), "Réponse").points,
182
+ /** Lance le paiement : commande en attente, stock réservé, facture Stripe. */
183
+ checkout: (input, visitor, locale) => call("/shop/checkout", visitor, "POST", input, locale),
184
+ /** Accès du compte aux vidéos. */
185
+ access: async (visitor) => (await call("/shop/access", visitor)).access,
186
+ /**
187
+ * Liens de lecture d'une vidéo (403 `no_access` sans droit). Une location pas encore
188
+ * commencée démarre ici : à n'appeler que sur une demande explicite du visiteur.
189
+ */
190
+ play: async (videoId, visitor) => (await call(`/shop/access/${encodeURIComponent(videoId)}/play`, visitor, "POST", {})).playback,
191
+ /** Abonnements du compte (en cours et terminés). */
192
+ subscriptions: async (visitor) => (await call("/shop/subscriptions", visitor))
193
+ .subscriptions,
194
+ /** Résilie à la fin de la période payée (accès gardés jusque-là). */
195
+ cancelSubscription: async (id, visitor) => call(`/shop/subscriptions/${encodeURIComponent(id)}/cancel`, visitor, "POST", {}),
196
+ /**
197
+ * « Changer de carte » : adresse du portail Stripe du client, qui revient sur `returnUrl`
198
+ * (`url: null` en paiement simulé).
199
+ */
200
+ paymentMethod: async (id, returnUrl, visitor) => call(`/shop/subscriptions/${encodeURIComponent(id)}/payment-method`, visitor, "POST", { returnUrl }),
201
+ orders: async (visitor) => (await call("/shop/orders", visitor)).orders,
202
+ order: async (id, visitor) => (await call(`/shop/orders/${encodeURIComponent(id)}`, visitor)).order,
203
+ /** Paiement simulé (`provider: "simulated"`, développement local seulement). */
204
+ simulatePayment: (orderId, visitor) => call(`/shop/orders/${encodeURIComponent(orderId)}/simulate-payment`, visitor, "POST", {}),
205
+ };
206
+ }
package/dist/types.d.ts CHANGED
@@ -11,11 +11,37 @@ export type ApiSite = {
11
11
  * pose alors `noindex` sur toutes les pages et un `robots.txt` bloquant.
12
12
  */
13
13
  indexable: boolean;
14
+ /**
15
+ * Adresse publique du site (`https://cave-vivante.fr`), sans `/` final : base des URL absolues
16
+ * (canonique, Open Graph, sitemap). Domaine public du client, sinon son adresse de test
17
+ * Vercel ; `null` si aucune n'est connue.
18
+ */
19
+ url: string | null;
20
+ /** Image de partage par défaut (C-18), quand une page n'a pas la sienne. */
21
+ shareImage: Media | null;
22
+ /** Logo du client dans le CMS (adresse), repli du logo des données structurées. */
23
+ logo: string | null;
14
24
  /**
15
25
  * Identifiant de mesure Google Analytics 4 (`G-…`) choisi dans C-21, seulement une fois le
16
26
  * site en ligne : à passer à `<CookieBanner gaId>`, qui le charge après accord.
17
27
  */
18
28
  ga4: string | null;
29
+ /** Réglages publics du module Comptes (U-03), `null` s'il n'est pas activé. */
30
+ accounts: ApiAccountsSettings | null;
31
+ };
32
+ /** Ce que le site doit savoir pour ses formulaires de compte. */
33
+ export type ApiAccountsSettings = {
34
+ /** Un compte doit cliquer sur le lien reçu avant de commander ou réserver. */
35
+ verifyEmail: boolean;
36
+ /** Téléphone à l'inscription : non demandé, facultatif ou obligatoire. */
37
+ phone: "none" | "optional" | "required";
38
+ /** Clé publique du widget Cloudflare Turnstile de l'inscription. */
39
+ turnstileSiteKey: string;
40
+ /** Pages du site qui reçoivent les liens des emails (chemins). */
41
+ pages: {
42
+ verification: string;
43
+ password: string;
44
+ };
19
45
  };
20
46
  export type ApiSection = {
21
47
  id: string;
@@ -49,11 +75,32 @@ export type ApiPage = {
49
75
  sections: ApiSection[];
50
76
  /** Adresse de la page dans chaque langue. */
51
77
  alternates: Record<string, string>;
78
+ /** Dernière publication (ISO), pour `lastmod` du sitemap ; `null` en aperçu d'un brouillon. */
79
+ updatedAt: string | null;
80
+ /** Contenu réservé : sections servies seulement au compte qui en a le droit. */
81
+ access?: ApiReserved;
82
+ };
83
+ /**
84
+ * Contenu réservé (lot 14e5c) : un produit d'accès l'ouvre. Sans droit (`granted: false`), les
85
+ * sections sont vides et `products` dit quoi acheter pour les débloquer.
86
+ */
87
+ export type ApiReserved = {
88
+ reserved: true;
89
+ granted: boolean;
90
+ products: {
91
+ id: string;
92
+ name: string;
93
+ url: string | null;
94
+ }[];
52
95
  };
53
96
  /** Une page et ses adresses, pour générer les routes du site. */
54
97
  export type ApiPageEntry = {
55
98
  id: string;
56
99
  urls: Record<string, string>;
100
+ /** Page parente (fil d'Ariane), `null` à la racine. */
101
+ parentId: string | null;
102
+ /** Titre de la page dans chaque langue (fil d'Ariane, `llms.txt`). */
103
+ titles: Record<string, string>;
57
104
  };
58
105
  /** Élément de collection : ses champs, puis les sections ajoutées sous le gabarit (E-01). */
59
106
  export type ApiItem = Record<string, unknown> & {
@@ -61,6 +108,320 @@ export type ApiItem = Record<string, unknown> & {
61
108
  slug: string;
62
109
  url: string | null;
63
110
  sections: ApiSection[];
111
+ /** Onglet SEO de l'éditeur (E-04), comme pour une page. */
112
+ seo: ApiSeo;
113
+ /** Dernière publication (ISO), pour `lastmod` du sitemap. */
114
+ updatedAt: string | null;
115
+ /** Élément de la collection `products` : prix, variantes, médias (module Boutique). */
116
+ product?: ApiProduct;
117
+ /** Contenu réservé : sections servies seulement au compte qui en a le droit. */
118
+ access?: ApiReserved;
119
+ };
120
+ /** Média d'un produit (médiathèque). */
121
+ export type ApiProductMedia = {
122
+ id: string;
123
+ url: string;
124
+ alt: string;
125
+ width?: number;
126
+ height?: number;
127
+ mime: string;
128
+ };
129
+ /** Variante d'un produit physique : combinaison de valeurs d'options, prix, disponibilité. */
130
+ export type ApiProductVariant = {
131
+ id: string;
132
+ /** `{ <id de l'option>: <id de la valeur> }` ; vide pour la variante unique. */
133
+ options: Record<string, string>;
134
+ /** Prix TTC en centimes ; `compareAt` : prix barré. */
135
+ price: number;
136
+ compareAt: number | null;
137
+ sku: string | null;
138
+ /** En stock (le stock exact n'est pas servi). */
139
+ available: boolean;
140
+ /** Stock sous le seuil de stock bas. */
141
+ lowStock: boolean;
142
+ media: ApiProductMedia | null;
143
+ };
144
+ /**
145
+ * Données de boutique d'un élément de la collection `products` (`item.product`). Prix en
146
+ * centimes, TTC. Un produit masqué ou réservé n'est pas servi.
147
+ */
148
+ export type ApiProduct = {
149
+ /** Identifiant du produit (celui de l'élément de la collection `products`). */
150
+ id: string;
151
+ type: "physical" | "access" | "subscription" | "gift_card";
152
+ /** Prix le plus bas des variantes ; `priceFrom` : « à partir de ». */
153
+ price: number | null;
154
+ priceFrom: boolean;
155
+ compareAt: number | null;
156
+ /** Taux de TVA en points de base (2000 = 20 %). */
157
+ vatRate: number;
158
+ available: boolean;
159
+ categories: {
160
+ id: string;
161
+ name: string;
162
+ }[];
163
+ media: ApiProductMedia[];
164
+ thumbnail: ApiProductMedia | null;
165
+ options: {
166
+ id: string;
167
+ name: string;
168
+ values: {
169
+ id: string;
170
+ label: string;
171
+ }[];
172
+ }[];
173
+ variants: ApiProductVariant[];
174
+ /** Carte cadeau : montants proposés et montant libre (minimum, maximum), en centimes. */
175
+ giftCard: {
176
+ amounts: number[];
177
+ custom: {
178
+ min: number;
179
+ max: number;
180
+ } | null;
181
+ } | null;
182
+ /** Abonnement : période et essai gratuit (le prix est celui de la période). */
183
+ subscription: {
184
+ interval: "month" | "year";
185
+ trialDays: number | null;
186
+ } | null;
187
+ /** Accès : location (durée en heures, délai pour commencer) ou définitif, vidéos données. */
188
+ access: {
189
+ mode: "rental" | "lifetime";
190
+ durationHours: number | null;
191
+ startWithinDays: number | null;
192
+ videoIds: string[];
193
+ } | null;
194
+ /** Produits associés servis, dans l'ordre. */
195
+ related: {
196
+ id: string;
197
+ name: string;
198
+ slug: string;
199
+ url: string | null;
200
+ price: number | null;
201
+ priceFrom: boolean;
202
+ thumbnail: ApiProductMedia | null;
203
+ }[];
204
+ };
205
+ /** Ligne du panier (montants TTC en centimes, recalculés par le CMS). */
206
+ export type ApiCartLine = {
207
+ /** Vide pour une carte cadeau (voir `giftCardLineId`). */
208
+ variantId: string;
209
+ productId: string;
210
+ name: string;
211
+ variantLabel: string | null;
212
+ quantity: number;
213
+ unitPrice: number;
214
+ total: number;
215
+ /** Quantité encore disponible : au-delà, la ligne ne peut pas être payée. */
216
+ available: number;
217
+ /** Produit toujours en vente pour ce compte. */
218
+ sellable: boolean;
219
+ /** Type du produit : un accès ou un abonnement se prend en un exemplaire. */
220
+ productType: "physical" | "access" | "subscription" | "gift_card";
221
+ /** Carte cadeau : ligne à retirer (`removeGiftCard`) et destinataire. */
222
+ giftCardLineId: string | null;
223
+ giftCard: {
224
+ recipientName: string;
225
+ recipientEmail: string;
226
+ message: string;
227
+ /** Envoi programmé (ISO) ; `null` : au paiement. */
228
+ sendAt: string | null;
229
+ } | null;
230
+ };
231
+ /** Carte cadeau à ajouter au panier : montant proposé (ou libre) et destinataire. */
232
+ export type ApiGiftCardInput = {
233
+ productId: string;
234
+ amountCents: number;
235
+ recipientName: string;
236
+ recipientEmail: string;
237
+ message?: string;
238
+ /** `AAAA-MM-JJ` : envoi ce jour-là à 9 h (heure de Paris) ; absent : au paiement. */
239
+ sendOn?: string | null;
240
+ };
241
+ export type ApiCart = {
242
+ lines: ApiCartLine[];
243
+ count: number;
244
+ subtotal: number;
245
+ /** Code promo appliqué (`cart(visitor, locale, promoCode)`), ou `null`. */
246
+ promo: {
247
+ code: string;
248
+ label: string;
249
+ discount: number;
250
+ freeShipping: boolean;
251
+ } | null;
252
+ /** Pourquoi le code demandé ne s'applique pas (inconnu, expiré, minimum…). */
253
+ promoError: string | null;
254
+ /** Carte cadeau saisie : fin du code et solde disponible, déduit au paiement. */
255
+ giftCard: {
256
+ last4: string;
257
+ available: number;
258
+ } | null;
259
+ giftCardError: string | null;
260
+ /** Sous-total moins la remise, hors livraison. */
261
+ total: number;
262
+ /** Des articles sont à livrer ; sinon (cartes cadeaux, accès) ni adresse ni livraison. */
263
+ shippingRequired: boolean;
264
+ };
265
+ export type ApiAddress = {
266
+ name: string;
267
+ line1: string;
268
+ line2?: string | null;
269
+ postalCode: string;
270
+ city: string;
271
+ /** Code pays à deux lettres (`FR`). */
272
+ country: string;
273
+ phone?: string | null;
274
+ };
275
+ /** Mode de livraison proposé pour le panier (modes du compte Sendcloud, retrait sur place). */
276
+ export type ApiShippingOption = {
277
+ /** `sc_<mode Sendcloud>` ou `pickup` : à renvoyer dans `checkout`. */
278
+ id: string;
279
+ kind: "sendcloud" | "pickup";
280
+ label: string;
281
+ /** Transporteur Sendcloud (`colissimo`, `mondial_relay`…). */
282
+ carrier: string | null;
283
+ /** Centimes TTC (0 si offerte). */
284
+ price: number;
285
+ free: boolean;
286
+ /** Un point relais est à choisir (`servicePoints`). */
287
+ servicePoint: boolean;
288
+ pickup: {
289
+ address: string;
290
+ hours: string;
291
+ instructions: string;
292
+ } | null;
293
+ };
294
+ /** Point relais proche (mode à point relais). */
295
+ export type ApiServicePoint = {
296
+ id: number;
297
+ name: string;
298
+ carrier: string;
299
+ address: string;
300
+ postalCode: string;
301
+ city: string;
302
+ country: string;
303
+ /** Distance en mètres. */
304
+ distance: number | null;
305
+ };
306
+ /** Ce qu'envoie le site pour payer : adresse (sauf retrait), mode de livraison, CGV. */
307
+ export type ApiCheckoutInput = {
308
+ shippingAddress?: ApiAddress | null;
309
+ /** Absente pour un panier de cartes cadeaux seulement. */
310
+ shipping?: {
311
+ option: string;
312
+ servicePoint?: number | null;
313
+ } | null;
314
+ /** Code promo (un par commande) ; refus `promo_invalid` (409) s'il ne s'applique plus. */
315
+ promoCode?: string | null;
316
+ /** Carte cadeau : son solde paie tout ou partie de la commande ; refus `gift_card_invalid`. */
317
+ giftCardCode?: string | null;
318
+ acceptTerms: true;
319
+ };
320
+ /** Paiement lancé : commande en attente (stock réservé 30 minutes) et facture à régler. */
321
+ export type ApiCheckout = {
322
+ /** `due` : reste à payer par carte bancaire (total moins la carte cadeau). */
323
+ order: {
324
+ id: string;
325
+ number: string;
326
+ total: number;
327
+ due: number;
328
+ };
329
+ payment: {
330
+ /** `simulated` : développement local, sans Stripe (`simulatePayment`). */
331
+ provider: "stripe" | "simulated";
332
+ publishableKey: string | null;
333
+ /** Secret du Payment Element de Stripe. */
334
+ clientSecret: string | null;
335
+ test: boolean;
336
+ /** Tout est réglé par carte cadeau : la commande est déjà payée, aller à la confirmation. */
337
+ paid: boolean;
338
+ };
339
+ };
340
+ export type ApiOrder = {
341
+ id: string;
342
+ number: string;
343
+ payment: "pending" | "paid" | "failed" | "abandoned" | "refunded" | "partially_refunded";
344
+ fulfillment: "to_prepare" | "ready" | "shipped" | "delivered" | "picked_up" | "canceled" | "completed" | null;
345
+ total: number;
346
+ subtotal: number;
347
+ shipping: number;
348
+ /** Remise du code promo, et le code. */
349
+ discount: number;
350
+ promoCode: string | null;
351
+ /** Montant réglé par carte cadeau. */
352
+ giftCard: number;
353
+ /** Livraison choisie : mode, point relais ou retrait, suivi du colis. */
354
+ delivery: {
355
+ kind: "sendcloud" | "pickup";
356
+ label: string;
357
+ servicePoint: string | null;
358
+ pickup: {
359
+ address: string;
360
+ hours: string;
361
+ } | null;
362
+ tracking: {
363
+ number: string;
364
+ url: string | null;
365
+ } | null;
366
+ } | null;
367
+ vat: number;
368
+ /** Facture Stripe (PDF et page hébergée). */
369
+ invoiceUrl: string | null;
370
+ createdAt: string;
371
+ paidAt: string | null;
372
+ lines: {
373
+ name: string;
374
+ variantLabel: string | null;
375
+ quantity: number;
376
+ unitPrice: number;
377
+ total: number;
378
+ }[];
379
+ };
380
+ /** Accès du compte connecté à une vidéo (produit d'accès acheté ou accès donné). */
381
+ export type ApiAccess = {
382
+ id: string;
383
+ /** `video`, ou `document` pour un contenu réservé. */
384
+ kind: "video" | "document";
385
+ videoId: string | null;
386
+ documentId: string | null;
387
+ title: string;
388
+ durationSeconds: number | null;
389
+ product: string | null;
390
+ mode: "rental" | "lifetime";
391
+ /** Location : durée à partir du premier visionnage. */
392
+ durationHours: number | null;
393
+ /** `not_started` : la location démarre au premier visionnage, avant `startBefore`. */
394
+ state: "not_started" | "active" | "ended" | "expired";
395
+ startBefore: string | null;
396
+ endsAt: string | null;
397
+ };
398
+ /** Abonnement du compte connecté (lot 14e6b). */
399
+ export type ApiSubscription = {
400
+ id: string;
401
+ product: string;
402
+ /** Prix TTC de chaque période, en centimes. */
403
+ priceCents: number;
404
+ interval: "month" | "year";
405
+ /** `trialing`, `active`, `past_due` (paiement refusé, relance en cours), `canceled`… */
406
+ status: string;
407
+ /** Résilié : s'arrête à `currentPeriodEnd`, accès gardés jusque-là. */
408
+ cancelAtPeriodEnd: boolean;
409
+ currentPeriodEnd: string | null;
410
+ trialEnd: string | null;
411
+ /** Terminé (accès coupés) le… */
412
+ endedAt: string | null;
413
+ };
414
+ /** Liens de lecture signés, valables jusqu'à `expiresAt` (au plus 2 heures). */
415
+ export type ApiPlayback = {
416
+ /** Lecteur Stream à intégrer en `<iframe>`. */
417
+ iframe: string | null;
418
+ hls: string | null;
419
+ /** Fichier direct (Stream simulé, développement local). */
420
+ mp4: string | null;
421
+ thumbnail: string | null;
422
+ expiresAt: string;
423
+ /** Fin du droit (location) ; `null` : définitif. */
424
+ endsAt: string | null;
64
425
  };
65
426
  /** Aperçu de l'éditeur : une page ou un élément de collection, brouillon compris. */
66
427
  export type ApiPreviewDocument = {
@@ -108,3 +469,51 @@ export type ApiErrorBody = {
108
469
  details?: string[];
109
470
  };
110
471
  };
472
+ /** Compte d'une personne sur le site. */
473
+ export type ApiAccount<X = Record<string, unknown>> = {
474
+ id: string;
475
+ email: string;
476
+ firstName: string;
477
+ lastName: string;
478
+ phone: string | null;
479
+ /** Adresse confirmée par le lien reçu (requis pour commander quand le site le demande). */
480
+ emailVerified: boolean;
481
+ createdAt: string;
482
+ /** Champs personnalisés déclarés par le site (`defineFields`), `null` quand vides. */
483
+ custom: X;
484
+ };
485
+ /** Session ouverte : le jeton est à garder dans un cookie httpOnly du site. */
486
+ export type ApiSession = {
487
+ token: string;
488
+ expiresAt: string;
489
+ };
490
+ export type ApiAccountSession<X = Record<string, unknown>> = {
491
+ account: ApiAccount<X>;
492
+ session: ApiSession;
493
+ };
494
+ export type ApiRegisterInput<X = Record<string, unknown>> = {
495
+ email: string;
496
+ password: string;
497
+ firstName: string;
498
+ lastName: string;
499
+ phone?: string | null;
500
+ /** Jeton Cloudflare Turnstile du formulaire d'inscription. */
501
+ turnstileToken: string;
502
+ /**
503
+ * Page du site qui reçoit le lien de vérification (`?token=…` ajouté) ; par défaut celle
504
+ * réglée dans U-03.
505
+ */
506
+ verifyUrl?: string;
507
+ /** Champs personnalisés (`defineFields`) ; les obligatoires doivent être remplis. */
508
+ custom?: Partial<X>;
509
+ };
510
+ export type ApiAccountUpdate<X = Record<string, unknown>> = {
511
+ email?: string;
512
+ firstName?: string;
513
+ lastName?: string;
514
+ phone?: string | null;
515
+ /** Obligatoire si l'email change : la nouvelle adresse reçoit un lien de vérification. */
516
+ verifyUrl?: string;
517
+ /** Champs personnalisés à changer (les autres sont gardés). */
518
+ custom?: Partial<X>;
519
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agence-zef/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
4
4
  "description": "Client typé de l'API d'un CMS ZEF (client.agence-zef.fr/api/v1).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@agence-zef/schema": "0.2.0"
20
+ "@agence-zef/schema": "0.4.0"
21
21
  },
22
22
  "homepage": "https://agence-zef.fr",
23
23
  "scripts": {