@odoro-cli/server-commerce 0.4.0 → 0.5.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 CHANGED
@@ -33,10 +33,10 @@ createApp({
33
33
  - discount codes (shop 1.2.0): a percentage or an amount off the basket, priced by the server from `shop.discount_codes` — dates, active flag, minimum basket and usage cap checked; a use counts once the order is paid; once the payment is open, the code is frozen. On a shop still in 1.1.0, a code is refused by name;
34
34
  - `POST /api/storefront/payment-callback`: the order becomes paid only on a callback signed with `callbackSecret` (HMAC-SHA256 of `reference.state.timestamp`, 5-minute window). Idempotent: the stock leaves once.
35
35
 
36
- ## What it does not serve yet
36
+ ## The customer account (shop 1.3.0)
37
37
 
38
- Customer accounts (503, said by name).
38
+ With a `mail` port (`odoroMail`: Odoro sends the link, the shop holds no mail key), `/api/storefront/account` serves the central storefront's contract: `lien` (a fifteen-minute link, at most three an hour, the answer never says whether the address is known), `lettre` (only with the ticked box), `profil` (session required; the consent can be withdrawn), `deconnexion`, and `GET` (profile, paid orders found by address, addresses). The link and the session live in the database as SHA-256 fingerprints only. The link is opened by `openSession`, which the app mounts on `/api/storefront/account/login` (a redirect with cookies, not JSON). Without the port, or on a shop before 1.3.0, the account says it is not available, by name.
39
39
 
40
40
  ## Tests
41
41
 
42
- `COMMERCE_TEST_URL=postgres://user@host:port/postgres pnpm test` — each run creates a database, loads `test/fixtures/shop-1.2.0.sql`, and drops it.
42
+ `COMMERCE_TEST_URL=postgres://user@host:port/postgres pnpm test` — each run creates a database, loads `test/fixtures/shop-1.3.0.sql`, and drops it.
package/dist/index.d.ts CHANGED
@@ -117,6 +117,63 @@ declare function verifyCallback(secret: string, input: {
117
117
  signature: string;
118
118
  }, now?: number): boolean;
119
119
 
120
+ /**
121
+ * The customer's account, in the shop's own database (shop 1.3.0).
122
+ *
123
+ * No password: a fifteen-minute link, sent to the address. The link and the
124
+ * session live in the database as FINGERPRINTS only (SHA-256): a database
125
+ * that is read connects nobody.
126
+ *
127
+ * The same contract as Odoro's central storefront, so the site's account
128
+ * page works unchanged:
129
+ *
130
+ * POST { geste: 'lien', email, offres? } create the account / sign in:
131
+ * a link leaves by e-mail;
132
+ * POST { geste: 'lettre', email, offres } the newsletter — `offres` MUST be
133
+ * true (the ticked box);
134
+ * POST { geste: 'profil', nom?, offres? } the profile (session required);
135
+ * POST { geste: 'deconnexion' } closes the session;
136
+ * GET the profile, the orders, the addresses.
137
+ *
138
+ * `lien` and `lettre` always answer `{ ok: true }`: the form must not become
139
+ * a directory of the shop's customers. At most three links per person per
140
+ * hour: without that bound, the page would fill someone's mailbox.
141
+ *
142
+ * The link itself is opened by `openSession` — mounted by the app on
143
+ * `/api/storefront/account/login`, because it answers with a redirect and
144
+ * cookies, not with JSON.
145
+ *
146
+ * @module
147
+ */
148
+
149
+ /** The session cookie: HttpOnly, the browser's script never reads it. */
150
+ declare const CUSTOMER_COOKIE = "odoro_client";
151
+ /** How long a session lasts, in seconds. */
152
+ declare const SESSION_MAX_AGE: number;
153
+ /** Links per person per hour. */
154
+ declare const LINKS_PER_HOUR = 3;
155
+ /** Where the sign-in link leaves: Odoro sends it, the shop holds no mail key. */
156
+ interface MailPort {
157
+ sendLoginLink(input: {
158
+ readonly email: string;
159
+ readonly token: string;
160
+ }): Promise<void>;
161
+ }
162
+ /** Are the accounts installed (shop 1.3.0)? */
163
+ declare function accountsInstalled(db: Query): Promise<boolean>;
164
+ /**
165
+ * The link from the e-mail, opened: it serves ONCE. Returns the session to
166
+ * set, and the cart to set when the person had one elsewhere — or `null`
167
+ * when the link is unknown, used or expired.
168
+ *
169
+ * The cart follows the person: this browser's cart becomes theirs; if this
170
+ * browser has none, their last open cart comes back.
171
+ */
172
+ declare function openSession(db: Base, token: string, cartToken: string | undefined): Promise<{
173
+ session: string;
174
+ cart: string | null;
175
+ } | null>;
176
+
120
177
  interface CommerceOptions {
121
178
  /** The site's shop database (`@odoro-cli/cloud-connect`, or any `Base`). */
122
179
  readonly db: Base;
@@ -126,11 +183,44 @@ interface CommerceOptions {
126
183
  readonly callbackSecret: string;
127
184
  /** The shop's currency. @defaultValue 'EUR' */
128
185
  readonly currency?: string;
129
- /** Mark the cart cookie `Secure`. @defaultValue true */
186
+ /** Mark the cart and session cookies `Secure`. @defaultValue true */
130
187
  readonly secureCookie?: boolean;
188
+ /**
189
+ * Where the sign-in links leave: Odoro sends them. Without it, the account
190
+ * says it is not available — by name.
191
+ */
192
+ readonly mail?: MailPort;
131
193
  }
132
194
  declare function createCommerceModule(options: CommerceOptions): _odoro_cli_server.ModuleDefinition<Record<never, never>>;
133
195
 
196
+ /**
197
+ * The sign-in link, sent by Odoro.
198
+ *
199
+ * A shop holds no mail key: it asks Odoro to send the link, signed with the
200
+ * site's secret exactly like a payment (`odoro-payment.ts`). Odoro writes the
201
+ * address of the link itself — the shop's own published address — and only
202
+ * takes the TOKEN from here: a compromised site cannot make Odoro mail a link
203
+ * that leads anywhere else.
204
+ *
205
+ * @module
206
+ */
207
+
208
+ interface OdoroMailOptions {
209
+ /** Odoro's address, e.g. `https://odoro.ai`. */
210
+ readonly origin: string;
211
+ /** This site's identifier at Odoro. */
212
+ readonly site: string;
213
+ /** This site's secret, shared with Odoro alone. */
214
+ readonly secret: string;
215
+ /** @internal For the tests. */
216
+ readonly fetch?: typeof fetch;
217
+ /** @internal For the tests: seconds since the epoch. */
218
+ readonly now?: () => number;
219
+ }
220
+ /** Where Odoro sends a site's sign-in link. */
221
+ declare const ODORO_MAIL_PATH = "/api/hosting/site-login-link";
222
+ declare function odoroMail(options: OdoroMailOptions): MailPort;
223
+
134
224
  /**
135
225
  * The payment, opened at Odoro.
136
226
  *
@@ -219,4 +309,4 @@ interface StoredImage {
219
309
  */
220
310
  declare function productImage(db: Query, productId: string): Promise<StoredImage | null>;
221
311
 
222
- export { type Base, CART_COOKIE, type CheckoutInput, type CommerceOptions, ODORO_PAYMENT_PATH, type OdoroPaymentOptions, type PaymentPort, type PgClientLike, type PgPoolLike, type Query, SHOP_MAJOR, type StoredImage, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroPayment, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
312
+ export { type Base, CART_COOKIE, CUSTOMER_COOKIE, type CheckoutInput, type CommerceOptions, LINKS_PER_HOUR, type MailPort, ODORO_MAIL_PATH, ODORO_PAYMENT_PATH, type OdoroMailOptions, type OdoroPaymentOptions, type PaymentPort, type PgClientLike, type PgPoolLike, type Query, SESSION_MAX_AGE, SHOP_MAJOR, type StoredImage, accountsInstalled, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroMail, odoroPayment, openSession, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createHmac, timingSafeEqual, randomBytes } from 'crypto';
1
+ import { createHmac, timingSafeEqual, createHash, randomBytes } from 'crypto';
2
2
  import { route, defineModule, NotFoundError, ConflictError, ServiceUnavailableError, ApiError } from '@odoro-cli/server';
3
3
  import { z } from 'zod';
4
4
 
@@ -394,7 +394,7 @@ async function openCheckout(db, cartId, input, currency = "EUR") {
394
394
  if (!EMAIL.test(email))
395
395
  throw new CheckoutError(400, "Cette adresse e-mail n'est pas valable.");
396
396
  const name = need(input.nom, "Indiquez votre nom.");
397
- const address = {
397
+ const address2 = {
398
398
  ligne1: need(input.ligne1, "Indiquez votre adresse."),
399
399
  ligne2: (input.ligne2 ?? "").trim(),
400
400
  code_postal: need(input.code_postal, "Indiquez votre code postal."),
@@ -465,7 +465,7 @@ async function openCheckout(db, cartId, input, currency = "EUR") {
465
465
  const { rows: created } = await tx.query(
466
466
  `INSERT INTO shop.orders (cart_id, email, customer_name, shipping_address, currency, subtotal_cents, total_cents)
467
467
  VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING id`,
468
- [cartId, email, name, JSON.stringify(address), currency, subtotal]
468
+ [cartId, email, name, JSON.stringify(address2), currency, subtotal]
469
469
  );
470
470
  const orderId = created[0].id;
471
471
  for (const l of lines) {
@@ -677,6 +677,195 @@ async function orderState(db, orderId) {
677
677
  livraisons: []
678
678
  };
679
679
  }
680
+ var CUSTOMER_COOKIE = "odoro_client";
681
+ var SESSION_MAX_AGE = 90 * 24 * 3600;
682
+ var LINK_MINUTES = 15;
683
+ var LINKS_PER_HOUR = 3;
684
+ var TOKEN2 = /^[A-Za-z0-9_-]{20,80}$/;
685
+ var EMAIL2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
686
+ async function accountsInstalled(db) {
687
+ try {
688
+ const { rows } = await db.query(
689
+ `SELECT to_regclass('shop.customers') IS NOT NULL AS present`
690
+ );
691
+ return rows[0]?.present === true;
692
+ } catch {
693
+ return false;
694
+ }
695
+ }
696
+ function fingerprint(token) {
697
+ return createHash("sha256").update(token).digest("hex");
698
+ }
699
+ function newToken() {
700
+ return randomBytes(32).toString("base64url");
701
+ }
702
+ async function register(db, email, input) {
703
+ const { rows } = await db.query(
704
+ `INSERT INTO shop.customers (email, name, marketing_consent, consent_source, consent_at)
705
+ VALUES ($1, $2, $3, CASE WHEN $3 THEN $4 END, CASE WHEN $3 THEN now() END)
706
+ ON CONFLICT (email) DO UPDATE SET
707
+ name = CASE WHEN excluded.name <> '' THEN excluded.name ELSE shop.customers.name END,
708
+ marketing_consent = shop.customers.marketing_consent OR excluded.marketing_consent,
709
+ consent_source = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent
710
+ THEN excluded.consent_source ELSE shop.customers.consent_source END,
711
+ consent_at = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent
712
+ THEN now() ELSE shop.customers.consent_at END
713
+ RETURNING id`,
714
+ [email, (input.name ?? "").trim().slice(0, 120), input.consent, input.source]
715
+ );
716
+ return rows[0].id;
717
+ }
718
+ function address(raw) {
719
+ const email = (raw ?? "").trim().toLowerCase().slice(0, 254);
720
+ if (!EMAIL2.test(email))
721
+ throw new CheckoutError(400, "Cette adresse de courriel n'est pas valable.");
722
+ return email;
723
+ }
724
+ async function requestLink(db, mail, input) {
725
+ const email = address(input.email);
726
+ const id = await register(db, email, {
727
+ ...input.name === void 0 ? {} : { name: input.name },
728
+ consent: input.consent === true,
729
+ source: "compte"
730
+ });
731
+ const { rows } = await db.query(
732
+ `SELECT count(*)::integer AS n FROM shop.customer_links
733
+ WHERE customer_id = $1 AND created_at > now() - interval '1 hour'`,
734
+ [id]
735
+ );
736
+ if (Number(rows[0]?.n ?? 0) >= LINKS_PER_HOUR) return;
737
+ const token = newToken();
738
+ await db.query(
739
+ `INSERT INTO shop.customer_links (customer_id, fingerprint, expires_at)
740
+ VALUES ($1, $2, now() + make_interval(mins => $3))`,
741
+ [id, fingerprint(token), LINK_MINUTES]
742
+ );
743
+ try {
744
+ await mail.sendLoginLink({ email, token });
745
+ } catch {
746
+ }
747
+ }
748
+ async function subscribe(db, input) {
749
+ if (input.consent !== true) throw new CheckoutError(400, "Requ\xEAte illisible.");
750
+ await register(db, address(input.email), {
751
+ ...input.name === void 0 ? {} : { name: input.name },
752
+ consent: true,
753
+ source: "lettre"
754
+ });
755
+ }
756
+ async function openSession(db, token, cartToken) {
757
+ if (!TOKEN2.test(token)) return null;
758
+ return await db.transaction(async (tx) => {
759
+ const { rows } = await tx.query(
760
+ `UPDATE shop.customer_links SET used_at = now()
761
+ WHERE fingerprint = $1 AND used_at IS NULL AND expires_at > now()
762
+ RETURNING customer_id`,
763
+ [fingerprint(token)]
764
+ );
765
+ const customer = rows[0]?.customer_id;
766
+ if (customer === void 0) return null;
767
+ const session = newToken();
768
+ await tx.query(
769
+ `INSERT INTO shop.customer_sessions (customer_id, fingerprint, expires_at)
770
+ VALUES ($1, $2, now() + make_interval(secs => $3))`,
771
+ [customer, fingerprint(session), SESSION_MAX_AGE]
772
+ );
773
+ await tx.query("UPDATE shop.customers SET last_seen_at = now() WHERE id = $1", [
774
+ customer
775
+ ]);
776
+ if (cartToken !== void 0 && cartToken !== "") {
777
+ const { rows: mine } = await tx.query(
778
+ `UPDATE shop.carts SET customer_id = $2, updated_at = now()
779
+ WHERE token = $1 AND state = 'ouvert' RETURNING id`,
780
+ [cartToken, customer]
781
+ );
782
+ if (mine.length > 0) return { session, cart: null };
783
+ }
784
+ const { rows: last } = await tx.query(
785
+ `SELECT token FROM shop.carts
786
+ WHERE customer_id = $1 AND state = 'ouvert'
787
+ ORDER BY updated_at DESC LIMIT 1`,
788
+ [customer]
789
+ );
790
+ return { session, cart: last[0]?.token ?? null };
791
+ });
792
+ }
793
+ async function customerOf(db, session) {
794
+ if (session === void 0 || !TOKEN2.test(session)) return null;
795
+ const { rows } = await db.query(
796
+ `SELECT c.id, c.email, c.name, c.marketing_consent
797
+ FROM shop.customer_sessions s JOIN shop.customers c ON c.id = s.customer_id
798
+ WHERE s.fingerprint = $1 AND s.expires_at > now()`,
799
+ [fingerprint(session)]
800
+ );
801
+ const c = rows[0];
802
+ return c === void 0 ? null : { id: c.id, email: c.email, name: c.name, consent: c.marketing_consent };
803
+ }
804
+ async function profile(db, customer) {
805
+ const { rows: orders } = await db.query(
806
+ `SELECT id, state, total_cents, currency, created_at, paid_at, shipping_address, customer_name
807
+ FROM shop.orders
808
+ WHERE lower(email) = $1 AND state IN ('payee', 'remboursee', 'annulee')
809
+ ORDER BY created_at DESC LIMIT 50`,
810
+ [customer.email]
811
+ );
812
+ const seen = /* @__PURE__ */ new Set();
813
+ const addresses = [];
814
+ for (const o of orders) {
815
+ const a = o.shipping_address ?? {};
816
+ const lignes = [
817
+ a["ligne1"],
818
+ a["ligne2"],
819
+ `${a["code_postal"] ?? ""} ${a["ville"] ?? ""}`.trim(),
820
+ a["pays"]
821
+ ].filter((x) => typeof x === "string" && x !== "");
822
+ const key = lignes.join("|");
823
+ if (lignes.length === 0 || seen.has(key) || addresses.length >= 5) continue;
824
+ seen.add(key);
825
+ addresses.push({ nom: o.customer_name || null, lignes });
826
+ }
827
+ return {
828
+ nom: customer.name === "" ? null : customer.name,
829
+ courriel: customer.email,
830
+ offres: customer.consent,
831
+ commandes: orders.map((o) => ({
832
+ id: o.id,
833
+ etat: o.state,
834
+ total: new Intl.NumberFormat("fr", {
835
+ style: "currency",
836
+ currency: o.currency
837
+ }).format(Number(o.total_cents) / 100),
838
+ le: new Date(o.paid_at ?? o.created_at).toISOString()
839
+ })),
840
+ adresses: addresses
841
+ };
842
+ }
843
+ async function updateProfile(db, customer, input) {
844
+ await db.query(
845
+ `UPDATE shop.customers SET
846
+ name = coalesce($2, name),
847
+ marketing_consent = coalesce($3, marketing_consent),
848
+ consent_source = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN 'compte'
849
+ WHEN $3 IS FALSE THEN NULL ELSE consent_source END,
850
+ consent_at = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN now()
851
+ WHEN $3 IS FALSE THEN NULL ELSE consent_at END,
852
+ last_seen_at = now()
853
+ WHERE id = $1`,
854
+ [
855
+ customer.id,
856
+ input.name === void 0 ? null : input.name.trim().slice(0, 120),
857
+ input.consent === void 0 ? null : input.consent
858
+ ]
859
+ );
860
+ }
861
+ async function closeSession(db, session) {
862
+ if (session === void 0 || !TOKEN2.test(session)) return;
863
+ await db.query("DELETE FROM shop.customer_sessions WHERE fingerprint = $1", [
864
+ fingerprint(session)
865
+ ]);
866
+ }
867
+
868
+ // src/module.ts
680
869
  function refusal(kind, message) {
681
870
  const extensions = { extensions: { erreur: message } };
682
871
  if (kind === "NOT_FOUND") return new NotFoundError(message, extensions);
@@ -904,19 +1093,65 @@ function createCommerceModule(options) {
904
1093
  method: "GET",
905
1094
  path: "/api/storefront/account",
906
1095
  auth: "public",
907
- handler: () => ({ connecte: false })
1096
+ handler: async ({ cookies }) => {
1097
+ if (options.mail === void 0 || !await accountsInstalled(db))
1098
+ return { connecte: false };
1099
+ const customer = await customerOf(db, cookies.get(CUSTOMER_COOKIE));
1100
+ if (customer === null) return { connecte: false };
1101
+ return { connecte: true, profil: await profile(db, customer) };
1102
+ }
908
1103
  });
909
1104
  const accountWrite = route({
910
1105
  name: "storefront.account.write",
911
1106
  method: "POST",
912
1107
  path: "/api/storefront/account",
913
1108
  auth: "public",
914
- handler: () => {
915
- throw refusal(
916
- "UNAVAILABLE",
917
- "Le compte client n'est pas encore disponible sur cette boutique."
918
- );
919
- }
1109
+ input: z.object({
1110
+ geste: z.enum(["lien", "lettre", "profil", "deconnexion"]).optional(),
1111
+ email: z.string().max(254).optional(),
1112
+ offres: z.boolean().optional(),
1113
+ nom: z.string().max(120).optional()
1114
+ }),
1115
+ handler: async ({ input, cookies }) => await speaking(async () => {
1116
+ const mail = options.mail;
1117
+ if (mail === void 0 || !await accountsInstalled(db)) {
1118
+ throw refusal(
1119
+ "UNAVAILABLE",
1120
+ "Le compte client n'est pas encore disponible sur cette boutique."
1121
+ );
1122
+ }
1123
+ const geste = input.geste ?? "lien";
1124
+ const who = {
1125
+ email: input.email,
1126
+ ...input.nom === void 0 ? {} : { name: input.nom },
1127
+ ...input.offres === void 0 ? {} : { consent: input.offres }
1128
+ };
1129
+ if (geste === "lien") {
1130
+ await requestLink(db, mail, who);
1131
+ return { ok: true };
1132
+ }
1133
+ if (geste === "lettre") {
1134
+ await subscribe(db, who);
1135
+ return { ok: true };
1136
+ }
1137
+ const session = cookies.get(CUSTOMER_COOKIE);
1138
+ if (geste === "deconnexion") {
1139
+ await closeSession(db, session);
1140
+ cookies.clear(CUSTOMER_COOKIE);
1141
+ return { ok: true };
1142
+ }
1143
+ const customer = await customerOf(db, session);
1144
+ if (customer === null) {
1145
+ const message = "Ce lien de connexion est p\xE9rim\xE9 ou a d\xE9j\xE0 servi. Demandez-en un nouveau.";
1146
+ throw new ApiError("UNAUTHORIZED", message, { extensions: { erreur: message } });
1147
+ }
1148
+ await updateProfile(db, customer, {
1149
+ ...input.nom === void 0 ? {} : { name: input.nom },
1150
+ ...input.offres === void 0 ? {} : { consent: input.offres }
1151
+ });
1152
+ const after = await customerOf(db, session) ?? customer;
1153
+ return { ok: true, profil: await profile(db, after) };
1154
+ })
920
1155
  });
921
1156
  return defineModule({
922
1157
  name: "commerce",
@@ -940,7 +1175,7 @@ var UNAVAILABLE = "Le paiement n'a pas pu s'ouvrir. R\xE9essayez dans un instant
940
1175
  function odoroPayment(options) {
941
1176
  const send = options.fetch ?? fetch;
942
1177
  const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
943
- const address = new URL(ODORO_PAYMENT_PATH, options.origin).toString();
1178
+ const address2 = new URL(ODORO_PAYMENT_PATH, options.origin).toString();
944
1179
  return {
945
1180
  open: async (order) => {
946
1181
  if (options.secret === "") throw new CheckoutError(409, UNAVAILABLE);
@@ -955,7 +1190,7 @@ function odoroPayment(options) {
955
1190
  const timestamp = now();
956
1191
  let reply;
957
1192
  try {
958
- reply = await send(address, {
1193
+ reply = await send(address2, {
959
1194
  method: "POST",
960
1195
  headers: {
961
1196
  "content-type": "application/json",
@@ -978,6 +1213,32 @@ function odoroPayment(options) {
978
1213
  };
979
1214
  }
980
1215
 
1216
+ // src/odoro-mail.ts
1217
+ var ODORO_MAIL_PATH = "/api/hosting/site-login-link";
1218
+ function odoroMail(options) {
1219
+ const send = options.fetch ?? fetch;
1220
+ const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
1221
+ const address2 = new URL(ODORO_MAIL_PATH, options.origin).toString();
1222
+ return {
1223
+ sendLoginLink: async ({ email, token }) => {
1224
+ if (options.secret === "") throw new Error("site secret missing");
1225
+ const body = JSON.stringify({ site: options.site, courriel: email, jeton: token });
1226
+ const timestamp = now();
1227
+ const reply = await send(address2, {
1228
+ method: "POST",
1229
+ headers: {
1230
+ "content-type": "application/json",
1231
+ "x-odoro-timestamp": String(timestamp),
1232
+ "x-odoro-signature": signRequest(options.secret, timestamp, body)
1233
+ },
1234
+ body,
1235
+ signal: AbortSignal.timeout(15e3)
1236
+ });
1237
+ if (!reply.ok) throw new Error(`Odoro refused the link (${reply.status})`);
1238
+ }
1239
+ };
1240
+ }
1241
+
981
1242
  // src/pg.ts
982
1243
  function reader(run) {
983
1244
  return {
@@ -1007,6 +1268,6 @@ function baseFromPool(pool) {
1007
1268
  };
1008
1269
  }
1009
1270
 
1010
- export { CART_COOKIE, ODORO_PAYMENT_PATH, SHOP_MAJOR, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroPayment, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
1271
+ export { CART_COOKIE, CUSTOMER_COOKIE, LINKS_PER_HOUR, ODORO_MAIL_PATH, ODORO_PAYMENT_PATH, SESSION_MAX_AGE, SHOP_MAJOR, accountsInstalled, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroMail, odoroPayment, openSession, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
1011
1272
  //# sourceMappingURL=index.js.map
1012
1273
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/base.ts","../src/images.ts","../src/catalogue.ts","../src/cart.ts","../src/checkout.ts","../src/module.ts","../src/odoro-payment.ts","../src/pg.ts"],"names":["text","count","createHmac"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * What the commerce module needs from a database — and nothing more.\n *\n * `query` and `transaction`, the shape `@odoro-cli/cloud-connect` already has:\n * the app wires its connection, and the tests a plain `pg` pool. The module\n * never opens a connection itself, and never reads a connection string: that\n * belongs to the app's configuration.\n *\n * @module\n */\n\n/** Something that runs SQL: a connection, or a transaction. */\nexport interface Query {\n query<Row extends object = Record<string, unknown>>(\n text: string,\n values?: readonly unknown[],\n ): Promise<{ readonly rows: readonly Row[] }>\n}\n\n/** A database the module can also run a transaction on. */\nexport interface Base extends Query {\n transaction<T>(work: (tx: Query) => Promise<T>): Promise<T>\n}\n\n/** The capability version this module speaks. A `shop` 2.x would not be read. */\nexport const SHOP_MAJOR = 1\n\n/**\n * Is the `shop` capability installed, at a version this module reads?\n *\n * Read in the database's own registry (`odoro.features`), which the capability\n * manager writes. Checked once per process: a capability is not uninstalled\n * under a running site.\n */\nexport async function shopInstalled(base: Query): Promise<boolean> {\n try {\n const { rows } = await base.query<{ version: string }>(\n `SELECT version FROM odoro.features WHERE name = 'shop' AND active`,\n )\n const version = rows[0]?.version\n return (\n version !== undefined &&\n Number.parseInt(version.split('.')[0] ?? '', 10) === SHOP_MAJOR\n )\n } catch {\n return false\n }\n}\n","/**\n * Product images, read from the site's own storage (capability `storage`,\n * DECISIONS §44 of the Odoro repository).\n *\n * `shop.product_images.storage_key` names an object of `storage.objects`. The\n * storefront gives an image two ways, as Odoro's central storefront does:\n * inline in `visuel` (base64, so a card renders without a second request),\n * and as bytes for `geste=image` (so a page points at a URL that follows the\n * merchant's latest photo without republishing).\n *\n * A site whose database has no `storage` capability yet keeps working: its\n * images are `null`, as before. That is checked once per process — a\n * capability is not uninstalled under a running site.\n *\n * @module\n */\n\nimport type { Query } from './base.js'\n\n/** SVG can carry script: it is never served, whatever the database says. */\nconst SERVED_TYPES = new Set([\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/avif',\n 'image/gif',\n])\n\nexport async function storageInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('storage.objects') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport interface StoredImage {\n readonly bytes: Buffer\n readonly type: string\n}\n\n/**\n * The first image of a product FOR SALE, as bytes. `null` for a draft, an\n * unknown id, a product without image, or a type that is never served.\n */\nexport async function productImage(\n db: Query,\n productId: string,\n): Promise<StoredImage | null> {\n if (!/^[0-9a-f-]{36}$/i.test(productId)) return null\n if (!(await storageInstalled(db))) return null\n const { rows } = await db.query<{ bytes: Buffer; mime_type: string }>(\n `SELECT o.bytes, o.mime_type\n FROM shop.product_images i\n JOIN shop.products p ON p.id = i.product_id\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL\n ORDER BY i.position, i.id\n LIMIT 1`,\n [productId],\n )\n const row = rows[0]\n if (row === undefined || !SERVED_TYPES.has(row.mime_type)) return null\n return { bytes: row.bytes, type: row.mime_type }\n}\n\n/** The SQL that yields a product's images as base64, when storage exists. */\nexport const SQL_IMAGES = `\n SELECT encode(o.bytes, 'base64') AS contenu, o.mime_type AS type, i.alt_text AS texte\n FROM shop.product_images i\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE i.product_id = p.id AND o.mime_type <> 'image/svg+xml'\n ORDER BY i.position, i.id`\n","/**\n * The catalogue, read from the `shop` schema in the storefront's shapes.\n *\n * Only what is FOR SALE: published, not deleted. A draft is the merchant's\n * business; a visitor who guesses its id gets the same 404 as for a product\n * that never existed.\n *\n * Availability is per VARIANT — the stock lives there — minus the open,\n * unexpired reservations of carts being paid. An expired reservation holds\n * nothing: an abandoned checkout must not take an item off sale forever.\n *\n * Images: `shop.product_images` names objects of the site's `storage`\n * capability (see `images.ts`). They come inline, base64, as in Odoro's\n * central storefront. A database without `storage` keeps `visuel: null`.\n *\n * @module\n */\n\nimport type {\n CatalogueQuery,\n CollectionEnVitrine,\n FicheProduit,\n ProduitEnVitrine,\n VarianteEnVitrine,\n} from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_IMAGES, storageInstalled } from './images.js'\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** A variant is available when its stock, minus what is being paid, covers one. */\nexport const SQL_AVAILABLE = `\n (v.stock IS NULL OR v.stock - coalesce((\n SELECT sum(r.quantity)::integer FROM shop.stock_reservations r\n WHERE r.variant_id = v.id AND r.state = 'ouverte' AND r.expires_at > now()\n ), 0) >= 1)`\n\n/** The sort orders a visitor may ask for: a closed list, never a concatenated `ORDER BY`. */\nconst ORDERS: Readonly<Record<NonNullable<CatalogueQuery['tri']>, string>> = {\n nouveautes: 'p.created_at DESC, p.name',\n prix_croissant: 'p.price_cents ASC, p.name',\n prix_decroissant: 'p.price_cents DESC, p.name',\n nom: 'p.name ASC',\n}\n\ninterface ProductRow {\n id: string\n name: string\n description: string\n kind: string\n price_cents: number\n compare_at_cents: number | null\n available: boolean\n /** The first image, when the database has `storage`. */\n visuel?: { contenu: string; type: string; texte: string } | null\n}\n\n/** The first image of each product, as a SELECT column — or a plain null. */\nfunction colonneDuVisuel(avecStockage: boolean): string {\n return avecStockage\n ? `(SELECT json_build_object('contenu', im.contenu, 'type', im.type, 'texte', im.texte)\n FROM (${SQL_IMAGES} LIMIT 1) im) AS visuel`\n : 'NULL::json AS visuel'\n}\n\nfunction toProduct(r: ProductRow): ProduitEnVitrine {\n return {\n id: r.id,\n nom: r.name,\n description: r.description,\n genre: r.kind,\n prixCentimes: Number(r.price_cents),\n prixBarreCentimes: r.compare_at_cents === null ? null : Number(r.compare_at_cents),\n disponible: r.available === true,\n visuel: r.visuel ?? null,\n etiquettes: [],\n prixUnitaire: null,\n }\n}\n\nexport async function readCatalogue(\n db: Query,\n q: CatalogueQuery = {},\n): Promise<{ produits: ProduitEnVitrine[]; total: number }> {\n const text = (q.q ?? '').trim()\n const values: unknown[] = [\n text === '' ? null : `%${text.toLowerCase()}%`,\n q.collection !== undefined && UUID.test(q.collection) ? q.collection : null,\n q.genre ?? null,\n q.prixMin ?? null,\n q.prixMax ?? null,\n q.disponibles === true,\n ]\n const where = `\n FROM shop.products p\n WHERE p.published AND p.deleted_at IS NULL\n AND ($1::text IS NULL OR lower(p.name) LIKE $1 OR lower(p.description) LIKE $1)\n AND ($2::uuid IS NULL OR EXISTS (\n SELECT 1 FROM shop.collection_products cp\n JOIN shop.collections c ON c.id = cp.collection_id AND c.published\n WHERE cp.collection_id = $2 AND cp.product_id = p.id))\n AND ($3::text IS NULL OR p.kind = $3)\n AND ($4::integer IS NULL OR p.price_cents >= $4)\n AND ($5::integer IS NULL OR p.price_cents <= $5)\n AND (NOT $6::boolean OR EXISTS (\n SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}))`\n // An unknown order falls back to the merchant's, silently: it is a sort, not an error.\n const order = (q.tri === undefined ? undefined : ORDERS[q.tri]) ?? 'p.position, p.name'\n const limit = Math.min(Math.max(q.combien ?? 24, 1), 60)\n const offset = Math.max(q.depuis ?? 0, 0)\n\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n ${where}\n ORDER BY ${order}\n LIMIT $7 OFFSET $8`,\n [...values, limit, offset],\n )\n const { rows: count } = await db.query<{ n: number }>(\n `SELECT count(*)::integer AS n ${where}`,\n values,\n )\n return { produits: rows.map(toProduct), total: Number(count[0]?.n ?? 0) }\n}\n\nexport async function readProduct(db: Query, id: string): Promise<FicheProduit | null> {\n if (!UUID.test(id)) return null\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n FROM shop.products p\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL`,\n [id],\n )\n const row = rows[0]\n if (row === undefined) return null\n\n const { rows: options } = await db.query<{\n id: string\n name: string\n values: string[] | null\n }>(\n `SELECT o.id, o.name,\n array_agg(DISTINCT vv.value) FILTER (WHERE vv.value IS NOT NULL) AS values\n FROM shop.product_options o\n LEFT JOIN shop.product_variant_values vv ON vv.option_id = o.id\n WHERE o.product_id = $1\n GROUP BY o.id, o.name, o.position\n ORDER BY o.position, o.name`,\n [id],\n )\n const { rows: variants } = await db.query<{\n id: string\n price_cents: number\n available: boolean\n choices: { option: string; valeur: string }[] | null\n }>(\n `SELECT v.id,\n coalesce(v.price_cents, p.price_cents) AS price_cents,\n ${SQL_AVAILABLE} AS available,\n (SELECT json_agg(json_build_object('option', o.name, 'valeur', vv.value) ORDER BY o.position, o.name)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS choices\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.product_id = $1\n ORDER BY v.position, v.created_at`,\n [id],\n )\n // Every image of the product, in order, for the product page.\n const visuels = avecStockage\n ? (\n await db.query<{ contenu: string; type: string; texte: string }>(\n `SELECT im.contenu, im.type, im.texte FROM (${SQL_IMAGES}) im`.replace(\n 'p.id',\n '$1',\n ),\n [id],\n )\n ).rows.map((v) => ({ contenu: v.contenu, type: v.type, texte: v.texte }))\n : []\n const product = toProduct(row)\n const variantes: VarianteEnVitrine[] = variants.map((v) => {\n const choix = v.choices ?? []\n const price = Number(v.price_cents)\n return {\n id: v.id,\n libelle:\n choix.length === 0\n ? row.name\n : choix.map((c) => `${c.option} : ${c.valeur}`).join(' / '),\n choix,\n prixCentimes: price,\n prixBarreCentimes:\n row.compare_at_cents !== null && row.compare_at_cents > price\n ? Number(row.compare_at_cents)\n : null,\n disponible: v.available === true,\n visuelRang: null,\n prixUnitaire: null,\n }\n })\n return {\n ...product,\n seoTitre: row.name,\n seoResume: row.description,\n visuels,\n options: options.map((o) => ({ id: o.id, nom: o.name, valeurs: o.values ?? [] })),\n variantes,\n }\n}\n\nexport async function readCollections(db: Query): Promise<CollectionEnVitrine[]> {\n const { rows } = await db.query<{\n id: string\n name: string\n description: string\n n: number\n }>(\n `SELECT c.id, c.name, c.description,\n (SELECT count(*)::integer FROM shop.collection_products cp\n JOIN shop.products p ON p.id = cp.product_id\n WHERE cp.collection_id = c.id AND p.published AND p.deleted_at IS NULL) AS n\n FROM shop.collections c\n WHERE c.published\n ORDER BY c.name`,\n )\n return rows.map((c) => ({\n id: c.id,\n nom: c.name,\n description: c.description,\n combien: Number(c.n),\n }))\n}\n","/**\n * The cart, found by its token.\n *\n * The token travels in an `HttpOnly` cookie: no script of the page reads it,\n * so no third-party script injected into a site can copy it elsewhere. It is\n * set on a GESTURE (an add), never on a visit — a site that sets a cookie on\n * every page view would need a consent banner for it.\n *\n * The unit price is recorded at the add: checkout compares it with the price of\n * the day, and says so when it changed.\n *\n * @module\n */\n\nimport { randomBytes } from 'node:crypto'\n\nimport type { LigneDePanier, PanierEnVitrine } from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_AVAILABLE } from './catalogue.js'\n\nexport const CART_COOKIE = 'odoro_panier'\n/** Thirty days: the window of an abandoned-cart reminder, no longer. */\nexport const CART_MAX_AGE = 60 * 60 * 24 * 30\n\nconst TOKEN = /^vitrine-[0-9a-f]{48}$/\n\nexport function isCartToken(value: string | undefined): value is string {\n return value !== undefined && TOKEN.test(value)\n}\n\nexport function newCartToken(): string {\n return `vitrine-${randomBytes(24).toString('hex')}`\n}\n\n/** Refused gestures carry the sentence the visitor reads. */\nexport class CartError extends Error {}\n\nexport async function findCart(\n db: Query,\n token: string | undefined,\n): Promise<{ id: string; token: string } | null> {\n if (!isCartToken(token)) return null\n const { rows } = await db.query<{ id: string }>(\n `SELECT id FROM shop.carts WHERE token = $1 AND state = 'ouvert' AND NOT preview`,\n [token],\n )\n const row = rows[0]\n return row === undefined ? null : { id: row.id, token }\n}\n\nexport async function openCart(db: Query): Promise<{ id: string; token: string }> {\n const token = newCartToken()\n const { rows } = await db.query<{ id: string }>(\n `INSERT INTO shop.carts (token) VALUES ($1) RETURNING id`,\n [token],\n )\n return { id: rows[0]!.id, token }\n}\n\nasync function touch(db: Query, cartId: string): Promise<void> {\n await db.query('UPDATE shop.carts SET updated_at = now() WHERE id = $1', [cartId])\n}\n\n/**\n * Adds a variant. The join on a PUBLISHED product is the guard: a guessed\n * variant id of a draft adds nothing.\n */\nexport async function addToCart(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 1), 1), 99)\n const { rows } = await db.query<{ variant_id: string }>(\n `INSERT INTO shop.cart_lines (cart_id, variant_id, quantity, unit_price_cents)\n SELECT $1, v.id, $3, coalesce(v.price_cents, p.price_cents)\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.id = $2 AND p.published AND p.deleted_at IS NULL\n ON CONFLICT (cart_id, variant_id)\n DO UPDATE SET quantity = least(shop.cart_lines.quantity + excluded.quantity, 99)\n RETURNING variant_id`,\n [cartId, variantId, qty],\n )\n if (rows.length === 0) throw new CartError(\"Cet article n'est plus en vente.\")\n await touch(db, cartId)\n}\n\n/** A quantity. Zero removes the line — the gesture the screen expects. */\nexport async function setQuantity(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 0), 0), 99)\n if (qty === 0) {\n await db.query('DELETE FROM shop.cart_lines WHERE cart_id = $1 AND variant_id = $2', [\n cartId,\n variantId,\n ])\n } else {\n await db.query(\n 'UPDATE shop.cart_lines SET quantity = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, variantId, qty],\n )\n }\n await touch(db, cartId)\n}\n\nexport async function readCart(\n db: Query,\n cart: { id: string; token: string },\n isNew = false,\n): Promise<PanierEnVitrine> {\n const { rows } = await db.query<{\n variant_id: string\n product_id: string\n name: string\n label: string | null\n unit_price_cents: number\n quantity: number\n available: boolean\n }>(\n `SELECT l.variant_id, v.product_id, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label,\n l.unit_price_cents, l.quantity,\n (p.published AND p.deleted_at IS NULL AND ${SQL_AVAILABLE}) AS available\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.added_at, l.variant_id`,\n [cart.id],\n )\n const { rows: head } = await db.query<{ email: string | null }>(\n 'SELECT email FROM shop.carts WHERE id = $1',\n [cart.id],\n )\n const lignes: LigneDePanier[] = rows.map((l) => ({\n varianteId: l.variant_id,\n produitId: l.product_id,\n nom: l.name,\n declinaison: l.label ?? '',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n disponible: l.available === true,\n visuel: null,\n }))\n return {\n panierId: cart.id,\n jeton: cart.token,\n courriel: head[0]?.email ?? null,\n lignes,\n combien: lignes.reduce((n, l) => n + l.quantite, 0),\n sousTotalCentimes: lignes.reduce((n, l) => n + l.sousTotalCentimes, 0),\n neuf: isNew,\n }\n}\n","/**\n * Checkout: the order lives in the site's database, the MONEY does not.\n *\n * Decided with the founder of Odoro (DECISIONS §43 of the main repository):\n * the payment stays with Odoro (Whop), which knows money and its obligations.\n * This module opens the order, reserves the stock, and hands the payment to a\n * PORT the app wires — Odoro's payment API. The order only keeps the opaque\n * reference the payment returned; it learns the outcome through a SIGNED\n * callback (`confirmPayment`), never from the visitor's browser.\n *\n * The three gestures of the storefront contract:\n *\n * · `ouvrir` — reprices the cart at today's prices (and says what changed),\n * checks and reserves the stock for thirty minutes, writes the order;\n * · `chiffrer` — the total, with the discount code the visitor typed. The\n * code is priced HERE, from the shop's own `discount_codes` (shop 1.2.0,\n * copied from Odoro): the browser only names it. An unknown code is\n * refused by name, never silently ignored;\n * · `payer` — the payment page, from the port.\n *\n * @module\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\nimport type { Base, Query } from './base.js'\nimport { CartError } from './cart.js'\n\n/** A refused checkout gesture: `status` and the sentence the visitor reads. */\nexport class CheckoutError extends Error {\n constructor(\n readonly status: 400 | 404 | 409,\n message: string,\n ) {\n super(message)\n }\n}\n\n/** What the visitor typed, under the storefront contract's names. */\nexport interface CheckoutInput {\n courriel: string\n nom: string\n ligne1: string\n ligne2?: string\n code_postal: string\n ville: string\n pays: string\n telephone?: string\n}\n\n/** Where the money goes: Odoro's payment, wired by the app. */\nexport interface PaymentPort {\n open(input: {\n readonly orderId: string\n readonly number: number\n readonly totalCents: number\n readonly currency: string\n readonly email: string\n }): Promise<{ readonly reference: string; readonly paymentUrl: string }>\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n/** How long a checkout holds the stock. */\nconst RESERVATION_MINUTES = 30\n\nfunction need(value: string | undefined, message: string): string {\n const v = (value ?? '').trim()\n if (v === '') throw new CheckoutError(400, message)\n return v\n}\n\ninterface OrderRow {\n id: string\n number: string | number\n state: string\n email: string\n currency: string\n subtotal_cents: number\n shipping_cents: number\n tax_cents: number\n total_cents: number\n /** Absent on a shop still in 1.1.0 — see `codesInstalled`. */\n discount_code?: string | null\n discount_cents?: number\n payment_ref: string | null\n}\n\n/**\n * Are the discount codes installed (shop 1.2.0)? A shop still in 1.1.0 keeps\n * selling: a code is refused by name there, as before this module served\n * them, and nothing reads a column the shop does not have.\n */\nexport async function codesInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('shop.discount_codes') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport async function quote(db: Query, orderId: string) {\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n const { rows: lines } = await db.query<{\n variant_id: string | null\n product_id: string | null\n product_name: string\n unit_price_cents: number\n quantity: number\n }>(\n `SELECT l.variant_id, v.product_id, l.product_name, l.unit_price_cents, l.quantity\n FROM shop.order_lines l LEFT JOIN shop.product_variants v ON v.id = l.variant_id\n WHERE l.order_id = $1 ORDER BY l.product_name`,\n [orderId],\n )\n return {\n caisseId: order.id,\n devise: order.currency,\n lignes: lines.map((l) => ({\n varianteId: l.variant_id ?? '',\n produitId: l.product_id ?? '',\n nom: l.product_name,\n genre: 'physique',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n taxable: true,\n })),\n remises:\n (order.discount_code ?? null) === null\n ? []\n : [\n {\n remiseId: null,\n code: order.discount_code ?? '',\n libelle: order.discount_code ?? '',\n montantCentimes: Number(order.discount_cents ?? 0),\n },\n ],\n carteCadeau: null,\n methodeDeLivraison: null,\n sousTotalCentimes: Number(order.subtotal_cents),\n remiseCentimes: Number(order.discount_cents ?? 0),\n portCentimes: Number(order.shipping_cents),\n taxeCentimes: Number(order.tax_cents),\n carteCadeauCentimes: 0,\n totalCentimes: Number(order.total_cents),\n taxeIncluse: true,\n }\n}\n\nexport async function openCheckout(\n db: Base,\n cartId: string,\n input: CheckoutInput,\n currency = 'EUR',\n): Promise<{\n orderId: string\n repriced: { varianteId: string; ancienCentimes: number; nouveauCentimes: number }[]\n}> {\n const email = need(input.courriel, 'Indiquez votre adresse e-mail.')\n if (!EMAIL.test(email))\n throw new CheckoutError(400, \"Cette adresse e-mail n'est pas valable.\")\n const name = need(input.nom, 'Indiquez votre nom.')\n const address = {\n ligne1: need(input.ligne1, 'Indiquez votre adresse.'),\n ligne2: (input.ligne2 ?? '').trim(),\n code_postal: need(input.code_postal, 'Indiquez votre code postal.'),\n ville: need(input.ville, 'Indiquez votre ville.'),\n pays: need(input.pays, 'Indiquez votre pays.'),\n telephone: (input.telephone ?? '').trim(),\n }\n\n return await db.transaction(async (tx) => {\n // A checkout reopened on the same cart replaces the previous one: its\n // reservation is released, and only one open order stays.\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee'\n WHERE cart_id = $1 AND state = 'ouverte'`,\n [cartId],\n )\n await tx.query(\n `UPDATE shop.orders SET state = 'annulee'\n WHERE cart_id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [cartId],\n )\n\n const { rows: lines } = await tx.query<{\n variant_id: string\n quantity: number\n unit_price_cents: number\n current_cents: number\n stock: number | null\n for_sale: boolean\n name: string\n label: string | null\n }>(\n `SELECT l.variant_id, l.quantity, l.unit_price_cents,\n coalesce(v.price_cents, p.price_cents) AS current_cents,\n v.stock, (p.published AND p.deleted_at IS NULL) AS for_sale, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.variant_id\n FOR UPDATE OF v`,\n [cartId],\n )\n if (lines.length === 0) throw new CartError('Votre panier est vide.')\n const gone = lines.find((l) => !l.for_sale)\n if (gone !== undefined) {\n throw new CheckoutError(\n 409,\n `« ${gone.name} » n'est plus en vente : retirez-le de votre panier.`,\n )\n }\n\n for (const l of lines) {\n if (l.stock === null) continue\n const { rows } = await tx.query<{ held: number }>(\n `SELECT coalesce(sum(quantity), 0)::integer AS held FROM shop.stock_reservations\n WHERE variant_id = $1 AND state = 'ouverte' AND expires_at > now()`,\n [l.variant_id],\n )\n if (Number(l.stock) - Number(rows[0]?.held ?? 0) < Number(l.quantity)) {\n throw new CheckoutError(409, `« ${l.name} » : il n'en reste pas assez.`)\n }\n }\n\n const repriced = lines\n .filter((l) => Number(l.current_cents) !== Number(l.unit_price_cents))\n .map((l) => ({\n varianteId: l.variant_id,\n ancienCentimes: Number(l.unit_price_cents),\n nouveauCentimes: Number(l.current_cents),\n }))\n for (const r of repriced) {\n await tx.query(\n 'UPDATE shop.cart_lines SET unit_price_cents = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, r.varianteId, r.nouveauCentimes],\n )\n }\n\n const subtotal = lines.reduce(\n (n, l) => n + Number(l.current_cents) * Number(l.quantity),\n 0,\n )\n const { rows: created } = await tx.query<{ id: string }>(\n `INSERT INTO shop.orders (cart_id, email, customer_name, shipping_address, currency, subtotal_cents, total_cents)\n VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING id`,\n [cartId, email, name, JSON.stringify(address), currency, subtotal],\n )\n const orderId = created[0]!.id\n for (const l of lines) {\n await tx.query(\n `INSERT INTO shop.order_lines (order_id, variant_id, product_name, variant_label, unit_price_cents, quantity)\n VALUES ($1, $2, $3, $4, $5, $6)`,\n [orderId, l.variant_id, l.name, l.label ?? '', l.current_cents, l.quantity],\n )\n if (l.stock !== null) {\n await tx.query(\n `INSERT INTO shop.stock_reservations (variant_id, cart_id, quantity, expires_at)\n VALUES ($1, $2, $3, now() + make_interval(mins => $4))`,\n [l.variant_id, cartId, l.quantity, RESERVATION_MINUTES],\n )\n }\n }\n await tx.query('UPDATE shop.carts SET email = $2, updated_at = now() WHERE id = $1', [\n cartId,\n email,\n ])\n return { orderId, repriced }\n })\n}\n\n/**\n * What a code takes off a subtotal. Rounded DOWN: a rounding never gives the\n * buyer a cent the seller did not offer. Never more than the subtotal.\n *\n * A percentage is counted in thousandths of a percent (10000 = 10 %), as Odoro\n * counts it — the codes are copied from there unchanged.\n */\nexport function discountFor(\n kind: 'pourcentage' | 'montant',\n value: number,\n subtotalCents: number,\n): number {\n const raw =\n kind === 'pourcentage' ? Math.floor((subtotalCents * value) / 100_000) : value\n return Math.max(0, Math.min(raw, subtotalCents))\n}\n\nfunction euros(cents: number, currency: string): string {\n return new Intl.NumberFormat('fr', { style: 'currency', currency }).format(cents / 100)\n}\n\n/**\n * The quote with the code the visitor typed — or without, when the field is\n * empty: every `chiffrer` and every `payer` names the code, and the order\n * carries exactly that one.\n *\n * Once the payment is open, the total is frozen: Odoro refuses a payment\n * whose amount changed under the same order. Naming the same code again is\n * fine; another one asks to reopen the checkout.\n */\nexport async function applyCode(db: Query, orderId: string, code: string | undefined) {\n const wanted = (code ?? '').trim()\n if (!(await codesInstalled(db))) {\n if (wanted !== '')\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n return await quote(db, orderId)\n }\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte' || order.payment_ref !== null) {\n if (wanted.toUpperCase() === (order.discount_code ?? '').toUpperCase())\n return await quote(db, orderId)\n throw new CheckoutError(\n 409,\n 'Le paiement de cette commande est déjà ouvert : rouvrez la caisse pour changer de code.',\n )\n }\n\n const subtotal = Number(order.subtotal_cents)\n let discount = 0\n let applied: string | null = null\n if (wanted !== '') {\n const { rows: found } = await db.query<{\n id: string\n code: string\n kind: 'pourcentage' | 'montant'\n value: number\n min_subtotal_cents: number\n max_uses: number | null\n used: number\n }>(\n `SELECT c.id, c.code, c.kind, c.value, c.min_subtotal_cents, c.max_uses,\n (SELECT count(*) FROM shop.discount_uses u WHERE u.discount_id = c.id)::integer AS used\n FROM shop.discount_codes c\n WHERE upper(c.code) = upper($1) AND c.active\n AND (c.starts_at IS NULL OR c.starts_at <= now())\n AND (c.ends_at IS NULL OR c.ends_at > now())`,\n [wanted],\n )\n const found0 = found[0]\n if (found0 === undefined)\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n if (found0.max_uses !== null && Number(found0.used) >= Number(found0.max_uses))\n throw new CheckoutError(409, 'Ce code a déjà servi autant de fois que prévu.')\n if (subtotal < Number(found0.min_subtotal_cents))\n throw new CheckoutError(\n 409,\n `Ce code demande un panier d'au moins ${euros(Number(found0.min_subtotal_cents), order.currency)}.`,\n )\n discount = discountFor(found0.kind, Number(found0.value), subtotal)\n // Nothing is free here: a payment of zero cannot be opened, and a code\n // that erased the whole order would leave the visitor at a dead end.\n if (subtotal - discount + Number(order.shipping_cents) + Number(order.tax_cents) <= 0)\n throw new CheckoutError(409, 'Ce code ne peut pas rendre la commande gratuite.')\n applied = found0.code\n }\n\n await db.query(\n `UPDATE shop.orders\n SET discount_code = $2, discount_cents = $3,\n total_cents = subtotal_cents - $3 + shipping_cents + tax_cents\n WHERE id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [orderId, applied, discount],\n )\n return await quote(db, orderId)\n}\n\nexport async function pay(db: Query, orderId: string, port: PaymentPort) {\n const { rows } = await db.query<OrderRow & { expired: boolean }>(\n `SELECT o.*,\n EXISTS (SELECT 1 FROM shop.stock_reservations r\n WHERE r.cart_id = o.cart_id AND r.state = 'ouverte' AND r.expires_at <= now()) AS expired\n FROM shop.orders o WHERE o.id = $1`,\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte')\n throw new CheckoutError(409, 'Cette commande ne peut plus être payée.')\n if (order.expired)\n throw new CheckoutError(409, 'Votre réservation a expiré : rouvrez la caisse.')\n\n const opened = await port.open({\n orderId: order.id,\n number: Number(order.number),\n totalCents: Number(order.total_cents),\n currency: order.currency,\n email: order.email,\n })\n await db.query('UPDATE shop.orders SET payment_ref = $2 WHERE id = $1', [\n order.id,\n opened.reference,\n ])\n const total = Number(order.total_cents)\n return {\n commandeId: order.id,\n numero: Number(order.number),\n jetonDeSuivi: order.id,\n totalCentimes: total,\n totalTexte: new Intl.NumberFormat('fr', {\n style: 'currency',\n currency: order.currency,\n }).format(total / 100),\n adresseDePaiement: opened.paymentUrl,\n }\n}\n\n/** How long a signed callback stays valid. */\nconst CALLBACK_WINDOW_S = 300\n\n/** The signature of a payment callback: HMAC-SHA256 of `reference.state.timestamp`. */\nexport function signCallback(\n secret: string,\n reference: string,\n state: string,\n timestamp: number,\n): string {\n return createHmac('sha256', secret)\n .update(`${reference}.${state}.${timestamp}`)\n .digest('hex')\n}\n\nexport function verifyCallback(\n secret: string,\n input: { reference: string; etat: string; horodatage: number; signature: string },\n now = Date.now(),\n): boolean {\n if (secret === '' || Math.abs(now / 1000 - input.horodatage) > CALLBACK_WINDOW_S)\n return false\n const expected = Buffer.from(\n signCallback(secret, input.reference, input.etat, input.horodatage),\n )\n const received = Buffer.from(input.signature)\n return expected.length === received.length && timingSafeEqual(expected, received)\n}\n\n/**\n * The payment's outcome, from Odoro. Idempotent: a callback delivered twice\n * changes nothing the second time.\n */\nexport async function confirmPayment(\n db: Base,\n reference: string,\n state: 'payee' | 'echouee',\n): Promise<boolean> {\n return await db.transaction(async (tx) => {\n const { rows } = await tx.query<{\n id: string\n cart_id: string | null\n state: string\n }>('SELECT id, cart_id, state FROM shop.orders WHERE payment_ref = $1 FOR UPDATE', [\n reference,\n ])\n const order = rows[0]\n if (order === undefined) return false\n if (order.state !== 'ouverte') return true\n\n if (state === 'echouee') {\n await tx.query(`UPDATE shop.orders SET state = 'echouee' WHERE id = $1`, [order.id])\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n return true\n }\n\n await tx.query(\n `UPDATE shop.orders SET state = 'payee', paid_at = now() WHERE id = $1`,\n [order.id],\n )\n // A code counts once it is PAID for: an abandoned checkout uses nothing.\n if (await codesInstalled(tx))\n await tx.query(\n `INSERT INTO shop.discount_uses (order_id, discount_id)\n SELECT o.id, c.id FROM shop.orders o\n JOIN shop.discount_codes c ON upper(c.code) = upper(o.discount_code)\n WHERE o.id = $1\n ON CONFLICT (order_id) DO NOTHING`,\n [order.id],\n )\n // The stock leaves when the money arrives, not before.\n await tx.query(\n `UPDATE shop.product_variants v\n SET stock = greatest(v.stock - l.quantity, 0)\n FROM shop.order_lines l\n WHERE l.order_id = $1 AND l.variant_id = v.id AND v.stock IS NOT NULL`,\n [order.id],\n )\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'consommee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n await tx.query(\n `UPDATE shop.carts SET state = 'commande', updated_at = now() WHERE id = $1`,\n [order.cart_id],\n )\n return true\n })\n}\n\n/** An order's state, for the tracking link the buyer received. */\nexport async function orderState(db: Query, orderId: string) {\n if (!/^[0-9a-f-]{36}$/i.test(orderId)) return null\n const { rows } = await db.query<OrderRow & { created_at: Date }>(\n 'SELECT * FROM shop.orders WHERE id = $1',\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) return null\n const { rows: lines } = await db.query<{\n product_name: string\n quantity: number\n unit_price_cents: number\n }>(\n 'SELECT product_name, quantity, unit_price_cents FROM shop.order_lines WHERE order_id = $1 ORDER BY product_name',\n [orderId],\n )\n return {\n numero: Number(order.number),\n etat: order.state,\n courriel: order.email,\n totalCentimes: Number(order.total_cents),\n passeeLe: new Date(order.created_at).toISOString(),\n lignes: lines.map((l) => ({\n nom: l.product_name,\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n })),\n livraisons: [],\n }\n}\n","/**\n * The storefront contract, as an `@odoro-cli/server` module.\n *\n * A site that sells, running in its own container with its own database,\n * mounts this module and serves `/api/storefront/*` itself — the same answers\n * as Odoro's central storefront, so the V4 script (`odoro-boutique.js`) and\n * `@odoro-cli/commerce` talk to it without knowing which one they reach.\n *\n * ## What it serves, and what it does not yet\n *\n * The catalogue, product pages, collections, the cart, checkout (open, quote,\n * pay through Odoro) and order tracking. Customer accounts and discount codes\n * are not served here yet: they answer so, by name — an account request says\n * the account is not available, a code says it is not recognised. A gesture\n * that silently does nothing is worse than one that says no.\n *\n * ## Errors speak the contract\n *\n * The storefront's clients read `erreur`. Every refusal is a problem document\n * (RFC 9457, like the rest of the kernel) that also carries `erreur`.\n *\n * @module\n */\n\nimport {\n ApiError,\n ConflictError,\n NotFoundError,\n ServiceUnavailableError,\n defineModule,\n route,\n type Cookies,\n} from '@odoro-cli/server'\nimport { z } from 'zod'\n\nimport { type Base, shopInstalled } from './base.js'\nimport {\n CART_COOKIE,\n CART_MAX_AGE,\n CartError,\n addToCart,\n findCart,\n openCart,\n readCart,\n setQuantity,\n} from './cart.js'\nimport { readCatalogue, readCollections, readProduct } from './catalogue.js'\nimport {\n CheckoutError,\n applyCode,\n confirmPayment,\n openCheckout,\n orderState,\n pay,\n quote,\n verifyCallback,\n type PaymentPort,\n} from './checkout.js'\n\nexport interface CommerceOptions {\n /** The site's shop database (`@odoro-cli/cloud-connect`, or any `Base`). */\n readonly db: Base\n /** Where the money goes: Odoro's payment. */\n readonly payment: PaymentPort\n /** The secret Odoro signs its payment callbacks with. Empty = callbacks refused. */\n readonly callbackSecret: string\n /** The shop's currency. @defaultValue 'EUR' */\n readonly currency?: string\n /** Mark the cart cookie `Secure`. @defaultValue true */\n readonly secureCookie?: boolean\n}\n\nfunction refusal(\n kind: 'VALIDATION' | 'NOT_FOUND' | 'CONFLICT' | 'UNAVAILABLE',\n message: string,\n): ApiError {\n const extensions = { extensions: { erreur: message } }\n if (kind === 'NOT_FOUND') return new NotFoundError(message, extensions)\n if (kind === 'CONFLICT') return new ConflictError(message, extensions)\n if (kind === 'UNAVAILABLE')\n return new ServiceUnavailableError(message, undefined, extensions)\n return new ApiError('VALIDATION', message, extensions)\n}\n\n/** Domain refusals become contract errors; anything else stays an internal error. */\nasync function speaking<T>(work: () => Promise<T>): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n if (cause instanceof CheckoutError) {\n throw refusal(\n cause.status === 404\n ? 'NOT_FOUND'\n : cause.status === 409\n ? 'CONFLICT'\n : 'VALIDATION',\n cause.message,\n )\n }\n if (cause instanceof CartError) throw refusal('CONFLICT', cause.message)\n throw cause\n }\n}\n\nconst text = z.string().max(200).optional()\nconst count = z.coerce.number().int().min(0).max(100_000).optional()\n\nexport function createCommerceModule(options: CommerceOptions) {\n const { db } = options\n const currency = options.currency ?? 'EUR'\n let installed: Promise<boolean> | null = null\n\n /** Once per process: the `shop` capability, at a version this module reads. */\n async function ready(): Promise<void> {\n installed ??= shopInstalled(db)\n if (!(await installed)) {\n installed = null\n throw refusal('UNAVAILABLE', \"La boutique de ce site n'est pas encore installée.\")\n }\n }\n\n async function cartOf(cookies: Cookies) {\n return await findCart(db, cookies.get(CART_COOKIE))\n }\n\n function keep(cookies: Cookies, token: string): void {\n cookies.set(CART_COOKIE, token, {\n httpOnly: true,\n sameSite: 'lax',\n maxAge: CART_MAX_AGE,\n ...(options.secureCookie === false ? { secure: false } : {}),\n })\n }\n\n const storefront = route({\n name: 'storefront.read',\n method: 'GET',\n path: '/api/storefront',\n auth: 'public',\n input: z.object({\n geste: z\n .enum(['catalogue', 'fiche', 'collections', 'commande', 'suggestions'])\n .default('catalogue'),\n produit: text,\n jeton: text,\n q: text,\n collection: text,\n genre: text,\n prix_min: count,\n prix_max: count,\n disponibles: z.string().optional(),\n tri: z\n .enum(['nouveautes', 'prix_croissant', 'prix_decroissant', 'nom'])\n .optional()\n .catch(undefined),\n combien: count,\n depuis: count,\n }),\n summary:\n 'The catalogue, a product page, the collections or an order, per the storefront contract.',\n handler: async ({ input }) => {\n await ready()\n if (input.geste === 'fiche') {\n const fiche = await readProduct(db, input.produit ?? '')\n if (fiche === null) throw refusal('NOT_FOUND', \"Cet article n'est plus en vente.\")\n return { fiche }\n }\n if (input.geste === 'collections') return { collections: await readCollections(db) }\n if (input.geste === 'commande') {\n const commande = await orderState(db, input.jeton ?? '')\n if (commande === null) throw refusal('NOT_FOUND', \"Cette commande n'existe pas.\")\n return { commande }\n }\n if (input.geste === 'suggestions') {\n const { produits } = await readCatalogue(db, { q: input.q ?? '', combien: 6 })\n return { suggestions: produits.map((p) => ({ id: p.id, nom: p.nom })) }\n }\n return await readCatalogue(db, {\n ...(input.q === undefined ? {} : { q: input.q }),\n ...(input.collection === undefined ? {} : { collection: input.collection }),\n ...(input.genre === undefined ? {} : { genre: input.genre }),\n ...(input.prix_min === undefined ? {} : { prixMin: input.prix_min }),\n ...(input.prix_max === undefined ? {} : { prixMax: input.prix_max }),\n disponibles: input.disponibles === '1',\n ...(input.tri === undefined ? {} : { tri: input.tri }),\n ...(input.combien === undefined ? {} : { combien: input.combien }),\n ...(input.depuis === undefined ? {} : { depuis: input.depuis }),\n })\n },\n })\n\n /** A page view. Nothing is counted or stored here: no tracker on the site. */\n const visit = route({\n name: 'storefront.visit',\n method: 'POST',\n path: '/api/storefront',\n auth: 'public',\n handler: () => ({ ok: true }),\n })\n\n const readCartRoute = route({\n name: 'storefront.cart.read',\n method: 'GET',\n path: '/api/storefront/cart',\n auth: 'public',\n handler: async ({ cookies }) => {\n await ready()\n const cart = await cartOf(cookies)\n if (cart === null) {\n return {\n panier: {\n panierId: '',\n jeton: '',\n courriel: null,\n lignes: [],\n combien: 0,\n sousTotalCentimes: 0,\n neuf: false,\n },\n }\n }\n return { panier: await readCart(db, cart) }\n },\n })\n\n const changeCart = route({\n name: 'storefront.cart.change',\n method: 'POST',\n path: '/api/storefront/cart',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ajouter', 'quantite']).default('ajouter'),\n variante: z.string().regex(/^[0-9a-f-]{36}$/i, \"Cet article n'est plus en vente.\"),\n quantite: z.coerce.number().int().min(0).max(99).default(1),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n const existing = await cartOf(cookies)\n const cart = existing ?? (await openCart(db))\n if (input.geste === 'quantite')\n await setQuantity(db, cart.id, input.variante, input.quantite)\n else await addToCart(db, cart.id, input.variante, input.quantite)\n // The cookie is set on a GESTURE, never on a visit.\n if (existing === null) keep(cookies, cart.token)\n return { panier: await readCart(db, cart, existing === null) }\n }),\n })\n\n const checkout = route({\n name: 'storefront.checkout',\n method: 'POST',\n path: '/api/storefront/checkout',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ouvrir', 'chiffrer', 'payer']),\n caisse: z.string().max(64).optional(),\n code: z.string().max(40).optional(),\n courriel: text,\n nom: text,\n ligne1: text,\n ligne2: text,\n code_postal: z.string().max(20).optional(),\n ville: text,\n pays: z.string().max(40).optional(),\n telephone: z.string().max(30).optional(),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n if (input.geste === 'ouvrir') {\n const cart = await cartOf(cookies)\n if (cart === null) throw new CheckoutError(400, 'Votre panier est vide.')\n const opened = await openCheckout(\n db,\n cart.id,\n {\n courriel: input.courriel ?? '',\n nom: input.nom ?? '',\n ligne1: input.ligne1 ?? '',\n ligne2: input.ligne2 ?? '',\n code_postal: input.code_postal ?? '',\n ville: input.ville ?? '',\n pays: input.pays ?? '',\n telephone: input.telephone ?? '',\n },\n currency,\n )\n return {\n caisse: opened.orderId,\n prixRevalorises: opened.repriced,\n total: await quote(db, opened.orderId),\n }\n }\n const orderId = input.caisse ?? ''\n if (!/^[0-9a-f-]{36}$/i.test(orderId))\n throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (input.geste === 'chiffrer')\n return { total: await applyCode(db, orderId, input.code) }\n await applyCode(db, orderId, input.code)\n return await pay(db, orderId, options.payment)\n }),\n })\n\n /** Odoro tells the payment's outcome. Signed: the browser never decides that an order is paid. */\n const callback = route({\n name: 'storefront.payment.callback',\n method: 'POST',\n path: '/api/storefront/payment-callback',\n auth: 'public',\n input: z.object({\n reference: z.string().min(1).max(200),\n etat: z.enum(['payee', 'echouee']),\n horodatage: z.coerce.number().int(),\n signature: z.string().min(1).max(128),\n }),\n handler: async ({ input }) => {\n await ready()\n if (!verifyCallback(options.callbackSecret, input)) {\n throw refusal('NOT_FOUND', 'Rappel de paiement refusé.')\n }\n const known = await confirmPayment(db, input.reference, input.etat)\n if (!known) throw refusal('NOT_FOUND', 'Paiement inconnu.')\n return { ok: true }\n },\n })\n\n /** Not served by this module yet — said by name, never a dead button. */\n const accountRead = route({\n name: 'storefront.account.read',\n method: 'GET',\n path: '/api/storefront/account',\n auth: 'public',\n handler: () => ({ connecte: false }),\n })\n const accountWrite = route({\n name: 'storefront.account.write',\n method: 'POST',\n path: '/api/storefront/account',\n auth: 'public',\n handler: () => {\n throw refusal(\n 'UNAVAILABLE',\n \"Le compte client n'est pas encore disponible sur cette boutique.\",\n )\n },\n })\n\n return defineModule({\n name: 'commerce',\n routes: [\n storefront,\n visit,\n readCartRoute,\n changeCart,\n checkout,\n callback,\n accountRead,\n accountWrite,\n ] as never,\n })\n}\n","/**\n * The payment, opened at Odoro.\n *\n * A site that sells never holds a payment provider's key: it asks Odoro to\n * open the payment, and Odoro tells it the outcome through the signed callback\n * (`/api/storefront/payment-callback`). The site's secret is its own — shared\n * with Odoro, with no other site — so a compromised site can open payments\n * for itself and nothing else.\n *\n * ## The request's signature\n *\n * `x-odoro-signature` is the hex HMAC-SHA256 of `${timestamp}.${body}`, and\n * `x-odoro-timestamp` the Unix time in seconds. Odoro refuses a signature more\n * than five minutes old: a request copied from a log cannot be replayed.\n *\n * @module\n */\n\nimport { createHmac } from 'node:crypto'\n\nimport { CheckoutError, type PaymentPort } from './checkout.js'\n\nexport interface OdoroPaymentOptions {\n /** Odoro's address, e.g. `https://odoro.ai`. */\n readonly origin: string\n /** This site's identifier at Odoro. */\n readonly site: string\n /** This site's secret, shared with Odoro alone. */\n readonly secret: string\n /** @internal For the tests. */\n readonly fetch?: typeof fetch\n /** @internal For the tests: seconds since the epoch. */\n readonly now?: () => number\n}\n\n/** Where Odoro opens a site's payment. */\nexport const ODORO_PAYMENT_PATH = '/api/payment/site-order'\n\nexport function signRequest(secret: string, timestamp: number, body: string): string {\n return createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')\n}\n\nconst UNAVAILABLE = \"Le paiement n'a pas pu s'ouvrir. Réessayez dans un instant.\"\n\nexport function odoroPayment(options: OdoroPaymentOptions): PaymentPort {\n const send = options.fetch ?? fetch\n const now = options.now ?? (() => Math.floor(Date.now() / 1000))\n const address = new URL(ODORO_PAYMENT_PATH, options.origin).toString()\n\n return {\n open: async (order) => {\n if (options.secret === '') throw new CheckoutError(409, UNAVAILABLE)\n const body = JSON.stringify({\n site: options.site,\n commande: order.orderId,\n numero: order.number,\n totalCentimes: order.totalCents,\n devise: order.currency,\n courriel: order.email,\n })\n const timestamp = now()\n let reply: Response\n try {\n reply = await send(address, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-odoro-timestamp': String(timestamp),\n 'x-odoro-signature': signRequest(options.secret, timestamp, body),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n })\n } catch {\n throw new CheckoutError(409, UNAVAILABLE)\n }\n const payload = (await reply.json().catch(() => ({}))) as {\n reference?: unknown\n adresse?: unknown\n erreur?: unknown\n }\n if (\n !reply.ok ||\n typeof payload.reference !== 'string' ||\n typeof payload.adresse !== 'string'\n ) {\n // Odoro's refusal is written for the buyer (\"the shop is not open yet\"):\n // it is passed on as it is. Anything else says to try again.\n const said =\n typeof payload.erreur === 'string' && reply.status < 500\n ? payload.erreur\n : UNAVAILABLE\n throw new CheckoutError(409, said)\n }\n return { reference: payload.reference, paymentUrl: payload.adresse }\n },\n }\n}\n","/**\n * A `Base` from a `pg` pool — without depending on `pg`.\n *\n * The shape is structural: any pool whose `query` and `connect` look like\n * node-postgres works, and the site keeps the driver version it chose.\n *\n * A transaction takes ONE client for its whole length: `BEGIN` on one\n * connection and `COMMIT` on another would commit nothing, silently.\n *\n * @module\n */\n\nimport type { Base, Query } from './base.js'\n\ninterface PgResult {\n rows: unknown[]\n}\n\nexport interface PgClientLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n release(): void\n}\n\nexport interface PgPoolLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n connect(): Promise<PgClientLike>\n}\n\nfunction reader(\n run: (text: string, values?: readonly unknown[]) => Promise<PgResult>,\n): Query {\n return {\n query: async <Row extends object>(text: string, values?: readonly unknown[]) => {\n const { rows } = await run(text, values)\n return { rows: rows as Row[] }\n },\n }\n}\n\nexport function baseFromPool(pool: PgPoolLike): Base {\n return {\n ...reader((text, values) => pool.query(text, values)),\n transaction: async (work) => {\n const client = await pool.connect()\n try {\n await client.query('BEGIN')\n const result = await work(reader((text, values) => client.query(text, values)))\n await client.query('COMMIT')\n return result\n } catch (cause) {\n await client.query('ROLLBACK').catch(() => undefined)\n throw cause\n } finally {\n client.release()\n }\n },\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/base.ts","../src/images.ts","../src/catalogue.ts","../src/cart.ts","../src/checkout.ts","../src/account.ts","../src/module.ts","../src/odoro-payment.ts","../src/odoro-mail.ts","../src/pg.ts"],"names":["text","count","address","TOKEN","EMAIL","randomBytes","createHmac"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * What the commerce module needs from a database — and nothing more.\n *\n * `query` and `transaction`, the shape `@odoro-cli/cloud-connect` already has:\n * the app wires its connection, and the tests a plain `pg` pool. The module\n * never opens a connection itself, and never reads a connection string: that\n * belongs to the app's configuration.\n *\n * @module\n */\n\n/** Something that runs SQL: a connection, or a transaction. */\nexport interface Query {\n query<Row extends object = Record<string, unknown>>(\n text: string,\n values?: readonly unknown[],\n ): Promise<{ readonly rows: readonly Row[] }>\n}\n\n/** A database the module can also run a transaction on. */\nexport interface Base extends Query {\n transaction<T>(work: (tx: Query) => Promise<T>): Promise<T>\n}\n\n/** The capability version this module speaks. A `shop` 2.x would not be read. */\nexport const SHOP_MAJOR = 1\n\n/**\n * Is the `shop` capability installed, at a version this module reads?\n *\n * Read in the database's own registry (`odoro.features`), which the capability\n * manager writes. Checked once per process: a capability is not uninstalled\n * under a running site.\n */\nexport async function shopInstalled(base: Query): Promise<boolean> {\n try {\n const { rows } = await base.query<{ version: string }>(\n `SELECT version FROM odoro.features WHERE name = 'shop' AND active`,\n )\n const version = rows[0]?.version\n return (\n version !== undefined &&\n Number.parseInt(version.split('.')[0] ?? '', 10) === SHOP_MAJOR\n )\n } catch {\n return false\n }\n}\n","/**\n * Product images, read from the site's own storage (capability `storage`,\n * DECISIONS §44 of the Odoro repository).\n *\n * `shop.product_images.storage_key` names an object of `storage.objects`. The\n * storefront gives an image two ways, as Odoro's central storefront does:\n * inline in `visuel` (base64, so a card renders without a second request),\n * and as bytes for `geste=image` (so a page points at a URL that follows the\n * merchant's latest photo without republishing).\n *\n * A site whose database has no `storage` capability yet keeps working: its\n * images are `null`, as before. That is checked once per process — a\n * capability is not uninstalled under a running site.\n *\n * @module\n */\n\nimport type { Query } from './base.js'\n\n/** SVG can carry script: it is never served, whatever the database says. */\nconst SERVED_TYPES = new Set([\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/avif',\n 'image/gif',\n])\n\nexport async function storageInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('storage.objects') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport interface StoredImage {\n readonly bytes: Buffer\n readonly type: string\n}\n\n/**\n * The first image of a product FOR SALE, as bytes. `null` for a draft, an\n * unknown id, a product without image, or a type that is never served.\n */\nexport async function productImage(\n db: Query,\n productId: string,\n): Promise<StoredImage | null> {\n if (!/^[0-9a-f-]{36}$/i.test(productId)) return null\n if (!(await storageInstalled(db))) return null\n const { rows } = await db.query<{ bytes: Buffer; mime_type: string }>(\n `SELECT o.bytes, o.mime_type\n FROM shop.product_images i\n JOIN shop.products p ON p.id = i.product_id\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL\n ORDER BY i.position, i.id\n LIMIT 1`,\n [productId],\n )\n const row = rows[0]\n if (row === undefined || !SERVED_TYPES.has(row.mime_type)) return null\n return { bytes: row.bytes, type: row.mime_type }\n}\n\n/** The SQL that yields a product's images as base64, when storage exists. */\nexport const SQL_IMAGES = `\n SELECT encode(o.bytes, 'base64') AS contenu, o.mime_type AS type, i.alt_text AS texte\n FROM shop.product_images i\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE i.product_id = p.id AND o.mime_type <> 'image/svg+xml'\n ORDER BY i.position, i.id`\n","/**\n * The catalogue, read from the `shop` schema in the storefront's shapes.\n *\n * Only what is FOR SALE: published, not deleted. A draft is the merchant's\n * business; a visitor who guesses its id gets the same 404 as for a product\n * that never existed.\n *\n * Availability is per VARIANT — the stock lives there — minus the open,\n * unexpired reservations of carts being paid. An expired reservation holds\n * nothing: an abandoned checkout must not take an item off sale forever.\n *\n * Images: `shop.product_images` names objects of the site's `storage`\n * capability (see `images.ts`). They come inline, base64, as in Odoro's\n * central storefront. A database without `storage` keeps `visuel: null`.\n *\n * @module\n */\n\nimport type {\n CatalogueQuery,\n CollectionEnVitrine,\n FicheProduit,\n ProduitEnVitrine,\n VarianteEnVitrine,\n} from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_IMAGES, storageInstalled } from './images.js'\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** A variant is available when its stock, minus what is being paid, covers one. */\nexport const SQL_AVAILABLE = `\n (v.stock IS NULL OR v.stock - coalesce((\n SELECT sum(r.quantity)::integer FROM shop.stock_reservations r\n WHERE r.variant_id = v.id AND r.state = 'ouverte' AND r.expires_at > now()\n ), 0) >= 1)`\n\n/** The sort orders a visitor may ask for: a closed list, never a concatenated `ORDER BY`. */\nconst ORDERS: Readonly<Record<NonNullable<CatalogueQuery['tri']>, string>> = {\n nouveautes: 'p.created_at DESC, p.name',\n prix_croissant: 'p.price_cents ASC, p.name',\n prix_decroissant: 'p.price_cents DESC, p.name',\n nom: 'p.name ASC',\n}\n\ninterface ProductRow {\n id: string\n name: string\n description: string\n kind: string\n price_cents: number\n compare_at_cents: number | null\n available: boolean\n /** The first image, when the database has `storage`. */\n visuel?: { contenu: string; type: string; texte: string } | null\n}\n\n/** The first image of each product, as a SELECT column — or a plain null. */\nfunction colonneDuVisuel(avecStockage: boolean): string {\n return avecStockage\n ? `(SELECT json_build_object('contenu', im.contenu, 'type', im.type, 'texte', im.texte)\n FROM (${SQL_IMAGES} LIMIT 1) im) AS visuel`\n : 'NULL::json AS visuel'\n}\n\nfunction toProduct(r: ProductRow): ProduitEnVitrine {\n return {\n id: r.id,\n nom: r.name,\n description: r.description,\n genre: r.kind,\n prixCentimes: Number(r.price_cents),\n prixBarreCentimes: r.compare_at_cents === null ? null : Number(r.compare_at_cents),\n disponible: r.available === true,\n visuel: r.visuel ?? null,\n etiquettes: [],\n prixUnitaire: null,\n }\n}\n\nexport async function readCatalogue(\n db: Query,\n q: CatalogueQuery = {},\n): Promise<{ produits: ProduitEnVitrine[]; total: number }> {\n const text = (q.q ?? '').trim()\n const values: unknown[] = [\n text === '' ? null : `%${text.toLowerCase()}%`,\n q.collection !== undefined && UUID.test(q.collection) ? q.collection : null,\n q.genre ?? null,\n q.prixMin ?? null,\n q.prixMax ?? null,\n q.disponibles === true,\n ]\n const where = `\n FROM shop.products p\n WHERE p.published AND p.deleted_at IS NULL\n AND ($1::text IS NULL OR lower(p.name) LIKE $1 OR lower(p.description) LIKE $1)\n AND ($2::uuid IS NULL OR EXISTS (\n SELECT 1 FROM shop.collection_products cp\n JOIN shop.collections c ON c.id = cp.collection_id AND c.published\n WHERE cp.collection_id = $2 AND cp.product_id = p.id))\n AND ($3::text IS NULL OR p.kind = $3)\n AND ($4::integer IS NULL OR p.price_cents >= $4)\n AND ($5::integer IS NULL OR p.price_cents <= $5)\n AND (NOT $6::boolean OR EXISTS (\n SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}))`\n // An unknown order falls back to the merchant's, silently: it is a sort, not an error.\n const order = (q.tri === undefined ? undefined : ORDERS[q.tri]) ?? 'p.position, p.name'\n const limit = Math.min(Math.max(q.combien ?? 24, 1), 60)\n const offset = Math.max(q.depuis ?? 0, 0)\n\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n ${where}\n ORDER BY ${order}\n LIMIT $7 OFFSET $8`,\n [...values, limit, offset],\n )\n const { rows: count } = await db.query<{ n: number }>(\n `SELECT count(*)::integer AS n ${where}`,\n values,\n )\n return { produits: rows.map(toProduct), total: Number(count[0]?.n ?? 0) }\n}\n\nexport async function readProduct(db: Query, id: string): Promise<FicheProduit | null> {\n if (!UUID.test(id)) return null\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n FROM shop.products p\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL`,\n [id],\n )\n const row = rows[0]\n if (row === undefined) return null\n\n const { rows: options } = await db.query<{\n id: string\n name: string\n values: string[] | null\n }>(\n `SELECT o.id, o.name,\n array_agg(DISTINCT vv.value) FILTER (WHERE vv.value IS NOT NULL) AS values\n FROM shop.product_options o\n LEFT JOIN shop.product_variant_values vv ON vv.option_id = o.id\n WHERE o.product_id = $1\n GROUP BY o.id, o.name, o.position\n ORDER BY o.position, o.name`,\n [id],\n )\n const { rows: variants } = await db.query<{\n id: string\n price_cents: number\n available: boolean\n choices: { option: string; valeur: string }[] | null\n }>(\n `SELECT v.id,\n coalesce(v.price_cents, p.price_cents) AS price_cents,\n ${SQL_AVAILABLE} AS available,\n (SELECT json_agg(json_build_object('option', o.name, 'valeur', vv.value) ORDER BY o.position, o.name)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS choices\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.product_id = $1\n ORDER BY v.position, v.created_at`,\n [id],\n )\n // Every image of the product, in order, for the product page.\n const visuels = avecStockage\n ? (\n await db.query<{ contenu: string; type: string; texte: string }>(\n `SELECT im.contenu, im.type, im.texte FROM (${SQL_IMAGES}) im`.replace(\n 'p.id',\n '$1',\n ),\n [id],\n )\n ).rows.map((v) => ({ contenu: v.contenu, type: v.type, texte: v.texte }))\n : []\n const product = toProduct(row)\n const variantes: VarianteEnVitrine[] = variants.map((v) => {\n const choix = v.choices ?? []\n const price = Number(v.price_cents)\n return {\n id: v.id,\n libelle:\n choix.length === 0\n ? row.name\n : choix.map((c) => `${c.option} : ${c.valeur}`).join(' / '),\n choix,\n prixCentimes: price,\n prixBarreCentimes:\n row.compare_at_cents !== null && row.compare_at_cents > price\n ? Number(row.compare_at_cents)\n : null,\n disponible: v.available === true,\n visuelRang: null,\n prixUnitaire: null,\n }\n })\n return {\n ...product,\n seoTitre: row.name,\n seoResume: row.description,\n visuels,\n options: options.map((o) => ({ id: o.id, nom: o.name, valeurs: o.values ?? [] })),\n variantes,\n }\n}\n\nexport async function readCollections(db: Query): Promise<CollectionEnVitrine[]> {\n const { rows } = await db.query<{\n id: string\n name: string\n description: string\n n: number\n }>(\n `SELECT c.id, c.name, c.description,\n (SELECT count(*)::integer FROM shop.collection_products cp\n JOIN shop.products p ON p.id = cp.product_id\n WHERE cp.collection_id = c.id AND p.published AND p.deleted_at IS NULL) AS n\n FROM shop.collections c\n WHERE c.published\n ORDER BY c.name`,\n )\n return rows.map((c) => ({\n id: c.id,\n nom: c.name,\n description: c.description,\n combien: Number(c.n),\n }))\n}\n","/**\n * The cart, found by its token.\n *\n * The token travels in an `HttpOnly` cookie: no script of the page reads it,\n * so no third-party script injected into a site can copy it elsewhere. It is\n * set on a GESTURE (an add), never on a visit — a site that sets a cookie on\n * every page view would need a consent banner for it.\n *\n * The unit price is recorded at the add: checkout compares it with the price of\n * the day, and says so when it changed.\n *\n * @module\n */\n\nimport { randomBytes } from 'node:crypto'\n\nimport type { LigneDePanier, PanierEnVitrine } from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_AVAILABLE } from './catalogue.js'\n\nexport const CART_COOKIE = 'odoro_panier'\n/** Thirty days: the window of an abandoned-cart reminder, no longer. */\nexport const CART_MAX_AGE = 60 * 60 * 24 * 30\n\nconst TOKEN = /^vitrine-[0-9a-f]{48}$/\n\nexport function isCartToken(value: string | undefined): value is string {\n return value !== undefined && TOKEN.test(value)\n}\n\nexport function newCartToken(): string {\n return `vitrine-${randomBytes(24).toString('hex')}`\n}\n\n/** Refused gestures carry the sentence the visitor reads. */\nexport class CartError extends Error {}\n\nexport async function findCart(\n db: Query,\n token: string | undefined,\n): Promise<{ id: string; token: string } | null> {\n if (!isCartToken(token)) return null\n const { rows } = await db.query<{ id: string }>(\n `SELECT id FROM shop.carts WHERE token = $1 AND state = 'ouvert' AND NOT preview`,\n [token],\n )\n const row = rows[0]\n return row === undefined ? null : { id: row.id, token }\n}\n\nexport async function openCart(db: Query): Promise<{ id: string; token: string }> {\n const token = newCartToken()\n const { rows } = await db.query<{ id: string }>(\n `INSERT INTO shop.carts (token) VALUES ($1) RETURNING id`,\n [token],\n )\n return { id: rows[0]!.id, token }\n}\n\nasync function touch(db: Query, cartId: string): Promise<void> {\n await db.query('UPDATE shop.carts SET updated_at = now() WHERE id = $1', [cartId])\n}\n\n/**\n * Adds a variant. The join on a PUBLISHED product is the guard: a guessed\n * variant id of a draft adds nothing.\n */\nexport async function addToCart(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 1), 1), 99)\n const { rows } = await db.query<{ variant_id: string }>(\n `INSERT INTO shop.cart_lines (cart_id, variant_id, quantity, unit_price_cents)\n SELECT $1, v.id, $3, coalesce(v.price_cents, p.price_cents)\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.id = $2 AND p.published AND p.deleted_at IS NULL\n ON CONFLICT (cart_id, variant_id)\n DO UPDATE SET quantity = least(shop.cart_lines.quantity + excluded.quantity, 99)\n RETURNING variant_id`,\n [cartId, variantId, qty],\n )\n if (rows.length === 0) throw new CartError(\"Cet article n'est plus en vente.\")\n await touch(db, cartId)\n}\n\n/** A quantity. Zero removes the line — the gesture the screen expects. */\nexport async function setQuantity(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 0), 0), 99)\n if (qty === 0) {\n await db.query('DELETE FROM shop.cart_lines WHERE cart_id = $1 AND variant_id = $2', [\n cartId,\n variantId,\n ])\n } else {\n await db.query(\n 'UPDATE shop.cart_lines SET quantity = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, variantId, qty],\n )\n }\n await touch(db, cartId)\n}\n\nexport async function readCart(\n db: Query,\n cart: { id: string; token: string },\n isNew = false,\n): Promise<PanierEnVitrine> {\n const { rows } = await db.query<{\n variant_id: string\n product_id: string\n name: string\n label: string | null\n unit_price_cents: number\n quantity: number\n available: boolean\n }>(\n `SELECT l.variant_id, v.product_id, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label,\n l.unit_price_cents, l.quantity,\n (p.published AND p.deleted_at IS NULL AND ${SQL_AVAILABLE}) AS available\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.added_at, l.variant_id`,\n [cart.id],\n )\n const { rows: head } = await db.query<{ email: string | null }>(\n 'SELECT email FROM shop.carts WHERE id = $1',\n [cart.id],\n )\n const lignes: LigneDePanier[] = rows.map((l) => ({\n varianteId: l.variant_id,\n produitId: l.product_id,\n nom: l.name,\n declinaison: l.label ?? '',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n disponible: l.available === true,\n visuel: null,\n }))\n return {\n panierId: cart.id,\n jeton: cart.token,\n courriel: head[0]?.email ?? null,\n lignes,\n combien: lignes.reduce((n, l) => n + l.quantite, 0),\n sousTotalCentimes: lignes.reduce((n, l) => n + l.sousTotalCentimes, 0),\n neuf: isNew,\n }\n}\n","/**\n * Checkout: the order lives in the site's database, the MONEY does not.\n *\n * Decided with the founder of Odoro (DECISIONS §43 of the main repository):\n * the payment stays with Odoro (Whop), which knows money and its obligations.\n * This module opens the order, reserves the stock, and hands the payment to a\n * PORT the app wires — Odoro's payment API. The order only keeps the opaque\n * reference the payment returned; it learns the outcome through a SIGNED\n * callback (`confirmPayment`), never from the visitor's browser.\n *\n * The three gestures of the storefront contract:\n *\n * · `ouvrir` — reprices the cart at today's prices (and says what changed),\n * checks and reserves the stock for thirty minutes, writes the order;\n * · `chiffrer` — the total, with the discount code the visitor typed. The\n * code is priced HERE, from the shop's own `discount_codes` (shop 1.2.0,\n * copied from Odoro): the browser only names it. An unknown code is\n * refused by name, never silently ignored;\n * · `payer` — the payment page, from the port.\n *\n * @module\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\nimport type { Base, Query } from './base.js'\nimport { CartError } from './cart.js'\n\n/** A refused checkout gesture: `status` and the sentence the visitor reads. */\nexport class CheckoutError extends Error {\n constructor(\n readonly status: 400 | 404 | 409,\n message: string,\n ) {\n super(message)\n }\n}\n\n/** What the visitor typed, under the storefront contract's names. */\nexport interface CheckoutInput {\n courriel: string\n nom: string\n ligne1: string\n ligne2?: string\n code_postal: string\n ville: string\n pays: string\n telephone?: string\n}\n\n/** Where the money goes: Odoro's payment, wired by the app. */\nexport interface PaymentPort {\n open(input: {\n readonly orderId: string\n readonly number: number\n readonly totalCents: number\n readonly currency: string\n readonly email: string\n }): Promise<{ readonly reference: string; readonly paymentUrl: string }>\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n/** How long a checkout holds the stock. */\nconst RESERVATION_MINUTES = 30\n\nfunction need(value: string | undefined, message: string): string {\n const v = (value ?? '').trim()\n if (v === '') throw new CheckoutError(400, message)\n return v\n}\n\ninterface OrderRow {\n id: string\n number: string | number\n state: string\n email: string\n currency: string\n subtotal_cents: number\n shipping_cents: number\n tax_cents: number\n total_cents: number\n /** Absent on a shop still in 1.1.0 — see `codesInstalled`. */\n discount_code?: string | null\n discount_cents?: number\n payment_ref: string | null\n}\n\n/**\n * Are the discount codes installed (shop 1.2.0)? A shop still in 1.1.0 keeps\n * selling: a code is refused by name there, as before this module served\n * them, and nothing reads a column the shop does not have.\n */\nexport async function codesInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('shop.discount_codes') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport async function quote(db: Query, orderId: string) {\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n const { rows: lines } = await db.query<{\n variant_id: string | null\n product_id: string | null\n product_name: string\n unit_price_cents: number\n quantity: number\n }>(\n `SELECT l.variant_id, v.product_id, l.product_name, l.unit_price_cents, l.quantity\n FROM shop.order_lines l LEFT JOIN shop.product_variants v ON v.id = l.variant_id\n WHERE l.order_id = $1 ORDER BY l.product_name`,\n [orderId],\n )\n return {\n caisseId: order.id,\n devise: order.currency,\n lignes: lines.map((l) => ({\n varianteId: l.variant_id ?? '',\n produitId: l.product_id ?? '',\n nom: l.product_name,\n genre: 'physique',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n taxable: true,\n })),\n remises:\n (order.discount_code ?? null) === null\n ? []\n : [\n {\n remiseId: null,\n code: order.discount_code ?? '',\n libelle: order.discount_code ?? '',\n montantCentimes: Number(order.discount_cents ?? 0),\n },\n ],\n carteCadeau: null,\n methodeDeLivraison: null,\n sousTotalCentimes: Number(order.subtotal_cents),\n remiseCentimes: Number(order.discount_cents ?? 0),\n portCentimes: Number(order.shipping_cents),\n taxeCentimes: Number(order.tax_cents),\n carteCadeauCentimes: 0,\n totalCentimes: Number(order.total_cents),\n taxeIncluse: true,\n }\n}\n\nexport async function openCheckout(\n db: Base,\n cartId: string,\n input: CheckoutInput,\n currency = 'EUR',\n): Promise<{\n orderId: string\n repriced: { varianteId: string; ancienCentimes: number; nouveauCentimes: number }[]\n}> {\n const email = need(input.courriel, 'Indiquez votre adresse e-mail.')\n if (!EMAIL.test(email))\n throw new CheckoutError(400, \"Cette adresse e-mail n'est pas valable.\")\n const name = need(input.nom, 'Indiquez votre nom.')\n const address = {\n ligne1: need(input.ligne1, 'Indiquez votre adresse.'),\n ligne2: (input.ligne2 ?? '').trim(),\n code_postal: need(input.code_postal, 'Indiquez votre code postal.'),\n ville: need(input.ville, 'Indiquez votre ville.'),\n pays: need(input.pays, 'Indiquez votre pays.'),\n telephone: (input.telephone ?? '').trim(),\n }\n\n return await db.transaction(async (tx) => {\n // A checkout reopened on the same cart replaces the previous one: its\n // reservation is released, and only one open order stays.\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee'\n WHERE cart_id = $1 AND state = 'ouverte'`,\n [cartId],\n )\n await tx.query(\n `UPDATE shop.orders SET state = 'annulee'\n WHERE cart_id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [cartId],\n )\n\n const { rows: lines } = await tx.query<{\n variant_id: string\n quantity: number\n unit_price_cents: number\n current_cents: number\n stock: number | null\n for_sale: boolean\n name: string\n label: string | null\n }>(\n `SELECT l.variant_id, l.quantity, l.unit_price_cents,\n coalesce(v.price_cents, p.price_cents) AS current_cents,\n v.stock, (p.published AND p.deleted_at IS NULL) AS for_sale, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.variant_id\n FOR UPDATE OF v`,\n [cartId],\n )\n if (lines.length === 0) throw new CartError('Votre panier est vide.')\n const gone = lines.find((l) => !l.for_sale)\n if (gone !== undefined) {\n throw new CheckoutError(\n 409,\n `« ${gone.name} » n'est plus en vente : retirez-le de votre panier.`,\n )\n }\n\n for (const l of lines) {\n if (l.stock === null) continue\n const { rows } = await tx.query<{ held: number }>(\n `SELECT coalesce(sum(quantity), 0)::integer AS held FROM shop.stock_reservations\n WHERE variant_id = $1 AND state = 'ouverte' AND expires_at > now()`,\n [l.variant_id],\n )\n if (Number(l.stock) - Number(rows[0]?.held ?? 0) < Number(l.quantity)) {\n throw new CheckoutError(409, `« ${l.name} » : il n'en reste pas assez.`)\n }\n }\n\n const repriced = lines\n .filter((l) => Number(l.current_cents) !== Number(l.unit_price_cents))\n .map((l) => ({\n varianteId: l.variant_id,\n ancienCentimes: Number(l.unit_price_cents),\n nouveauCentimes: Number(l.current_cents),\n }))\n for (const r of repriced) {\n await tx.query(\n 'UPDATE shop.cart_lines SET unit_price_cents = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, r.varianteId, r.nouveauCentimes],\n )\n }\n\n const subtotal = lines.reduce(\n (n, l) => n + Number(l.current_cents) * Number(l.quantity),\n 0,\n )\n const { rows: created } = await tx.query<{ id: string }>(\n `INSERT INTO shop.orders (cart_id, email, customer_name, shipping_address, currency, subtotal_cents, total_cents)\n VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING id`,\n [cartId, email, name, JSON.stringify(address), currency, subtotal],\n )\n const orderId = created[0]!.id\n for (const l of lines) {\n await tx.query(\n `INSERT INTO shop.order_lines (order_id, variant_id, product_name, variant_label, unit_price_cents, quantity)\n VALUES ($1, $2, $3, $4, $5, $6)`,\n [orderId, l.variant_id, l.name, l.label ?? '', l.current_cents, l.quantity],\n )\n if (l.stock !== null) {\n await tx.query(\n `INSERT INTO shop.stock_reservations (variant_id, cart_id, quantity, expires_at)\n VALUES ($1, $2, $3, now() + make_interval(mins => $4))`,\n [l.variant_id, cartId, l.quantity, RESERVATION_MINUTES],\n )\n }\n }\n await tx.query('UPDATE shop.carts SET email = $2, updated_at = now() WHERE id = $1', [\n cartId,\n email,\n ])\n return { orderId, repriced }\n })\n}\n\n/**\n * What a code takes off a subtotal. Rounded DOWN: a rounding never gives the\n * buyer a cent the seller did not offer. Never more than the subtotal.\n *\n * A percentage is counted in thousandths of a percent (10000 = 10 %), as Odoro\n * counts it — the codes are copied from there unchanged.\n */\nexport function discountFor(\n kind: 'pourcentage' | 'montant',\n value: number,\n subtotalCents: number,\n): number {\n const raw =\n kind === 'pourcentage' ? Math.floor((subtotalCents * value) / 100_000) : value\n return Math.max(0, Math.min(raw, subtotalCents))\n}\n\nfunction euros(cents: number, currency: string): string {\n return new Intl.NumberFormat('fr', { style: 'currency', currency }).format(cents / 100)\n}\n\n/**\n * The quote with the code the visitor typed — or without, when the field is\n * empty: every `chiffrer` and every `payer` names the code, and the order\n * carries exactly that one.\n *\n * Once the payment is open, the total is frozen: Odoro refuses a payment\n * whose amount changed under the same order. Naming the same code again is\n * fine; another one asks to reopen the checkout.\n */\nexport async function applyCode(db: Query, orderId: string, code: string | undefined) {\n const wanted = (code ?? '').trim()\n if (!(await codesInstalled(db))) {\n if (wanted !== '')\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n return await quote(db, orderId)\n }\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte' || order.payment_ref !== null) {\n if (wanted.toUpperCase() === (order.discount_code ?? '').toUpperCase())\n return await quote(db, orderId)\n throw new CheckoutError(\n 409,\n 'Le paiement de cette commande est déjà ouvert : rouvrez la caisse pour changer de code.',\n )\n }\n\n const subtotal = Number(order.subtotal_cents)\n let discount = 0\n let applied: string | null = null\n if (wanted !== '') {\n const { rows: found } = await db.query<{\n id: string\n code: string\n kind: 'pourcentage' | 'montant'\n value: number\n min_subtotal_cents: number\n max_uses: number | null\n used: number\n }>(\n `SELECT c.id, c.code, c.kind, c.value, c.min_subtotal_cents, c.max_uses,\n (SELECT count(*) FROM shop.discount_uses u WHERE u.discount_id = c.id)::integer AS used\n FROM shop.discount_codes c\n WHERE upper(c.code) = upper($1) AND c.active\n AND (c.starts_at IS NULL OR c.starts_at <= now())\n AND (c.ends_at IS NULL OR c.ends_at > now())`,\n [wanted],\n )\n const found0 = found[0]\n if (found0 === undefined)\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n if (found0.max_uses !== null && Number(found0.used) >= Number(found0.max_uses))\n throw new CheckoutError(409, 'Ce code a déjà servi autant de fois que prévu.')\n if (subtotal < Number(found0.min_subtotal_cents))\n throw new CheckoutError(\n 409,\n `Ce code demande un panier d'au moins ${euros(Number(found0.min_subtotal_cents), order.currency)}.`,\n )\n discount = discountFor(found0.kind, Number(found0.value), subtotal)\n // Nothing is free here: a payment of zero cannot be opened, and a code\n // that erased the whole order would leave the visitor at a dead end.\n if (subtotal - discount + Number(order.shipping_cents) + Number(order.tax_cents) <= 0)\n throw new CheckoutError(409, 'Ce code ne peut pas rendre la commande gratuite.')\n applied = found0.code\n }\n\n await db.query(\n `UPDATE shop.orders\n SET discount_code = $2, discount_cents = $3,\n total_cents = subtotal_cents - $3 + shipping_cents + tax_cents\n WHERE id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [orderId, applied, discount],\n )\n return await quote(db, orderId)\n}\n\nexport async function pay(db: Query, orderId: string, port: PaymentPort) {\n const { rows } = await db.query<OrderRow & { expired: boolean }>(\n `SELECT o.*,\n EXISTS (SELECT 1 FROM shop.stock_reservations r\n WHERE r.cart_id = o.cart_id AND r.state = 'ouverte' AND r.expires_at <= now()) AS expired\n FROM shop.orders o WHERE o.id = $1`,\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte')\n throw new CheckoutError(409, 'Cette commande ne peut plus être payée.')\n if (order.expired)\n throw new CheckoutError(409, 'Votre réservation a expiré : rouvrez la caisse.')\n\n const opened = await port.open({\n orderId: order.id,\n number: Number(order.number),\n totalCents: Number(order.total_cents),\n currency: order.currency,\n email: order.email,\n })\n await db.query('UPDATE shop.orders SET payment_ref = $2 WHERE id = $1', [\n order.id,\n opened.reference,\n ])\n const total = Number(order.total_cents)\n return {\n commandeId: order.id,\n numero: Number(order.number),\n jetonDeSuivi: order.id,\n totalCentimes: total,\n totalTexte: new Intl.NumberFormat('fr', {\n style: 'currency',\n currency: order.currency,\n }).format(total / 100),\n adresseDePaiement: opened.paymentUrl,\n }\n}\n\n/** How long a signed callback stays valid. */\nconst CALLBACK_WINDOW_S = 300\n\n/** The signature of a payment callback: HMAC-SHA256 of `reference.state.timestamp`. */\nexport function signCallback(\n secret: string,\n reference: string,\n state: string,\n timestamp: number,\n): string {\n return createHmac('sha256', secret)\n .update(`${reference}.${state}.${timestamp}`)\n .digest('hex')\n}\n\nexport function verifyCallback(\n secret: string,\n input: { reference: string; etat: string; horodatage: number; signature: string },\n now = Date.now(),\n): boolean {\n if (secret === '' || Math.abs(now / 1000 - input.horodatage) > CALLBACK_WINDOW_S)\n return false\n const expected = Buffer.from(\n signCallback(secret, input.reference, input.etat, input.horodatage),\n )\n const received = Buffer.from(input.signature)\n return expected.length === received.length && timingSafeEqual(expected, received)\n}\n\n/**\n * The payment's outcome, from Odoro. Idempotent: a callback delivered twice\n * changes nothing the second time.\n */\nexport async function confirmPayment(\n db: Base,\n reference: string,\n state: 'payee' | 'echouee',\n): Promise<boolean> {\n return await db.transaction(async (tx) => {\n const { rows } = await tx.query<{\n id: string\n cart_id: string | null\n state: string\n }>('SELECT id, cart_id, state FROM shop.orders WHERE payment_ref = $1 FOR UPDATE', [\n reference,\n ])\n const order = rows[0]\n if (order === undefined) return false\n if (order.state !== 'ouverte') return true\n\n if (state === 'echouee') {\n await tx.query(`UPDATE shop.orders SET state = 'echouee' WHERE id = $1`, [order.id])\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n return true\n }\n\n await tx.query(\n `UPDATE shop.orders SET state = 'payee', paid_at = now() WHERE id = $1`,\n [order.id],\n )\n // A code counts once it is PAID for: an abandoned checkout uses nothing.\n if (await codesInstalled(tx))\n await tx.query(\n `INSERT INTO shop.discount_uses (order_id, discount_id)\n SELECT o.id, c.id FROM shop.orders o\n JOIN shop.discount_codes c ON upper(c.code) = upper(o.discount_code)\n WHERE o.id = $1\n ON CONFLICT (order_id) DO NOTHING`,\n [order.id],\n )\n // The stock leaves when the money arrives, not before.\n await tx.query(\n `UPDATE shop.product_variants v\n SET stock = greatest(v.stock - l.quantity, 0)\n FROM shop.order_lines l\n WHERE l.order_id = $1 AND l.variant_id = v.id AND v.stock IS NOT NULL`,\n [order.id],\n )\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'consommee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n await tx.query(\n `UPDATE shop.carts SET state = 'commande', updated_at = now() WHERE id = $1`,\n [order.cart_id],\n )\n return true\n })\n}\n\n/** An order's state, for the tracking link the buyer received. */\nexport async function orderState(db: Query, orderId: string) {\n if (!/^[0-9a-f-]{36}$/i.test(orderId)) return null\n const { rows } = await db.query<OrderRow & { created_at: Date }>(\n 'SELECT * FROM shop.orders WHERE id = $1',\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) return null\n const { rows: lines } = await db.query<{\n product_name: string\n quantity: number\n unit_price_cents: number\n }>(\n 'SELECT product_name, quantity, unit_price_cents FROM shop.order_lines WHERE order_id = $1 ORDER BY product_name',\n [orderId],\n )\n return {\n numero: Number(order.number),\n etat: order.state,\n courriel: order.email,\n totalCentimes: Number(order.total_cents),\n passeeLe: new Date(order.created_at).toISOString(),\n lignes: lines.map((l) => ({\n nom: l.product_name,\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n })),\n livraisons: [],\n }\n}\n","/**\n * The customer's account, in the shop's own database (shop 1.3.0).\n *\n * No password: a fifteen-minute link, sent to the address. The link and the\n * session live in the database as FINGERPRINTS only (SHA-256): a database\n * that is read connects nobody.\n *\n * The same contract as Odoro's central storefront, so the site's account\n * page works unchanged:\n *\n * POST { geste: 'lien', email, offres? } create the account / sign in:\n * a link leaves by e-mail;\n * POST { geste: 'lettre', email, offres } the newsletter — `offres` MUST be\n * true (the ticked box);\n * POST { geste: 'profil', nom?, offres? } the profile (session required);\n * POST { geste: 'deconnexion' } closes the session;\n * GET the profile, the orders, the addresses.\n *\n * `lien` and `lettre` always answer `{ ok: true }`: the form must not become\n * a directory of the shop's customers. At most three links per person per\n * hour: without that bound, the page would fill someone's mailbox.\n *\n * The link itself is opened by `openSession` — mounted by the app on\n * `/api/storefront/account/login`, because it answers with a redirect and\n * cookies, not with JSON.\n *\n * @module\n */\n\nimport { createHash, randomBytes } from 'node:crypto'\n\nimport type { Base, Query } from './base.js'\nimport { CheckoutError } from './checkout.js'\n\n/** The session cookie: HttpOnly, the browser's script never reads it. */\nexport const CUSTOMER_COOKIE = 'odoro_client'\n/** How long a session lasts, in seconds. */\nexport const SESSION_MAX_AGE = 90 * 24 * 3600\n/** How long a link in an e-mail stays valid. */\nconst LINK_MINUTES = 15\n/** Links per person per hour. */\nexport const LINKS_PER_HOUR = 3\n/** The shape of a token: what `randomBytes(32).toString('base64url')` gives. */\nconst TOKEN = /^[A-Za-z0-9_-]{20,80}$/\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** Where the sign-in link leaves: Odoro sends it, the shop holds no mail key. */\nexport interface MailPort {\n sendLoginLink(input: { readonly email: string; readonly token: string }): Promise<void>\n}\n\n/** Are the accounts installed (shop 1.3.0)? */\nexport async function accountsInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('shop.customers') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nfunction fingerprint(token: string): string {\n return createHash('sha256').update(token).digest('hex')\n}\n\nfunction newToken(): string {\n return randomBytes(32).toString('base64url')\n}\n\nexport interface Customer {\n readonly id: string\n readonly email: string\n readonly name: string\n readonly consent: boolean\n}\n\n/**\n * Creates the account if it does not exist, and records the consent when it\n * is GIVEN — an unticked box never withdraws one given before: that is the\n * profile's job, where the person sees what they change.\n */\nasync function register(\n db: Query,\n email: string,\n input: { name?: string; consent: boolean; source: 'compte' | 'lettre' },\n): Promise<string> {\n const { rows } = await db.query<{ id: string }>(\n `INSERT INTO shop.customers (email, name, marketing_consent, consent_source, consent_at)\n VALUES ($1, $2, $3, CASE WHEN $3 THEN $4 END, CASE WHEN $3 THEN now() END)\n ON CONFLICT (email) DO UPDATE SET\n name = CASE WHEN excluded.name <> '' THEN excluded.name ELSE shop.customers.name END,\n marketing_consent = shop.customers.marketing_consent OR excluded.marketing_consent,\n consent_source = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent\n THEN excluded.consent_source ELSE shop.customers.consent_source END,\n consent_at = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent\n THEN now() ELSE shop.customers.consent_at END\n RETURNING id`,\n [email, (input.name ?? '').trim().slice(0, 120), input.consent, input.source],\n )\n return rows[0]!.id\n}\n\nfunction address(raw: string | undefined): string {\n const email = (raw ?? '').trim().toLowerCase().slice(0, 254)\n if (!EMAIL.test(email))\n throw new CheckoutError(400, \"Cette adresse de courriel n'est pas valable.\")\n return email\n}\n\n/** `lien`: the account, and a link by e-mail — unless three already left this hour. */\nexport async function requestLink(\n db: Query,\n mail: MailPort,\n input: { email?: string; name?: string; consent?: boolean },\n): Promise<void> {\n const email = address(input.email)\n const id = await register(db, email, {\n ...(input.name === undefined ? {} : { name: input.name }),\n consent: input.consent === true,\n source: 'compte',\n })\n const { rows } = await db.query<{ n: number }>(\n `SELECT count(*)::integer AS n FROM shop.customer_links\n WHERE customer_id = $1 AND created_at > now() - interval '1 hour'`,\n [id],\n )\n if (Number(rows[0]?.n ?? 0) >= LINKS_PER_HOUR) return\n const token = newToken()\n await db.query(\n `INSERT INTO shop.customer_links (customer_id, fingerprint, expires_at)\n VALUES ($1, $2, now() + make_interval(mins => $3))`,\n [id, fingerprint(token), LINK_MINUTES],\n )\n try {\n await mail.sendLoginLink({ email, token })\n } catch {\n // The answer stays the same: it must teach nothing to whoever tries.\n }\n}\n\n/** `lettre`: the newsletter, on the ticked box only. */\nexport async function subscribe(\n db: Query,\n input: { email?: string; name?: string; consent?: boolean },\n): Promise<void> {\n if (input.consent !== true) throw new CheckoutError(400, 'Requête illisible.')\n await register(db, address(input.email), {\n ...(input.name === undefined ? {} : { name: input.name }),\n consent: true,\n source: 'lettre',\n })\n}\n\n/**\n * The link from the e-mail, opened: it serves ONCE. Returns the session to\n * set, and the cart to set when the person had one elsewhere — or `null`\n * when the link is unknown, used or expired.\n *\n * The cart follows the person: this browser's cart becomes theirs; if this\n * browser has none, their last open cart comes back.\n */\nexport async function openSession(\n db: Base,\n token: string,\n cartToken: string | undefined,\n): Promise<{ session: string; cart: string | null } | null> {\n if (!TOKEN.test(token)) return null\n return await db.transaction(async (tx) => {\n const { rows } = await tx.query<{ customer_id: string }>(\n `UPDATE shop.customer_links SET used_at = now()\n WHERE fingerprint = $1 AND used_at IS NULL AND expires_at > now()\n RETURNING customer_id`,\n [fingerprint(token)],\n )\n const customer = rows[0]?.customer_id\n if (customer === undefined) return null\n const session = newToken()\n await tx.query(\n `INSERT INTO shop.customer_sessions (customer_id, fingerprint, expires_at)\n VALUES ($1, $2, now() + make_interval(secs => $3))`,\n [customer, fingerprint(session), SESSION_MAX_AGE],\n )\n await tx.query('UPDATE shop.customers SET last_seen_at = now() WHERE id = $1', [\n customer,\n ])\n\n if (cartToken !== undefined && cartToken !== '') {\n const { rows: mine } = await tx.query<{ id: string }>(\n `UPDATE shop.carts SET customer_id = $2, updated_at = now()\n WHERE token = $1 AND state = 'ouvert' RETURNING id`,\n [cartToken, customer],\n )\n if (mine.length > 0) return { session, cart: null }\n }\n const { rows: last } = await tx.query<{ token: string }>(\n `SELECT token FROM shop.carts\n WHERE customer_id = $1 AND state = 'ouvert'\n ORDER BY updated_at DESC LIMIT 1`,\n [customer],\n )\n return { session, cart: last[0]?.token ?? null }\n })\n}\n\n/** The person a session cookie names, or `null`. */\nexport async function customerOf(\n db: Query,\n session: string | undefined,\n): Promise<Customer | null> {\n if (session === undefined || !TOKEN.test(session)) return null\n const { rows } = await db.query<{\n id: string\n email: string\n name: string\n marketing_consent: boolean\n }>(\n `SELECT c.id, c.email, c.name, c.marketing_consent\n FROM shop.customer_sessions s JOIN shop.customers c ON c.id = s.customer_id\n WHERE s.fingerprint = $1 AND s.expires_at > now()`,\n [fingerprint(session)],\n )\n const c = rows[0]\n return c === undefined\n ? null\n : { id: c.id, email: c.email, name: c.name, consent: c.marketing_consent }\n}\n\n/** The profile, as the site's account page reads it. */\nexport async function profile(db: Query, customer: Customer) {\n // The orders are found by ADDRESS: the link proved the person holds it,\n // and orders placed before the account existed are theirs too.\n const { rows: orders } = await db.query<{\n id: string\n state: string\n total_cents: number\n currency: string\n created_at: Date\n paid_at: Date | null\n shipping_address: Record<string, string> | null\n customer_name: string\n }>(\n `SELECT id, state, total_cents, currency, created_at, paid_at, shipping_address, customer_name\n FROM shop.orders\n WHERE lower(email) = $1 AND state IN ('payee', 'remboursee', 'annulee')\n ORDER BY created_at DESC LIMIT 50`,\n [customer.email],\n )\n const seen = new Set<string>()\n const addresses: { nom: string | null; lignes: string[] }[] = []\n for (const o of orders) {\n const a = o.shipping_address ?? {}\n const lignes = [\n a['ligne1'],\n a['ligne2'],\n `${a['code_postal'] ?? ''} ${a['ville'] ?? ''}`.trim(),\n a['pays'],\n ].filter((x): x is string => typeof x === 'string' && x !== '')\n const key = lignes.join('|')\n if (lignes.length === 0 || seen.has(key) || addresses.length >= 5) continue\n seen.add(key)\n addresses.push({ nom: o.customer_name || null, lignes })\n }\n return {\n nom: customer.name === '' ? null : customer.name,\n courriel: customer.email,\n offres: customer.consent,\n commandes: orders.map((o) => ({\n id: o.id,\n etat: o.state,\n total: new Intl.NumberFormat('fr', {\n style: 'currency',\n currency: o.currency,\n }).format(Number(o.total_cents) / 100),\n le: new Date(o.paid_at ?? o.created_at).toISOString(),\n })),\n adresses: addresses,\n }\n}\n\n/** `profil`: the name, and the consent — which the person CAN withdraw here. */\nexport async function updateProfile(\n db: Query,\n customer: Customer,\n input: { name?: string; consent?: boolean },\n): Promise<void> {\n await db.query(\n `UPDATE shop.customers SET\n name = coalesce($2, name),\n marketing_consent = coalesce($3, marketing_consent),\n consent_source = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN 'compte'\n WHEN $3 IS FALSE THEN NULL ELSE consent_source END,\n consent_at = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN now()\n WHEN $3 IS FALSE THEN NULL ELSE consent_at END,\n last_seen_at = now()\n WHERE id = $1`,\n [\n customer.id,\n input.name === undefined ? null : input.name.trim().slice(0, 120),\n input.consent === undefined ? null : input.consent,\n ],\n )\n}\n\n/** `deconnexion`: the session ends here, not only in this browser. */\nexport async function closeSession(\n db: Query,\n session: string | undefined,\n): Promise<void> {\n if (session === undefined || !TOKEN.test(session)) return\n await db.query('DELETE FROM shop.customer_sessions WHERE fingerprint = $1', [\n fingerprint(session),\n ])\n}\n","/**\n * The storefront contract, as an `@odoro-cli/server` module.\n *\n * A site that sells, running in its own container with its own database,\n * mounts this module and serves `/api/storefront/*` itself — the same answers\n * as Odoro's central storefront, so the V4 script (`odoro-boutique.js`) and\n * `@odoro-cli/commerce` talk to it without knowing which one they reach.\n *\n * ## What it serves, and what it does not yet\n *\n * The catalogue, product pages, collections, the cart, checkout (open, quote\n * with the shop's discount codes, pay through Odoro), order tracking, and the\n * customer's account (shop 1.3.0, with a `mail` port). What a shop cannot\n * serve answers so, by name — an account on a shop without accounts says the\n * account is not available, an unknown code says it is not recognised. A\n * gesture that silently does nothing is worse than one that says no.\n *\n * ## Errors speak the contract\n *\n * The storefront's clients read `erreur`. Every refusal is a problem document\n * (RFC 9457, like the rest of the kernel) that also carries `erreur`.\n *\n * @module\n */\n\nimport {\n ApiError,\n ConflictError,\n NotFoundError,\n ServiceUnavailableError,\n defineModule,\n route,\n type Cookies,\n} from '@odoro-cli/server'\nimport { z } from 'zod'\n\nimport {\n CUSTOMER_COOKIE,\n accountsInstalled,\n closeSession,\n customerOf,\n profile,\n requestLink,\n subscribe,\n updateProfile,\n type MailPort,\n} from './account.js'\nimport { type Base, shopInstalled } from './base.js'\nimport {\n CART_COOKIE,\n CART_MAX_AGE,\n CartError,\n addToCart,\n findCart,\n openCart,\n readCart,\n setQuantity,\n} from './cart.js'\nimport { readCatalogue, readCollections, readProduct } from './catalogue.js'\nimport {\n CheckoutError,\n applyCode,\n confirmPayment,\n openCheckout,\n orderState,\n pay,\n quote,\n verifyCallback,\n type PaymentPort,\n} from './checkout.js'\n\nexport interface CommerceOptions {\n /** The site's shop database (`@odoro-cli/cloud-connect`, or any `Base`). */\n readonly db: Base\n /** Where the money goes: Odoro's payment. */\n readonly payment: PaymentPort\n /** The secret Odoro signs its payment callbacks with. Empty = callbacks refused. */\n readonly callbackSecret: string\n /** The shop's currency. @defaultValue 'EUR' */\n readonly currency?: string\n /** Mark the cart and session cookies `Secure`. @defaultValue true */\n readonly secureCookie?: boolean\n /**\n * Where the sign-in links leave: Odoro sends them. Without it, the account\n * says it is not available — by name.\n */\n readonly mail?: MailPort\n}\n\nfunction refusal(\n kind: 'VALIDATION' | 'NOT_FOUND' | 'CONFLICT' | 'UNAVAILABLE',\n message: string,\n): ApiError {\n const extensions = { extensions: { erreur: message } }\n if (kind === 'NOT_FOUND') return new NotFoundError(message, extensions)\n if (kind === 'CONFLICT') return new ConflictError(message, extensions)\n if (kind === 'UNAVAILABLE')\n return new ServiceUnavailableError(message, undefined, extensions)\n return new ApiError('VALIDATION', message, extensions)\n}\n\n/** Domain refusals become contract errors; anything else stays an internal error. */\nasync function speaking<T>(work: () => Promise<T>): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n if (cause instanceof CheckoutError) {\n throw refusal(\n cause.status === 404\n ? 'NOT_FOUND'\n : cause.status === 409\n ? 'CONFLICT'\n : 'VALIDATION',\n cause.message,\n )\n }\n if (cause instanceof CartError) throw refusal('CONFLICT', cause.message)\n throw cause\n }\n}\n\nconst text = z.string().max(200).optional()\nconst count = z.coerce.number().int().min(0).max(100_000).optional()\n\nexport function createCommerceModule(options: CommerceOptions) {\n const { db } = options\n const currency = options.currency ?? 'EUR'\n let installed: Promise<boolean> | null = null\n\n /** Once per process: the `shop` capability, at a version this module reads. */\n async function ready(): Promise<void> {\n installed ??= shopInstalled(db)\n if (!(await installed)) {\n installed = null\n throw refusal('UNAVAILABLE', \"La boutique de ce site n'est pas encore installée.\")\n }\n }\n\n async function cartOf(cookies: Cookies) {\n return await findCart(db, cookies.get(CART_COOKIE))\n }\n\n function keep(cookies: Cookies, token: string): void {\n cookies.set(CART_COOKIE, token, {\n httpOnly: true,\n sameSite: 'lax',\n maxAge: CART_MAX_AGE,\n ...(options.secureCookie === false ? { secure: false } : {}),\n })\n }\n\n const storefront = route({\n name: 'storefront.read',\n method: 'GET',\n path: '/api/storefront',\n auth: 'public',\n input: z.object({\n geste: z\n .enum(['catalogue', 'fiche', 'collections', 'commande', 'suggestions'])\n .default('catalogue'),\n produit: text,\n jeton: text,\n q: text,\n collection: text,\n genre: text,\n prix_min: count,\n prix_max: count,\n disponibles: z.string().optional(),\n tri: z\n .enum(['nouveautes', 'prix_croissant', 'prix_decroissant', 'nom'])\n .optional()\n .catch(undefined),\n combien: count,\n depuis: count,\n }),\n summary:\n 'The catalogue, a product page, the collections or an order, per the storefront contract.',\n handler: async ({ input }) => {\n await ready()\n if (input.geste === 'fiche') {\n const fiche = await readProduct(db, input.produit ?? '')\n if (fiche === null) throw refusal('NOT_FOUND', \"Cet article n'est plus en vente.\")\n return { fiche }\n }\n if (input.geste === 'collections') return { collections: await readCollections(db) }\n if (input.geste === 'commande') {\n const commande = await orderState(db, input.jeton ?? '')\n if (commande === null) throw refusal('NOT_FOUND', \"Cette commande n'existe pas.\")\n return { commande }\n }\n if (input.geste === 'suggestions') {\n const { produits } = await readCatalogue(db, { q: input.q ?? '', combien: 6 })\n return { suggestions: produits.map((p) => ({ id: p.id, nom: p.nom })) }\n }\n return await readCatalogue(db, {\n ...(input.q === undefined ? {} : { q: input.q }),\n ...(input.collection === undefined ? {} : { collection: input.collection }),\n ...(input.genre === undefined ? {} : { genre: input.genre }),\n ...(input.prix_min === undefined ? {} : { prixMin: input.prix_min }),\n ...(input.prix_max === undefined ? {} : { prixMax: input.prix_max }),\n disponibles: input.disponibles === '1',\n ...(input.tri === undefined ? {} : { tri: input.tri }),\n ...(input.combien === undefined ? {} : { combien: input.combien }),\n ...(input.depuis === undefined ? {} : { depuis: input.depuis }),\n })\n },\n })\n\n /** A page view. Nothing is counted or stored here: no tracker on the site. */\n const visit = route({\n name: 'storefront.visit',\n method: 'POST',\n path: '/api/storefront',\n auth: 'public',\n handler: () => ({ ok: true }),\n })\n\n const readCartRoute = route({\n name: 'storefront.cart.read',\n method: 'GET',\n path: '/api/storefront/cart',\n auth: 'public',\n handler: async ({ cookies }) => {\n await ready()\n const cart = await cartOf(cookies)\n if (cart === null) {\n return {\n panier: {\n panierId: '',\n jeton: '',\n courriel: null,\n lignes: [],\n combien: 0,\n sousTotalCentimes: 0,\n neuf: false,\n },\n }\n }\n return { panier: await readCart(db, cart) }\n },\n })\n\n const changeCart = route({\n name: 'storefront.cart.change',\n method: 'POST',\n path: '/api/storefront/cart',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ajouter', 'quantite']).default('ajouter'),\n variante: z.string().regex(/^[0-9a-f-]{36}$/i, \"Cet article n'est plus en vente.\"),\n quantite: z.coerce.number().int().min(0).max(99).default(1),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n const existing = await cartOf(cookies)\n const cart = existing ?? (await openCart(db))\n if (input.geste === 'quantite')\n await setQuantity(db, cart.id, input.variante, input.quantite)\n else await addToCart(db, cart.id, input.variante, input.quantite)\n // The cookie is set on a GESTURE, never on a visit.\n if (existing === null) keep(cookies, cart.token)\n return { panier: await readCart(db, cart, existing === null) }\n }),\n })\n\n const checkout = route({\n name: 'storefront.checkout',\n method: 'POST',\n path: '/api/storefront/checkout',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ouvrir', 'chiffrer', 'payer']),\n caisse: z.string().max(64).optional(),\n code: z.string().max(40).optional(),\n courriel: text,\n nom: text,\n ligne1: text,\n ligne2: text,\n code_postal: z.string().max(20).optional(),\n ville: text,\n pays: z.string().max(40).optional(),\n telephone: z.string().max(30).optional(),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n if (input.geste === 'ouvrir') {\n const cart = await cartOf(cookies)\n if (cart === null) throw new CheckoutError(400, 'Votre panier est vide.')\n const opened = await openCheckout(\n db,\n cart.id,\n {\n courriel: input.courriel ?? '',\n nom: input.nom ?? '',\n ligne1: input.ligne1 ?? '',\n ligne2: input.ligne2 ?? '',\n code_postal: input.code_postal ?? '',\n ville: input.ville ?? '',\n pays: input.pays ?? '',\n telephone: input.telephone ?? '',\n },\n currency,\n )\n return {\n caisse: opened.orderId,\n prixRevalorises: opened.repriced,\n total: await quote(db, opened.orderId),\n }\n }\n const orderId = input.caisse ?? ''\n if (!/^[0-9a-f-]{36}$/i.test(orderId))\n throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (input.geste === 'chiffrer')\n return { total: await applyCode(db, orderId, input.code) }\n await applyCode(db, orderId, input.code)\n return await pay(db, orderId, options.payment)\n }),\n })\n\n /** Odoro tells the payment's outcome. Signed: the browser never decides that an order is paid. */\n const callback = route({\n name: 'storefront.payment.callback',\n method: 'POST',\n path: '/api/storefront/payment-callback',\n auth: 'public',\n input: z.object({\n reference: z.string().min(1).max(200),\n etat: z.enum(['payee', 'echouee']),\n horodatage: z.coerce.number().int(),\n signature: z.string().min(1).max(128),\n }),\n handler: async ({ input }) => {\n await ready()\n if (!verifyCallback(options.callbackSecret, input)) {\n throw refusal('NOT_FOUND', 'Rappel de paiement refusé.')\n }\n const known = await confirmPayment(db, input.reference, input.etat)\n if (!known) throw refusal('NOT_FOUND', 'Paiement inconnu.')\n return { ok: true }\n },\n })\n\n // The account serves only where it can: a shop in 1.3.0, with a way to\n // send the link. Anywhere else it says it is not available — by name,\n // never a dead button.\n const accountRead = route({\n name: 'storefront.account.read',\n method: 'GET',\n path: '/api/storefront/account',\n auth: 'public',\n handler: async ({ cookies }) => {\n if (options.mail === undefined || !(await accountsInstalled(db)))\n return { connecte: false }\n const customer = await customerOf(db, cookies.get(CUSTOMER_COOKIE))\n if (customer === null) return { connecte: false }\n return { connecte: true, profil: await profile(db, customer) }\n },\n })\n const accountWrite = route({\n name: 'storefront.account.write',\n method: 'POST',\n path: '/api/storefront/account',\n auth: 'public',\n input: z.object({\n geste: z.enum(['lien', 'lettre', 'profil', 'deconnexion']).optional(),\n email: z.string().max(254).optional(),\n offres: z.boolean().optional(),\n nom: z.string().max(120).optional(),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n const mail = options.mail\n if (mail === undefined || !(await accountsInstalled(db))) {\n throw refusal(\n 'UNAVAILABLE',\n \"Le compte client n'est pas encore disponible sur cette boutique.\",\n )\n }\n const geste = input.geste ?? 'lien'\n const who = {\n email: input.email,\n ...(input.nom === undefined ? {} : { name: input.nom }),\n ...(input.offres === undefined ? {} : { consent: input.offres }),\n }\n if (geste === 'lien') {\n await requestLink(db, mail, who)\n return { ok: true }\n }\n if (geste === 'lettre') {\n await subscribe(db, who)\n return { ok: true }\n }\n const session = cookies.get(CUSTOMER_COOKIE)\n if (geste === 'deconnexion') {\n await closeSession(db, session)\n cookies.clear(CUSTOMER_COOKIE)\n return { ok: true }\n }\n const customer = await customerOf(db, session)\n if (customer === null) {\n const message =\n 'Ce lien de connexion est périmé ou a déjà servi. Demandez-en un nouveau.'\n throw new ApiError('UNAUTHORIZED', message, { extensions: { erreur: message } })\n }\n await updateProfile(db, customer, {\n ...(input.nom === undefined ? {} : { name: input.nom }),\n ...(input.offres === undefined ? {} : { consent: input.offres }),\n })\n const after = (await customerOf(db, session)) ?? customer\n return { ok: true, profil: await profile(db, after) }\n }),\n })\n\n return defineModule({\n name: 'commerce',\n routes: [\n storefront,\n visit,\n readCartRoute,\n changeCart,\n checkout,\n callback,\n accountRead,\n accountWrite,\n ] as never,\n })\n}\n","/**\n * The payment, opened at Odoro.\n *\n * A site that sells never holds a payment provider's key: it asks Odoro to\n * open the payment, and Odoro tells it the outcome through the signed callback\n * (`/api/storefront/payment-callback`). The site's secret is its own — shared\n * with Odoro, with no other site — so a compromised site can open payments\n * for itself and nothing else.\n *\n * ## The request's signature\n *\n * `x-odoro-signature` is the hex HMAC-SHA256 of `${timestamp}.${body}`, and\n * `x-odoro-timestamp` the Unix time in seconds. Odoro refuses a signature more\n * than five minutes old: a request copied from a log cannot be replayed.\n *\n * @module\n */\n\nimport { createHmac } from 'node:crypto'\n\nimport { CheckoutError, type PaymentPort } from './checkout.js'\n\nexport interface OdoroPaymentOptions {\n /** Odoro's address, e.g. `https://odoro.ai`. */\n readonly origin: string\n /** This site's identifier at Odoro. */\n readonly site: string\n /** This site's secret, shared with Odoro alone. */\n readonly secret: string\n /** @internal For the tests. */\n readonly fetch?: typeof fetch\n /** @internal For the tests: seconds since the epoch. */\n readonly now?: () => number\n}\n\n/** Where Odoro opens a site's payment. */\nexport const ODORO_PAYMENT_PATH = '/api/payment/site-order'\n\nexport function signRequest(secret: string, timestamp: number, body: string): string {\n return createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')\n}\n\nconst UNAVAILABLE = \"Le paiement n'a pas pu s'ouvrir. Réessayez dans un instant.\"\n\nexport function odoroPayment(options: OdoroPaymentOptions): PaymentPort {\n const send = options.fetch ?? fetch\n const now = options.now ?? (() => Math.floor(Date.now() / 1000))\n const address = new URL(ODORO_PAYMENT_PATH, options.origin).toString()\n\n return {\n open: async (order) => {\n if (options.secret === '') throw new CheckoutError(409, UNAVAILABLE)\n const body = JSON.stringify({\n site: options.site,\n commande: order.orderId,\n numero: order.number,\n totalCentimes: order.totalCents,\n devise: order.currency,\n courriel: order.email,\n })\n const timestamp = now()\n let reply: Response\n try {\n reply = await send(address, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-odoro-timestamp': String(timestamp),\n 'x-odoro-signature': signRequest(options.secret, timestamp, body),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n })\n } catch {\n throw new CheckoutError(409, UNAVAILABLE)\n }\n const payload = (await reply.json().catch(() => ({}))) as {\n reference?: unknown\n adresse?: unknown\n erreur?: unknown\n }\n if (\n !reply.ok ||\n typeof payload.reference !== 'string' ||\n typeof payload.adresse !== 'string'\n ) {\n // Odoro's refusal is written for the buyer (\"the shop is not open yet\"):\n // it is passed on as it is. Anything else says to try again.\n const said =\n typeof payload.erreur === 'string' && reply.status < 500\n ? payload.erreur\n : UNAVAILABLE\n throw new CheckoutError(409, said)\n }\n return { reference: payload.reference, paymentUrl: payload.adresse }\n },\n }\n}\n","/**\n * The sign-in link, sent by Odoro.\n *\n * A shop holds no mail key: it asks Odoro to send the link, signed with the\n * site's secret exactly like a payment (`odoro-payment.ts`). Odoro writes the\n * address of the link itself — the shop's own published address — and only\n * takes the TOKEN from here: a compromised site cannot make Odoro mail a link\n * that leads anywhere else.\n *\n * @module\n */\n\nimport type { MailPort } from './account.js'\nimport { signRequest } from './odoro-payment.js'\n\nexport interface OdoroMailOptions {\n /** Odoro's address, e.g. `https://odoro.ai`. */\n readonly origin: string\n /** This site's identifier at Odoro. */\n readonly site: string\n /** This site's secret, shared with Odoro alone. */\n readonly secret: string\n /** @internal For the tests. */\n readonly fetch?: typeof fetch\n /** @internal For the tests: seconds since the epoch. */\n readonly now?: () => number\n}\n\n/** Where Odoro sends a site's sign-in link. */\nexport const ODORO_MAIL_PATH = '/api/hosting/site-login-link'\n\nexport function odoroMail(options: OdoroMailOptions): MailPort {\n const send = options.fetch ?? fetch\n const now = options.now ?? (() => Math.floor(Date.now() / 1000))\n const address = new URL(ODORO_MAIL_PATH, options.origin).toString()\n\n return {\n sendLoginLink: async ({ email, token }) => {\n if (options.secret === '') throw new Error('site secret missing')\n const body = JSON.stringify({ site: options.site, courriel: email, jeton: token })\n const timestamp = now()\n const reply = await send(address, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-odoro-timestamp': String(timestamp),\n 'x-odoro-signature': signRequest(options.secret, timestamp, body),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n })\n if (!reply.ok) throw new Error(`Odoro refused the link (${reply.status})`)\n },\n }\n}\n","/**\n * A `Base` from a `pg` pool — without depending on `pg`.\n *\n * The shape is structural: any pool whose `query` and `connect` look like\n * node-postgres works, and the site keeps the driver version it chose.\n *\n * A transaction takes ONE client for its whole length: `BEGIN` on one\n * connection and `COMMIT` on another would commit nothing, silently.\n *\n * @module\n */\n\nimport type { Base, Query } from './base.js'\n\ninterface PgResult {\n rows: unknown[]\n}\n\nexport interface PgClientLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n release(): void\n}\n\nexport interface PgPoolLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n connect(): Promise<PgClientLike>\n}\n\nfunction reader(\n run: (text: string, values?: readonly unknown[]) => Promise<PgResult>,\n): Query {\n return {\n query: async <Row extends object>(text: string, values?: readonly unknown[]) => {\n const { rows } = await run(text, values)\n return { rows: rows as Row[] }\n },\n }\n}\n\nexport function baseFromPool(pool: PgPoolLike): Base {\n return {\n ...reader((text, values) => pool.query(text, values)),\n transaction: async (work) => {\n const client = await pool.connect()\n try {\n await client.query('BEGIN')\n const result = await work(reader((text, values) => client.query(text, values)))\n await client.query('COMMIT')\n return result\n } catch (cause) {\n await client.query('ROLLBACK').catch(() => undefined)\n throw cause\n } finally {\n client.release()\n }\n },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odoro-cli/server-commerce",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "author": "BouBouw",