@pandait.tech/payment-nuvei 0.5.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,11 +110,17 @@ export const POST = createChargeHandler({
110
110
 
111
111
  Apply the same pattern to the remaining handlers — each one is documented inline with its own `XHandlerDeps` interface.
112
112
 
113
- ### Securing the webhook (important)
113
+ ### The webhook is OPTIONAL — and if you expose it, secure it
114
114
 
115
- Paymentez/Nuvei DMNs (webhooks) are **not signed**, so the webhook endpoint must be protected with a shared secret — otherwise anyone who knows an `orderId` could POST a fake "paid" notification.
115
+ First, set expectations correctly:
116
116
 
117
- 1. Set a secret and pass it to the handler (or via `NUVEI_WEBHOOK_SECRET`):
117
+ - **Payments are confirmed by the synchronous `/debit` response** (`createChargeHandler` reads `transaction.status === "success" && status_detail === 3`), plus the 3DS `term_url` callback + `verify` polling for challenged transactions. **That path fully confirms a payment without any webhook.**
118
+ - Paymentez's transaction-notification **callback** (`developers.paymentez.com/api/#webhook`) is an **optional, redundant async channel** — Paymentez POSTs transaction status to a URL you register *with Paymentez during integration* (there is no documented self-serve dashboard; for many accounts it is simply never wired up). In a real production consumer we observed Paymentez never calling it at all.
119
+ - So `createWebhookHandler` is a reconciliation backstop you can mount **if** your Paymentez account is configured to notify it. It is **not required** to take payments.
120
+
121
+ Because the endpoint is nonetheless a **public POST** route, if you mount it you must authenticate it — otherwise anyone who knows an `orderId` could POST a fake "paid" notification (`status_detail: 3`) and mark an order paid. Paymentez callbacks are **not signed**, so use a shared secret:
122
+
123
+ 1. Pass a secret to the handler (or via `NUVEI_WEBHOOK_SECRET`):
118
124
 
119
125
  ```ts
120
126
  export const POST = createWebhookHandler({
@@ -125,7 +131,7 @@ Paymentez/Nuvei DMNs (webhooks) are **not signed**, so the webhook endpoint must
125
131
  });
126
132
  ```
127
133
 
128
- 2. In the **Nuvei dashboard**, configure your DMN/callback URL with the secret as a query param:
134
+ 2. If/when you register the notification URL with Paymentez, append the secret as a query param:
129
135
 
130
136
  ```
131
137
  https://your-shop.com/api/webhooks/nuvei?key=YOUR_SECRET
@@ -133,9 +139,9 @@ Paymentez/Nuvei DMNs (webhooks) are **not signed**, so the webhook endpoint must
133
139
 
134
140
  (The secret is also accepted as the `x-webhook-key` header.)
135
141
 
136
- Requests without a matching key get `401` and are not processed (constant-time compared). If `webhookSecret` is **unset**, the handler still processes but logs a loud warning — treat that as not-production-ready.
142
+ Requests without a matching key get `401` and are not processed (constant-time compared). If `webhookSecret` is **unset**, the handler still processes but logs a loud warning. If you never register the URL with Paymentez, setting the secret simply seals the endpoint (everything → 401), which is a safe default.
137
143
 
138
- > `onPaymentSucceeded` fires once, on the transition into `paid` (idempotent across Nuvei's webhook retries).
144
+ > `onPaymentSucceeded` fires once, on the transition into `paid` (idempotent across retries).
139
145
 
140
146
  ## Hosting & deployment (3DS callback + Nuvei egress)
141
147
 
@@ -312,7 +318,22 @@ The handlers read/write a single `orders` collection (plus `promotions` for coup
312
318
 
313
319
  Public, supported entry points: the package root (SDK), `./handlers`, `./adapters`, `./payment-links`, `./ui`, and `./ui/styles.css`. Everything else (e.g. internal `http.ts`) is private and may change without notice.
314
320
 
315
- Pre-1.0 the package follows semver with the caveat that **minor versions may make small breaking changes to handler `*HandlerDeps` interfaces** as the design settles. From **1.0.0** onward the public API is frozen under semver. See `CHANGELOG.md`.
321
+ **As of `1.0.0` the public API is frozen under semver** — breaking changes to the
322
+ exported factories, their `*HandlerDeps` interfaces, the SDK functions, or the UI
323
+ component props require a major version bump. See `CHANGELOG.md`.
324
+
325
+ Design notes consumers should know:
326
+
327
+ - **Order shape is generic.** `MinimalOrder` extends `Record<string, unknown>` and
328
+ declares only fields the package itself reads/writes. Your business-specific
329
+ order fields are yours to read off the order inside your callbacks
330
+ (`onPaymentSucceeded`, `validateCustomOrder`, `getRetryUrl`) — narrow them there.
331
+ - **Payment links are primitives only.** The package ships token/expiry/price-bound
332
+ helpers; the payment-link *document shape* (what it points to) is yours to define.
333
+ - **The webhook secret is optional but strongly recommended.** It's left optional so
334
+ the package never forces a specific webhook-URL scheme, but an unconfigured
335
+ webhook is forgeable — always set `webhookSecret` in production (see "Securing the
336
+ webhook" above).
316
337
 
317
338
  ## Migration from `pauhenriques-website`
318
339
 
@@ -23,10 +23,9 @@ interface MinimalOrder extends FirestoreData {
23
23
  shippingAddress?: {
24
24
  fullName?: string;
25
25
  } & FirestoreData;
26
+ /** Optional note shown to the customer post-purchase; echoed into the
27
+ * confirmation email when present. */
26
28
  postPurchaseNote?: string;
27
- paymentLinkId?: string;
28
- tallerId?: string;
29
- courseId?: string;
30
29
  /** Set by charge.ts when the user opted out of saving the card AND the order
31
30
  * enters an intermediate status (3ds-pending / otp-pending). The downstream
32
31
  * handler that finalizes the payment (3ds-complete, webhook BY_CRES) reads
@@ -23,10 +23,9 @@ interface MinimalOrder extends FirestoreData {
23
23
  shippingAddress?: {
24
24
  fullName?: string;
25
25
  } & FirestoreData;
26
+ /** Optional note shown to the customer post-purchase; echoed into the
27
+ * confirmation email when present. */
26
28
  postPurchaseNote?: string;
27
- paymentLinkId?: string;
28
- tallerId?: string;
29
- courseId?: string;
30
29
  /** Set by charge.ts when the user opted out of saving the card AND the order
31
30
  * enters an intermediate status (3ds-pending / otp-pending). The downstream
32
31
  * handler that finalizes the payment (3ds-complete, webhook BY_CRES) reads
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/payment-links/paymentLink.ts","../../src/payment-links/anonymousAuth.ts"],"names":["randomBytes"],"mappings":";;;;;AAkCO,IAAM,sBAAA,GAAyB;AAC/B,IAAM,sBAAA,GAAyB;AAC/B,IAAM,oCAAA,GAAuC;AAC7C,IAAM,gCAAA,GAAmC;AAEzC,SAAS,wBAAA,GAAmC;AAGjD,EAAA,OAAOA,mBAAY,EAAE,CAAA,CAClB,QAAA,CAAS,QAAQ,EACjB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,QAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACvB;AAEO,SAAS,aAAA,GAAsB;AACpC,EAAA,MAAM,CAAA,uBAAQ,IAAA,EAAK;AACnB,EAAA,CAAA,CAAE,OAAA,CAAQ,CAAA,CAAE,OAAA,EAAQ,GAAI,gCAAgC,CAAA;AACxD,EAAA,OAAO,CAAA;AACT;AAEO,SAAS,UAAU,SAAA,EAAmC;AAC3D,EAAA,MAAM,MAAM,OAAO,SAAA,KAAc,QAAA,GAAW,SAAA,GAAY,UAAU,WAAA,EAAY;AAC9E,EAAA,OAAO,IAAI,IAAA,CAAK,GAAG,EAAE,OAAA,EAAQ,GAAI,KAAK,GAAA,EAAI;AAC5C;;;ACrBO,SAAS,0BAA0B,IAAA,EAA+B;AACvE,EAAA,MAAM,eAAA,GAAkB,KAAK,eAAA,IAAmB,mBAAA;AAEhD,EAAA,OAAO,eAAe,uBACpB,YAAA,EACe;AACf,IAAA,IAAI,IAAA,GAAO,YAAA,IAAgB,IAAA,CAAK,IAAA,CAAK,WAAA;AAErC,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,EAAE,iBAAA,EAAkB,GAAI,MAAM,OAAO,eAAe,CAAA;AAC1D,MAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,CAAkB,IAAA,CAAK,IAAI,CAAA;AAC9C,MAAA,IAAA,GAAO,IAAA,CAAK,IAAA;AAAA,IACd;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,UAAA;AAAA;AAAA,MAA8B;AAAA,KAAI;AAC7D,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,eAAA,EAAiB;AAAA,MACvC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,SAAS;AAAA,KACjC,CAAA;AAED,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oCAAA,EAAoC,GAAA,CAAI,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,IACnE;AAEA,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AACF","file":"index.cjs","sourcesContent":["import { randomBytes } from \"crypto\";\n\nexport interface PaymentLinkInput {\n price: number;\n label?: string;\n publicLabel?: string;\n expiresAt: Date;\n tallerId: string;\n notes?: string;\n}\n\nexport interface PaymentLink {\n id: string;\n token: string;\n tallerId: string;\n price: number;\n // Nombre interno (solo admin) — referencia para identificar el link en la\n // tabla y en el dashboard. NUNCA se muestra al cliente.\n label: string | null;\n // Etiqueta promocional visible al cliente en /pago/t/[token] junto al\n // emoji 🔥 (ej. \"Promo amigas\", \"Última semana\"). Opcional.\n publicLabel: string | null;\n notes: string | null;\n active: boolean;\n expiresAt: string;\n createdByUid: string;\n createdByEmail: string | null;\n createdByName: string | null;\n timesPaid: number;\n lastPaidAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport const PAYMENT_LINK_PRICE_MIN = 1;\nexport const PAYMENT_LINK_PRICE_MAX = 2000;\nexport const PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD = 200;\nexport const PAYMENT_LINK_DEFAULT_EXPIRY_DAYS = 30;\n\nexport function generatePaymentLinkToken(): string {\n // 12 bytes → 16 chars base64url, suficientemente largo para evitar adivinanza\n // (2^96 combinaciones) y lo bastante corto para un URL amigable.\n return randomBytes(12)\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n\nexport function defaultExpiry(): Date {\n const d = new Date();\n d.setDate(d.getDate() + PAYMENT_LINK_DEFAULT_EXPIRY_DAYS);\n return d;\n}\n\nexport function isExpired(expiresAt: Date | string): boolean {\n const iso = typeof expiresAt === \"string\" ? expiresAt : expiresAt.toISOString();\n return new Date(iso).getTime() < Date.now();\n}\n","// Source: pauhenriques-website/src/lib/pago-link/anonymousAuth.ts\n// Phase 1.4: refactored to factory pattern.\n//\n// Client-only helper used by guest checkout flows (e.g. payment links) that\n// need a Firebase __session cookie to call the package's POST handlers\n// (charge, 3ds-complete, refund, etc.) — those handlers verify a session\n// cookie, so guests must first sign in anonymously.\n//\n// The two consumer touchpoints (Firebase client Auth instance and session\n// endpoint path) are injected as deps rather than imported from a fixed\n// module path, so the helper works in any Next.js app regardless of how\n// the consumer initializes Firebase.\n\nimport type { Auth, User } from \"firebase/auth\";\n\n/** Dependencies for the anonymous-auth helper factory. */\nexport interface AnonymousAuthHelperDeps {\n /** Firebase client Auth instance. Must be the same instance used by the consumer's app for sign-in state to stay consistent across calls. */\n auth: Auth;\n /** URL where the helper POSTs `{ idToken }` to exchange the Firebase ID token for a server-side session cookie. The endpoint must respond 2xx on success. Default: \"/api/auth/session\". */\n sessionEndpoint?: string;\n}\n\n/**\n * Creates an `ensureAnonymousSession(existingUser)` function that signs in\n * anonymously (if not already signed in) and establishes a server-side\n * session cookie. Idempotent: if a user is already signed in, refreshes\n * the cookie with the current user.\n *\n * Usage:\n *\n * const ensureAnonymousSession = createAnonymousAuthHelper({\n * auth: clientAuth,\n * sessionEndpoint: \"/api/auth/session\",\n * });\n * const user = await ensureAnonymousSession(currentUser);\n */\nexport function createAnonymousAuthHelper(deps: AnonymousAuthHelperDeps) {\n const sessionEndpoint = deps.sessionEndpoint ?? \"/api/auth/session\";\n\n return async function ensureAnonymousSession(\n existingUser: User | null,\n ): Promise<User> {\n let user = existingUser ?? deps.auth.currentUser;\n\n if (!user) {\n const { signInAnonymously } = await import(\"firebase/auth\");\n const cred = await signInAnonymously(deps.auth);\n user = cred.user;\n }\n\n const idToken = await user.getIdToken(/* forceRefresh */ true);\n const res = await fetch(sessionEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ idToken }),\n });\n\n if (!res.ok) {\n throw new Error(`No se pudo establecer la sesión (${res.status})`);\n }\n\n return user;\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/payment-links/paymentLink.ts","../../src/payment-links/anonymousAuth.ts"],"names":["randomBytes"],"mappings":";;;;;AAQO,IAAM,sBAAA,GAAyB;AAC/B,IAAM,sBAAA,GAAyB;AAC/B,IAAM,oCAAA,GAAuC;AAC7C,IAAM,gCAAA,GAAmC;AAEzC,SAAS,wBAAA,GAAmC;AAGjD,EAAA,OAAOA,mBAAY,EAAE,CAAA,CAClB,QAAA,CAAS,QAAQ,EACjB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,QAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACvB;AAEO,SAAS,aAAA,GAAsB;AACpC,EAAA,MAAM,CAAA,uBAAQ,IAAA,EAAK;AACnB,EAAA,CAAA,CAAE,OAAA,CAAQ,CAAA,CAAE,OAAA,EAAQ,GAAI,gCAAgC,CAAA;AACxD,EAAA,OAAO,CAAA;AACT;AAEO,SAAS,UAAU,SAAA,EAAmC;AAC3D,EAAA,MAAM,MAAM,OAAO,SAAA,KAAc,QAAA,GAAW,SAAA,GAAY,UAAU,WAAA,EAAY;AAC9E,EAAA,OAAO,IAAI,IAAA,CAAK,GAAG,EAAE,OAAA,EAAQ,GAAI,KAAK,GAAA,EAAI;AAC5C;;;ACKO,SAAS,0BAA0B,IAAA,EAA+B;AACvE,EAAA,MAAM,eAAA,GAAkB,KAAK,eAAA,IAAmB,mBAAA;AAEhD,EAAA,OAAO,eAAe,uBACpB,YAAA,EACe;AACf,IAAA,IAAI,IAAA,GAAO,YAAA,IAAgB,IAAA,CAAK,IAAA,CAAK,WAAA;AAErC,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,EAAE,iBAAA,EAAkB,GAAI,MAAM,OAAO,eAAe,CAAA;AAC1D,MAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,CAAkB,IAAA,CAAK,IAAI,CAAA;AAC9C,MAAA,IAAA,GAAO,IAAA,CAAK,IAAA;AAAA,IACd;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,UAAA;AAAA;AAAA,MAA8B;AAAA,KAAI;AAC7D,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,eAAA,EAAiB;AAAA,MACvC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,SAAS;AAAA,KACjC,CAAA;AAED,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oCAAA,EAAoC,GAAA,CAAI,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,IACnE;AAEA,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AACF","file":"index.cjs","sourcesContent":["import { randomBytes } from \"crypto\";\n\n// NOTE: this module intentionally exposes only generic payment-link PRIMITIVES\n// (token generation, expiry helpers, price bounds). The shape of a payment link\n// document — what it points to (a workshop, a course, a session…), its labels,\n// usage counters, etc. — is consumer-specific, so consumers define their own\n// PaymentLink type and build their own resolver on top of these primitives.\n\nexport const PAYMENT_LINK_PRICE_MIN = 1;\nexport const PAYMENT_LINK_PRICE_MAX = 2000;\nexport const PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD = 200;\nexport const PAYMENT_LINK_DEFAULT_EXPIRY_DAYS = 30;\n\nexport function generatePaymentLinkToken(): string {\n // 12 bytes → 16 chars base64url, suficientemente largo para evitar adivinanza\n // (2^96 combinaciones) y lo bastante corto para un URL amigable.\n return randomBytes(12)\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n\nexport function defaultExpiry(): Date {\n const d = new Date();\n d.setDate(d.getDate() + PAYMENT_LINK_DEFAULT_EXPIRY_DAYS);\n return d;\n}\n\nexport function isExpired(expiresAt: Date | string): boolean {\n const iso = typeof expiresAt === \"string\" ? expiresAt : expiresAt.toISOString();\n return new Date(iso).getTime() < Date.now();\n}\n","// Source: pauhenriques-website/src/lib/pago-link/anonymousAuth.ts\n// Phase 1.4: refactored to factory pattern.\n//\n// Client-only helper used by guest checkout flows (e.g. payment links) that\n// need a Firebase __session cookie to call the package's POST handlers\n// (charge, 3ds-complete, refund, etc.) — those handlers verify a session\n// cookie, so guests must first sign in anonymously.\n//\n// The two consumer touchpoints (Firebase client Auth instance and session\n// endpoint path) are injected as deps rather than imported from a fixed\n// module path, so the helper works in any Next.js app regardless of how\n// the consumer initializes Firebase.\n\nimport type { Auth, User } from \"firebase/auth\";\n\n/** Dependencies for the anonymous-auth helper factory. */\nexport interface AnonymousAuthHelperDeps {\n /** Firebase client Auth instance. Must be the same instance used by the consumer's app for sign-in state to stay consistent across calls. */\n auth: Auth;\n /** URL where the helper POSTs `{ idToken }` to exchange the Firebase ID token for a server-side session cookie. The endpoint must respond 2xx on success. Default: \"/api/auth/session\". */\n sessionEndpoint?: string;\n}\n\n/**\n * Creates an `ensureAnonymousSession(existingUser)` function that signs in\n * anonymously (if not already signed in) and establishes a server-side\n * session cookie. Idempotent: if a user is already signed in, refreshes\n * the cookie with the current user.\n *\n * Usage:\n *\n * const ensureAnonymousSession = createAnonymousAuthHelper({\n * auth: clientAuth,\n * sessionEndpoint: \"/api/auth/session\",\n * });\n * const user = await ensureAnonymousSession(currentUser);\n */\nexport function createAnonymousAuthHelper(deps: AnonymousAuthHelperDeps) {\n const sessionEndpoint = deps.sessionEndpoint ?? \"/api/auth/session\";\n\n return async function ensureAnonymousSession(\n existingUser: User | null,\n ): Promise<User> {\n let user = existingUser ?? deps.auth.currentUser;\n\n if (!user) {\n const { signInAnonymously } = await import(\"firebase/auth\");\n const cred = await signInAnonymously(deps.auth);\n user = cred.user;\n }\n\n const idToken = await user.getIdToken(/* forceRefresh */ true);\n const res = await fetch(sessionEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ idToken }),\n });\n\n if (!res.ok) {\n throw new Error(`No se pudo establecer la sesión (${res.status})`);\n }\n\n return user;\n };\n}\n"]}
@@ -1,31 +1,5 @@
1
1
  import { Auth, User } from 'firebase/auth';
2
2
 
3
- interface PaymentLinkInput {
4
- price: number;
5
- label?: string;
6
- publicLabel?: string;
7
- expiresAt: Date;
8
- tallerId: string;
9
- notes?: string;
10
- }
11
- interface PaymentLink {
12
- id: string;
13
- token: string;
14
- tallerId: string;
15
- price: number;
16
- label: string | null;
17
- publicLabel: string | null;
18
- notes: string | null;
19
- active: boolean;
20
- expiresAt: string;
21
- createdByUid: string;
22
- createdByEmail: string | null;
23
- createdByName: string | null;
24
- timesPaid: number;
25
- lastPaidAt: string | null;
26
- createdAt: string;
27
- updatedAt: string;
28
- }
29
3
  declare const PAYMENT_LINK_PRICE_MIN = 1;
30
4
  declare const PAYMENT_LINK_PRICE_MAX = 2000;
31
5
  declare const PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD = 200;
@@ -57,4 +31,4 @@ interface AnonymousAuthHelperDeps {
57
31
  */
58
32
  declare function createAnonymousAuthHelper(deps: AnonymousAuthHelperDeps): (existingUser: User | null) => Promise<User>;
59
33
 
60
- export { type AnonymousAuthHelperDeps, PAYMENT_LINK_DEFAULT_EXPIRY_DAYS, PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD, PAYMENT_LINK_PRICE_MAX, PAYMENT_LINK_PRICE_MIN, type PaymentLink, type PaymentLinkInput, createAnonymousAuthHelper, defaultExpiry, generatePaymentLinkToken, isExpired };
34
+ export { type AnonymousAuthHelperDeps, PAYMENT_LINK_DEFAULT_EXPIRY_DAYS, PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD, PAYMENT_LINK_PRICE_MAX, PAYMENT_LINK_PRICE_MIN, createAnonymousAuthHelper, defaultExpiry, generatePaymentLinkToken, isExpired };
@@ -1,31 +1,5 @@
1
1
  import { Auth, User } from 'firebase/auth';
2
2
 
3
- interface PaymentLinkInput {
4
- price: number;
5
- label?: string;
6
- publicLabel?: string;
7
- expiresAt: Date;
8
- tallerId: string;
9
- notes?: string;
10
- }
11
- interface PaymentLink {
12
- id: string;
13
- token: string;
14
- tallerId: string;
15
- price: number;
16
- label: string | null;
17
- publicLabel: string | null;
18
- notes: string | null;
19
- active: boolean;
20
- expiresAt: string;
21
- createdByUid: string;
22
- createdByEmail: string | null;
23
- createdByName: string | null;
24
- timesPaid: number;
25
- lastPaidAt: string | null;
26
- createdAt: string;
27
- updatedAt: string;
28
- }
29
3
  declare const PAYMENT_LINK_PRICE_MIN = 1;
30
4
  declare const PAYMENT_LINK_PRICE_MAX = 2000;
31
5
  declare const PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD = 200;
@@ -57,4 +31,4 @@ interface AnonymousAuthHelperDeps {
57
31
  */
58
32
  declare function createAnonymousAuthHelper(deps: AnonymousAuthHelperDeps): (existingUser: User | null) => Promise<User>;
59
33
 
60
- export { type AnonymousAuthHelperDeps, PAYMENT_LINK_DEFAULT_EXPIRY_DAYS, PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD, PAYMENT_LINK_PRICE_MAX, PAYMENT_LINK_PRICE_MIN, type PaymentLink, type PaymentLinkInput, createAnonymousAuthHelper, defaultExpiry, generatePaymentLinkToken, isExpired };
34
+ export { type AnonymousAuthHelperDeps, PAYMENT_LINK_DEFAULT_EXPIRY_DAYS, PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD, PAYMENT_LINK_PRICE_MAX, PAYMENT_LINK_PRICE_MIN, createAnonymousAuthHelper, defaultExpiry, generatePaymentLinkToken, isExpired };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/payment-links/paymentLink.ts","../../src/payment-links/anonymousAuth.ts"],"names":[],"mappings":";;;AAkCO,IAAM,sBAAA,GAAyB;AAC/B,IAAM,sBAAA,GAAyB;AAC/B,IAAM,oCAAA,GAAuC;AAC7C,IAAM,gCAAA,GAAmC;AAEzC,SAAS,wBAAA,GAAmC;AAGjD,EAAA,OAAO,YAAY,EAAE,CAAA,CAClB,QAAA,CAAS,QAAQ,EACjB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,QAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACvB;AAEO,SAAS,aAAA,GAAsB;AACpC,EAAA,MAAM,CAAA,uBAAQ,IAAA,EAAK;AACnB,EAAA,CAAA,CAAE,OAAA,CAAQ,CAAA,CAAE,OAAA,EAAQ,GAAI,gCAAgC,CAAA;AACxD,EAAA,OAAO,CAAA;AACT;AAEO,SAAS,UAAU,SAAA,EAAmC;AAC3D,EAAA,MAAM,MAAM,OAAO,SAAA,KAAc,QAAA,GAAW,SAAA,GAAY,UAAU,WAAA,EAAY;AAC9E,EAAA,OAAO,IAAI,IAAA,CAAK,GAAG,EAAE,OAAA,EAAQ,GAAI,KAAK,GAAA,EAAI;AAC5C;;;ACrBO,SAAS,0BAA0B,IAAA,EAA+B;AACvE,EAAA,MAAM,eAAA,GAAkB,KAAK,eAAA,IAAmB,mBAAA;AAEhD,EAAA,OAAO,eAAe,uBACpB,YAAA,EACe;AACf,IAAA,IAAI,IAAA,GAAO,YAAA,IAAgB,IAAA,CAAK,IAAA,CAAK,WAAA;AAErC,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,EAAE,iBAAA,EAAkB,GAAI,MAAM,OAAO,eAAe,CAAA;AAC1D,MAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,CAAkB,IAAA,CAAK,IAAI,CAAA;AAC9C,MAAA,IAAA,GAAO,IAAA,CAAK,IAAA;AAAA,IACd;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,UAAA;AAAA;AAAA,MAA8B;AAAA,KAAI;AAC7D,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,eAAA,EAAiB;AAAA,MACvC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,SAAS;AAAA,KACjC,CAAA;AAED,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oCAAA,EAAoC,GAAA,CAAI,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,IACnE;AAEA,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AACF","file":"index.js","sourcesContent":["import { randomBytes } from \"crypto\";\n\nexport interface PaymentLinkInput {\n price: number;\n label?: string;\n publicLabel?: string;\n expiresAt: Date;\n tallerId: string;\n notes?: string;\n}\n\nexport interface PaymentLink {\n id: string;\n token: string;\n tallerId: string;\n price: number;\n // Nombre interno (solo admin) — referencia para identificar el link en la\n // tabla y en el dashboard. NUNCA se muestra al cliente.\n label: string | null;\n // Etiqueta promocional visible al cliente en /pago/t/[token] junto al\n // emoji 🔥 (ej. \"Promo amigas\", \"Última semana\"). Opcional.\n publicLabel: string | null;\n notes: string | null;\n active: boolean;\n expiresAt: string;\n createdByUid: string;\n createdByEmail: string | null;\n createdByName: string | null;\n timesPaid: number;\n lastPaidAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\nexport const PAYMENT_LINK_PRICE_MIN = 1;\nexport const PAYMENT_LINK_PRICE_MAX = 2000;\nexport const PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD = 200;\nexport const PAYMENT_LINK_DEFAULT_EXPIRY_DAYS = 30;\n\nexport function generatePaymentLinkToken(): string {\n // 12 bytes → 16 chars base64url, suficientemente largo para evitar adivinanza\n // (2^96 combinaciones) y lo bastante corto para un URL amigable.\n return randomBytes(12)\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n\nexport function defaultExpiry(): Date {\n const d = new Date();\n d.setDate(d.getDate() + PAYMENT_LINK_DEFAULT_EXPIRY_DAYS);\n return d;\n}\n\nexport function isExpired(expiresAt: Date | string): boolean {\n const iso = typeof expiresAt === \"string\" ? expiresAt : expiresAt.toISOString();\n return new Date(iso).getTime() < Date.now();\n}\n","// Source: pauhenriques-website/src/lib/pago-link/anonymousAuth.ts\n// Phase 1.4: refactored to factory pattern.\n//\n// Client-only helper used by guest checkout flows (e.g. payment links) that\n// need a Firebase __session cookie to call the package's POST handlers\n// (charge, 3ds-complete, refund, etc.) — those handlers verify a session\n// cookie, so guests must first sign in anonymously.\n//\n// The two consumer touchpoints (Firebase client Auth instance and session\n// endpoint path) are injected as deps rather than imported from a fixed\n// module path, so the helper works in any Next.js app regardless of how\n// the consumer initializes Firebase.\n\nimport type { Auth, User } from \"firebase/auth\";\n\n/** Dependencies for the anonymous-auth helper factory. */\nexport interface AnonymousAuthHelperDeps {\n /** Firebase client Auth instance. Must be the same instance used by the consumer's app for sign-in state to stay consistent across calls. */\n auth: Auth;\n /** URL where the helper POSTs `{ idToken }` to exchange the Firebase ID token for a server-side session cookie. The endpoint must respond 2xx on success. Default: \"/api/auth/session\". */\n sessionEndpoint?: string;\n}\n\n/**\n * Creates an `ensureAnonymousSession(existingUser)` function that signs in\n * anonymously (if not already signed in) and establishes a server-side\n * session cookie. Idempotent: if a user is already signed in, refreshes\n * the cookie with the current user.\n *\n * Usage:\n *\n * const ensureAnonymousSession = createAnonymousAuthHelper({\n * auth: clientAuth,\n * sessionEndpoint: \"/api/auth/session\",\n * });\n * const user = await ensureAnonymousSession(currentUser);\n */\nexport function createAnonymousAuthHelper(deps: AnonymousAuthHelperDeps) {\n const sessionEndpoint = deps.sessionEndpoint ?? \"/api/auth/session\";\n\n return async function ensureAnonymousSession(\n existingUser: User | null,\n ): Promise<User> {\n let user = existingUser ?? deps.auth.currentUser;\n\n if (!user) {\n const { signInAnonymously } = await import(\"firebase/auth\");\n const cred = await signInAnonymously(deps.auth);\n user = cred.user;\n }\n\n const idToken = await user.getIdToken(/* forceRefresh */ true);\n const res = await fetch(sessionEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ idToken }),\n });\n\n if (!res.ok) {\n throw new Error(`No se pudo establecer la sesión (${res.status})`);\n }\n\n return user;\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/payment-links/paymentLink.ts","../../src/payment-links/anonymousAuth.ts"],"names":[],"mappings":";;;AAQO,IAAM,sBAAA,GAAyB;AAC/B,IAAM,sBAAA,GAAyB;AAC/B,IAAM,oCAAA,GAAuC;AAC7C,IAAM,gCAAA,GAAmC;AAEzC,SAAS,wBAAA,GAAmC;AAGjD,EAAA,OAAO,YAAY,EAAE,CAAA,CAClB,QAAA,CAAS,QAAQ,EACjB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,QAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACvB;AAEO,SAAS,aAAA,GAAsB;AACpC,EAAA,MAAM,CAAA,uBAAQ,IAAA,EAAK;AACnB,EAAA,CAAA,CAAE,OAAA,CAAQ,CAAA,CAAE,OAAA,EAAQ,GAAI,gCAAgC,CAAA;AACxD,EAAA,OAAO,CAAA;AACT;AAEO,SAAS,UAAU,SAAA,EAAmC;AAC3D,EAAA,MAAM,MAAM,OAAO,SAAA,KAAc,QAAA,GAAW,SAAA,GAAY,UAAU,WAAA,EAAY;AAC9E,EAAA,OAAO,IAAI,IAAA,CAAK,GAAG,EAAE,OAAA,EAAQ,GAAI,KAAK,GAAA,EAAI;AAC5C;;;ACKO,SAAS,0BAA0B,IAAA,EAA+B;AACvE,EAAA,MAAM,eAAA,GAAkB,KAAK,eAAA,IAAmB,mBAAA;AAEhD,EAAA,OAAO,eAAe,uBACpB,YAAA,EACe;AACf,IAAA,IAAI,IAAA,GAAO,YAAA,IAAgB,IAAA,CAAK,IAAA,CAAK,WAAA;AAErC,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,EAAE,iBAAA,EAAkB,GAAI,MAAM,OAAO,eAAe,CAAA;AAC1D,MAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,CAAkB,IAAA,CAAK,IAAI,CAAA;AAC9C,MAAA,IAAA,GAAO,IAAA,CAAK,IAAA;AAAA,IACd;AAEA,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,UAAA;AAAA;AAAA,MAA8B;AAAA,KAAI;AAC7D,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,eAAA,EAAiB;AAAA,MACvC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,MAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE,SAAS;AAAA,KACjC,CAAA;AAED,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,oCAAA,EAAoC,GAAA,CAAI,MAAM,CAAA,CAAA,CAAG,CAAA;AAAA,IACnE;AAEA,IAAA,OAAO,IAAA;AAAA,EACT,CAAA;AACF","file":"index.js","sourcesContent":["import { randomBytes } from \"crypto\";\n\n// NOTE: this module intentionally exposes only generic payment-link PRIMITIVES\n// (token generation, expiry helpers, price bounds). The shape of a payment link\n// document — what it points to (a workshop, a course, a session…), its labels,\n// usage counters, etc. — is consumer-specific, so consumers define their own\n// PaymentLink type and build their own resolver on top of these primitives.\n\nexport const PAYMENT_LINK_PRICE_MIN = 1;\nexport const PAYMENT_LINK_PRICE_MAX = 2000;\nexport const PAYMENT_LINK_PRICE_CONFIRM_THRESHOLD = 200;\nexport const PAYMENT_LINK_DEFAULT_EXPIRY_DAYS = 30;\n\nexport function generatePaymentLinkToken(): string {\n // 12 bytes → 16 chars base64url, suficientemente largo para evitar adivinanza\n // (2^96 combinaciones) y lo bastante corto para un URL amigable.\n return randomBytes(12)\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n\nexport function defaultExpiry(): Date {\n const d = new Date();\n d.setDate(d.getDate() + PAYMENT_LINK_DEFAULT_EXPIRY_DAYS);\n return d;\n}\n\nexport function isExpired(expiresAt: Date | string): boolean {\n const iso = typeof expiresAt === \"string\" ? expiresAt : expiresAt.toISOString();\n return new Date(iso).getTime() < Date.now();\n}\n","// Source: pauhenriques-website/src/lib/pago-link/anonymousAuth.ts\n// Phase 1.4: refactored to factory pattern.\n//\n// Client-only helper used by guest checkout flows (e.g. payment links) that\n// need a Firebase __session cookie to call the package's POST handlers\n// (charge, 3ds-complete, refund, etc.) — those handlers verify a session\n// cookie, so guests must first sign in anonymously.\n//\n// The two consumer touchpoints (Firebase client Auth instance and session\n// endpoint path) are injected as deps rather than imported from a fixed\n// module path, so the helper works in any Next.js app regardless of how\n// the consumer initializes Firebase.\n\nimport type { Auth, User } from \"firebase/auth\";\n\n/** Dependencies for the anonymous-auth helper factory. */\nexport interface AnonymousAuthHelperDeps {\n /** Firebase client Auth instance. Must be the same instance used by the consumer's app for sign-in state to stay consistent across calls. */\n auth: Auth;\n /** URL where the helper POSTs `{ idToken }` to exchange the Firebase ID token for a server-side session cookie. The endpoint must respond 2xx on success. Default: \"/api/auth/session\". */\n sessionEndpoint?: string;\n}\n\n/**\n * Creates an `ensureAnonymousSession(existingUser)` function that signs in\n * anonymously (if not already signed in) and establishes a server-side\n * session cookie. Idempotent: if a user is already signed in, refreshes\n * the cookie with the current user.\n *\n * Usage:\n *\n * const ensureAnonymousSession = createAnonymousAuthHelper({\n * auth: clientAuth,\n * sessionEndpoint: \"/api/auth/session\",\n * });\n * const user = await ensureAnonymousSession(currentUser);\n */\nexport function createAnonymousAuthHelper(deps: AnonymousAuthHelperDeps) {\n const sessionEndpoint = deps.sessionEndpoint ?? \"/api/auth/session\";\n\n return async function ensureAnonymousSession(\n existingUser: User | null,\n ): Promise<User> {\n let user = existingUser ?? deps.auth.currentUser;\n\n if (!user) {\n const { signInAnonymously } = await import(\"firebase/auth\");\n const cred = await signInAnonymously(deps.auth);\n user = cred.user;\n }\n\n const idToken = await user.getIdToken(/* forceRefresh */ true);\n const res = await fetch(sessionEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ idToken }),\n });\n\n if (!res.ok) {\n throw new Error(`No se pudo establecer la sesión (${res.status})`);\n }\n\n return user;\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pandait.tech/payment-nuvei",
3
- "version": "0.5.0",
3
+ "version": "1.0.1",
4
4
  "description": "Nuvei Ecuador payment gateway adapter for Next.js: charge handler, webhook, 3DS flow, refunds, tokenized cards, and payment links. Part of panda-commerce-kit.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -91,6 +91,7 @@
91
91
  "@tailwindcss/cli": "^4",
92
92
  "@types/node": "^22.7.5",
93
93
  "@types/react": "^18.3.12",
94
+ "@vitest/coverage-v8": "^4.1.7",
94
95
  "firebase": "^11.0.0",
95
96
  "firebase-admin": "^12.7.0",
96
97
  "next": "^14.2.15",
@@ -107,7 +108,7 @@
107
108
  "build:css": "tailwindcss -i src/ui/styles.css -o dist/ui/styles.css --minify",
108
109
  "dev": "tsup --watch",
109
110
  "type-check": "tsc --noEmit",
110
- "test": "vitest run",
111
+ "test": "vitest run --coverage",
111
112
  "test:watch": "vitest",
112
113
  "lint": "eslint src",
113
114
  "clean": "rm -rf dist .turbo"