@waffo/pancake-nextjs 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to `@waffo/pancake-nextjs` will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.9.0] - 2026-10-10
8
+
9
+ Customers signed in to a merchant's app can enter the store's customer portal without email verification.
10
+
11
+ ### Added
12
+
13
+ - **`createCustomerPortalRedirect(config, { resolveAuthenticatedCustomer, loginUrl?, onError? })`** in `@waffo/pancake-nextjs/server` — returns a GET Route Handler. It asks your `resolveAuthenticatedCustomer(request)` for `{ storeId, buyerIdentity }` from your own authenticated session, calls `client.auth.createCustomerPortalLink()` and answers `302` to the tokenized portal URL. A `null` resolver result answers `302` to `loginUrl`, or `401` when it is not set; a failed link creation goes to `onError`, or a generic `502`. Every response the handler builds sets `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. The SDK reads no identity from the request itself.
14
+ - **Default error logging in `createCustomerPortalRedirect`.** Without `onError`, a failed link creation writes one `console.error` line — `[waffo-pancake] createCustomerPortalRedirect failed: <error message>` — before the generic `502`. Only the message is logged, never the error object; a `#token=` fragment is redacted. With `onError` nothing is logged: that is where your own logging goes.
15
+ - **`AuthenticatedPortalCustomer`, `CustomerPortalRedirectOptions`, `CustomerPortalRouteHandler`** types.
16
+
17
+ ### Changed
18
+
19
+ - **`createCustomerTokenAction` documentation** states that it is a generic token primitive and must not be used as an unauthenticated portal entry. Its behavior is unchanged.
20
+ - **`@waffo/pancake-ts` dependency raised to `^0.26.0`** — the version that adds `auth.createCustomerPortalLink()`. The range also picks up 0.24.0 (webhook amount fields; the six generic amount fields are deprecated, not removed) and 0.25.0 (refund payloads carry what the refunded payment collected); neither is breaking.
21
+
7
22
  ## [0.8.0] - 2026-09-20
8
23
 
9
24
  Picks up plan change links from `@waffo/pancake-ts` 0.23.0, **and its idempotency change** — see the BREAKING entries under Changed before upgrading.
package/README.md CHANGED
@@ -386,6 +386,53 @@ function AccountPage() {
386
386
 
387
387
  Action hooks return `{ execute, isLoading, error, data }`. Data hooks return `{ data, isLoading, error, refetch }`.
388
388
 
389
+ ## Customer Portal (SSO)
390
+
391
+ Send a customer who is signed in to **your** app straight into your store's Pancake customer portal — orders, subscriptions and payments — without the portal's email verification step. `createCustomerPortalRedirect` returns a GET Route Handler; link to it from your UI.
392
+
393
+ ```ts
394
+ // app/account/billing-portal/route.ts
395
+ import { createCustomerPortalRedirect } from "@waffo/pancake-nextjs/server";
396
+ import { auth } from "@/lib/auth"; // your auth library
397
+
398
+ export const dynamic = "force-dynamic"; // per-visit session check and redirect, never statically cached
399
+
400
+ export const GET = createCustomerPortalRedirect(
401
+ { merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY! },
402
+ {
403
+ // Identity comes from YOUR session — never from the request's query, body or headers
404
+ resolveAuthenticatedCustomer: async () => {
405
+ const session = await auth();
406
+ if (!session?.user) return null;
407
+ return { storeId: process.env.WAFFO_STORE_ID!, buyerIdentity: session.user.id };
408
+ },
409
+ loginUrl: "/login?returnTo=/account/billing-portal", // omit to answer 401
410
+ },
411
+ );
412
+ ```
413
+
414
+ ```tsx
415
+ <a href="/account/billing-portal">Manage billing</a>
416
+ ```
417
+
418
+ | Situation | Response |
419
+ | --------------------------- | ----------------------------------------------------------------------------------------------------------------- |
420
+ | Resolver returns a customer | `302` to the portal URL (carries `#token=...`) |
421
+ | Resolver returns `null` | `302` to `loginUrl`, or `401 { error: "Unauthorized" }` when `loginUrl` is not set |
422
+ | Portal link creation fails | `onError(error, request)` if given, otherwise one `console.error` line and `502 { error }` with a generic message |
423
+ | Resolver throws | The error propagates (Next.js answers 500) |
424
+
425
+ Every response the handler builds (all but a custom `onError` one) carries `Cache-Control: no-store` and `Referrer-Policy: no-referrer`, so the tokenized `Location` is neither cached nor forwarded as a Referer.
426
+
427
+ - **Why a Route Handler, not a Server Action**: the portal entry is a plain link navigation. An HTTP redirect keeps the tokenized portal URL out of client JavaScript and lets the response set `no-store` / `no-referrer`; a Server Action would have to hand the URL to the browser to navigate. It follows the same factory shape as `Webhook()`, but lives in `/server` because it holds the private key.
428
+ - **`buyerIdentity` must be the value you use for authenticated checkout**, so the portal shows the orders placed under it. Whoever controls `buyerIdentity` gets that customer's portal — the resolver is the security boundary.
429
+ - **Environment follows the API Key** in `config`: a test key opens the test portal, a prod key the prod portal. `config.environment` is not used here.
430
+ - **When the portal session expires**, the customer comes back through this route and gets a new link.
431
+ - **Logging is yours to add in `onError`.** Without it, the handler writes one server-side line — `[waffo-pancake] createCustomerPortalRedirect failed: <error message>`, with any `#token=` fragment redacted — and never the error object, the portal URL or the request. Pass `onError` to send failures to your own logger or error tracker, and keep tokens and portal URLs out of what you record.
432
+ - **Do not build `loginUrl` by copying request parameters verbatim.** A string or function that echoes, say, `?next=` from the request into the redirect turns this route into an open redirect. Use constants, or validate against an allow-list of your own paths.
433
+
434
+ > **Do not build a portal entry on `createCustomerTokenAction`.** It is a generic token primitive that signs whatever `buyerIdentity` it is called with, and a Server Action can be invoked from the browser with arbitrary arguments. Exposed as an unauthenticated portal or sign-in entry, it hands out any customer's token. Use `createCustomerPortalRedirect`, which only takes identity from your resolver.
435
+
389
436
  ## Business-Side Identifiers
390
437
 
391
438
  Attach your own internal references to a checkout or a refund ticket so cross-system reconciliation does not require Waffo IDs. Two flat keys, both optional (max 128 chars):
@@ -459,14 +506,15 @@ All merchant hooks return `{ data, isLoading, error, refetch }`.
459
506
 
460
507
  ## Server Actions
461
508
 
462
- | Factory | Returns | Description |
463
- | ------------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
464
- | `createCheckoutAction(config)` | `CheckoutAction` | Checkout session creation (anonymous + authenticated) and plan change links (`planChange` + `authenticatedPlanChange`) |
465
- | `createCustomerTokenAction(config)` | `CustomerTokenAction` | Customer session token issuance |
466
- | `createCustomerSessionAction(config)` | `CustomerSessionAction` | Customer self-service operations |
467
- | `createMerchantQueryAction(config)` | `MerchantQueryAction` | Merchant GraphQL queries |
509
+ | Factory | Returns | Description |
510
+ | ----------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
511
+ | `createCheckoutAction(config)` | `CheckoutAction` | Checkout session creation (anonymous + authenticated) and plan change links (`planChange` + `authenticatedPlanChange`) |
512
+ | `createCustomerTokenAction(config)` | `CustomerTokenAction` | Customer session token issuance |
513
+ | `createCustomerPortalRedirect(config, options)` | `CustomerPortalRouteHandler` | GET Route Handler: 302 to the customer portal for the customer your session resolver returns — see [Customer Portal (SSO)](#customer-portal-sso) |
514
+ | `createCustomerSessionAction(config)` | `CustomerSessionAction` | Customer self-service operations |
515
+ | `createMerchantQueryAction(config)` | `MerchantQueryAction` | Merchant GraphQL queries |
468
516
 
469
- Import from `@waffo/pancake-nextjs/server`. Config requires `merchantId` and `privateKey`.
517
+ Import from `@waffo/pancake-nextjs/server`. Config requires `merchantId` and `privateKey`. `createCustomerTokenAction` signs any `buyerIdentity` it receives: call it only with identity from your own session, never as an unauthenticated portal or sign-in entry.
470
518
 
471
519
  ## Idempotency
472
520
 
@@ -499,7 +547,7 @@ Uniqueness is yours to guarantee (at most 256 characters of letters, numbers, hy
499
547
 
500
548
  ### Types
501
549
 
502
- Key types: `PriceInfo`, `PriceSnapshot`, `BillingDetail`, `RequestOptions`, `WebhookEvent`, `CheckoutAction`, `CustomerTokenAction`, `CustomerSessionAction`, `MerchantQueryAction`, `CustomerConfig`, `CheckoutButtonProps`, `CheckoutMode`, `CashierLanguage`, `UseCheckoutReturn`, `UseCustomerReturn`, `CustomerActionState<T>`, `QueryState<T>`, `SalesOverview`, `SubscriptionOverview`, `WebhookConfig`.
550
+ Key types: `PriceInfo`, `PriceSnapshot`, `BillingDetail`, `RequestOptions`, `WebhookEvent`, `CheckoutAction`, `CustomerTokenAction`, `CustomerSessionAction`, `MerchantQueryAction`, `CustomerPortalRouteHandler`, `CustomerPortalRedirectOptions`, `AuthenticatedPortalCustomer`, `CustomerConfig`, `CheckoutButtonProps`, `CheckoutMode`, `CashierLanguage`, `UseCheckoutReturn`, `UseCustomerReturn`, `CustomerActionState<T>`, `QueryState<T>`, `SalesOverview`, `SubscriptionOverview`, `WebhookConfig`.
503
551
 
504
552
  ## Development
505
553
 
package/dist/server.cjs CHANGED
@@ -23,6 +23,7 @@ __export(server_exports, {
23
23
  createBuyerSessionAction: () => createBuyerSessionAction,
24
24
  createBuyerTokenAction: () => createBuyerTokenAction,
25
25
  createCheckoutAction: () => createCheckoutAction,
26
+ createCustomerPortalRedirect: () => createCustomerPortalRedirect,
26
27
  createCustomerSessionAction: () => createCustomerSessionAction,
27
28
  createCustomerTokenAction: () => createCustomerTokenAction,
28
29
  createMerchantQueryAction: () => createMerchantQueryAction
@@ -54,6 +55,55 @@ function createCustomerTokenAction(config) {
54
55
  return client.auth.issueSessionToken(params, options);
55
56
  };
56
57
  }
58
+ function portalHeaders(extra) {
59
+ const headers = new Headers(extra);
60
+ headers.set("Cache-Control", "no-store");
61
+ headers.set("Referrer-Policy", "no-referrer");
62
+ return headers;
63
+ }
64
+ function safeErrorMessage(error) {
65
+ const message = error instanceof Error ? error.message : String(error);
66
+ return message.replace(/#token=[^\s"'&]*/g, "#token=[redacted]").replace(/\s+/g, " ");
67
+ }
68
+ function createCustomerPortalRedirect(config, options) {
69
+ const client = new import_pancake_ts.WaffoPancake(config);
70
+ return async function GET(request) {
71
+ const customer = await options.resolveAuthenticatedCustomer(request);
72
+ if (!customer) {
73
+ if (options.loginUrl === void 0) {
74
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
75
+ status: 401,
76
+ headers: portalHeaders({ "Content-Type": "application/json" })
77
+ });
78
+ }
79
+ const loginUrl = typeof options.loginUrl === "function" ? options.loginUrl(request) : options.loginUrl;
80
+ return new Response(null, {
81
+ status: 302,
82
+ headers: portalHeaders({ Location: new URL(loginUrl, request.url).toString() })
83
+ });
84
+ }
85
+ let portalUrl;
86
+ try {
87
+ ({ portalUrl } = await client.auth.createCustomerPortalLink({
88
+ storeId: customer.storeId,
89
+ buyerIdentity: customer.buyerIdentity
90
+ }));
91
+ } catch (error) {
92
+ if (options.onError) {
93
+ return options.onError(error, request);
94
+ }
95
+ console.error("[waffo-pancake] createCustomerPortalRedirect failed:", safeErrorMessage(error));
96
+ return new Response(JSON.stringify({ error: "Failed to create customer portal link" }), {
97
+ status: 502,
98
+ headers: portalHeaders({ "Content-Type": "application/json" })
99
+ });
100
+ }
101
+ return new Response(null, {
102
+ status: 302,
103
+ headers: portalHeaders({ Location: portalUrl })
104
+ });
105
+ };
106
+ }
57
107
  function createCustomerSessionAction(config) {
58
108
  const client = new import_pancake_ts.WaffoPancake(config);
59
109
  return async (token, actionType, params, options) => {
@@ -91,6 +141,7 @@ var createBuyerSessionAction = createCustomerSessionAction;
91
141
  createBuyerSessionAction,
92
142
  createBuyerTokenAction,
93
143
  createCheckoutAction,
144
+ createCustomerPortalRedirect,
94
145
  createCustomerSessionAction,
95
146
  createCustomerTokenAction,
96
147
  createMerchantQueryAction
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side entry point for @waffo/pancake-nextjs.\n *\n * All functions in this module use the private key for RSA signing\n * and must only run on the server (Next.js Server Actions / Route Handlers).\n *\n * @example\n * ```ts\n * // app/lib/waffo.ts\n * \"use server\";\n * import { createCheckoutAction, createCustomerTokenAction, createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * const config = {\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * };\n *\n * export const checkout = createCheckoutAction(config);\n * export const issueCustomerToken = createCustomerTokenAction(config);\n * export const merchantQuery = createMerchantQueryAction(config);\n * ```\n */\nimport { WaffoPancake } from \"@waffo/pancake-ts\";\n\nimport type {\n RequestOptions,\n WaffoPancakeConfig,\n AnonymousCheckoutParams,\n AuthenticatedCheckoutParams,\n AuthenticatedPlanChangeParams,\n CreatePlanChangeSessionParams,\n CheckoutSessionResult,\n AuthenticatedCheckoutResult,\n IssueSessionTokenParams,\n SessionToken,\n GraphQLParams,\n GraphQLResponse,\n} from \"@waffo/pancake-ts\";\n\n// ============================================================\n// Checkout Action\n// ============================================================\n\n/**\n * Parameters for the checkout server action.\n *\n * The `type` field selects the flow: a new purchase (`anonymous` / `authenticated`)\n * or a plan change for an existing subscription (`planChange` /\n * `authenticatedPlanChange`, which carry the required `originOrderId`).\n */\nexport type CheckoutActionParams =\n | ({ type?: \"anonymous\" } & AnonymousCheckoutParams)\n | ({ type: \"authenticated\" } & AuthenticatedCheckoutParams)\n | ({ type: \"planChange\" } & CreatePlanChangeSessionParams)\n | ({ type: \"authenticatedPlanChange\" } & AuthenticatedPlanChangeParams);\n\n/** Result of the checkout server action */\nexport type CheckoutActionResult = CheckoutSessionResult | AuthenticatedCheckoutResult;\n\n/**\n * Server action signature for checkout.\n *\n * The optional `options` carries an `idempotencyKey`; without one no key is sent\n * and a retried call creates a second session.\n */\nexport type CheckoutAction = (params: CheckoutActionParams, options?: RequestOptions) => Promise<CheckoutActionResult>;\n\n/**\n * Create a server action that handles checkout session creation.\n *\n * The private key is captured in the closure and never sent to the client.\n *\n * Handles both new purchases and plan changes; `params.type` selects the flow.\n *\n * @param config - WaffoPancake client configuration (merchantId + privateKey)\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCheckoutAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const checkout = createCheckoutAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n *\n * @example\n * ```ts\n * // Plan change link for an existing subscription — originOrderId is required\n * const session = await checkout({\n * type: \"planChange\",\n * originOrderId: \"ORD_xxx\",\n * productId: \"PROD_target_plan\",\n * currency: \"USD\",\n * });\n * // session.checkoutUrl points at the change confirmation page\n * ```\n */\nexport function createCheckoutAction(config: WaffoPancakeConfig): CheckoutAction {\n const client = new WaffoPancake(config);\n\n return async (params: CheckoutActionParams, options?: RequestOptions): Promise<CheckoutActionResult> => {\n if (params.type === \"authenticated\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.create(sdkParams, options);\n }\n if (params.type === \"planChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.createPlanChangeSession(sdkParams, options);\n }\n if (params.type === \"authenticatedPlanChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.createPlanChange(sdkParams, options);\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.anonymous.create(sdkParams, options);\n };\n}\n\n// ============================================================\n// Customer Token Action\n// ============================================================\n\n/** Server action signature for issuing customer tokens */\nexport type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOptions) => Promise<SessionToken>;\n\n/**\n * Create a server action that issues customer session tokens.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerTokenAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const issueCustomerToken = createCustomerTokenAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createCustomerTokenAction(config: WaffoPancakeConfig): CustomerTokenAction {\n const client = new WaffoPancake(config);\n\n return async (params: IssueSessionTokenParams, options?: RequestOptions): Promise<SessionToken> => {\n return client.auth.issueSessionToken(params, options);\n };\n}\n\n// ============================================================\n// Customer Session Action\n// ============================================================\n\n/** Customer action types */\nexport type CustomerSessionActionType =\n | \"cancelSubscription\"\n | \"cancelOnetimeOrder\"\n | \"reactivateSubscription\"\n | \"createRefundTicket\"\n | \"resubmitRefundTicket\"\n | \"createPlanChangeSession\"\n | \"query\";\n\n/** Server action signature for customer session operations */\nexport type CustomerSessionAction = (\n token: string,\n actionType: CustomerSessionActionType,\n params: unknown,\n options?: RequestOptions,\n) => Promise<unknown>;\n\n/**\n * Create a server action that executes customer self-service operations.\n *\n * `config.environment` is required — session tokens carry no environment, and the\n * gateway rejects a session request without the matching header.\n *\n * `createPlanChangeSession` is the customer-driven half of a plan change: it returns\n * a confirmation-page URL, and the platform allows it only when the subscription\n * belongs to that customer, the target plan is in the same product group, and that\n * group's `selfServicePlanChange` is on — otherwise 403. The merchant-only pricing\n * fields are not part of its params; the platform drops them on this path silently.\n *\n * No call sends an idempotency key unless you pass one (`options.idempotencyKey`,\n * the last argument) — the same rule as every other method in the SDK — so a write\n * retried after a timeout executes twice.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerSessionAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const customerAction = createCustomerSessionAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * environment: \"test\",\n * });\n * ```\n */\nexport function createCustomerSessionAction(config: WaffoPancakeConfig): CustomerSessionAction {\n const client = new WaffoPancake(config);\n\n return async (token: string, actionType: CustomerSessionActionType, params: unknown, options?: RequestOptions): Promise<unknown> => {\n const customer = client.buyer(token);\n switch (actionType) {\n case \"cancelSubscription\":\n return customer.cancelSubscription(params as Parameters<typeof customer.cancelSubscription>[0], options);\n case \"cancelOnetimeOrder\":\n return customer.cancelOnetimeOrder(params as Parameters<typeof customer.cancelOnetimeOrder>[0], options);\n case \"reactivateSubscription\":\n return customer.reactivateSubscription(params as Parameters<typeof customer.reactivateSubscription>[0], options);\n case \"createRefundTicket\":\n return customer.createRefundTicket(params as Parameters<typeof customer.createRefundTicket>[0], options);\n case \"resubmitRefundTicket\":\n return customer.resubmitRefundTicket(params as Parameters<typeof customer.resubmitRefundTicket>[0], options);\n case \"createPlanChangeSession\":\n return customer.createPlanChangeSession(params as Parameters<typeof customer.createPlanChangeSession>[0], options);\n case \"query\":\n // Reads take no key: a cached replay would serve stale data.\n return customer.graphql.query(params as GraphQLParams);\n default:\n throw new Error(`Unknown customer action: ${actionType}`);\n }\n };\n}\n\n// ============================================================\n// Merchant Query Action\n// ============================================================\n\n/** Server action signature for merchant GraphQL queries */\nexport type MerchantQueryAction = (params: GraphQLParams) => Promise<GraphQLResponse>;\n\n/**\n * Create a server action that executes merchant GraphQL queries.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const merchantQuery = createMerchantQueryAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createMerchantQueryAction(config: WaffoPancakeConfig): MerchantQueryAction {\n const client = new WaffoPancake(config);\n\n return async (params: GraphQLParams): Promise<GraphQLResponse> => {\n return client.graphql.query(params);\n };\n}\n\n// ============================================================\n// Deprecated Aliases\n// ============================================================\n\n/** @deprecated Use {@link CustomerTokenAction} instead. */\nexport type BuyerTokenAction = CustomerTokenAction;\n\n/** @deprecated Use {@link createCustomerTokenAction} instead. */\nexport const createBuyerTokenAction = createCustomerTokenAction;\n\n/** @deprecated Use {@link CustomerSessionActionType} instead. */\nexport type BuyerSessionActionType = CustomerSessionActionType;\n\n/** @deprecated Use {@link CustomerSessionAction} instead. */\nexport type BuyerSessionAction = CustomerSessionAction;\n\n/** @deprecated Use {@link createCustomerSessionAction} instead. */\nexport const createBuyerSessionAction = createCustomerSessionAction;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBA,wBAA6B;AA8EtB,SAAS,qBAAqB,QAA4C;AAC/E,QAAM,SAAS,IAAI,+BAAa,MAAM;AAEtC,SAAO,OAAO,QAA8B,YAA4D;AACtG,QAAI,OAAO,SAAS,iBAAiB;AAEnC,YAAM,EAAE,MAAAA,OAAM,GAAGC,WAAU,IAAI;AAC/B,aAAO,OAAO,SAAS,cAAc,OAAOA,YAAW,OAAO;AAAA,IAChE;AACA,QAAI,OAAO,SAAS,cAAc;AAEhC,YAAM,EAAE,MAAAD,OAAM,GAAGC,WAAU,IAAI;AAC/B,aAAO,OAAO,SAAS,wBAAwBA,YAAW,OAAO;AAAA,IACnE;AACA,QAAI,OAAO,SAAS,2BAA2B;AAE7C,YAAM,EAAE,MAAAD,OAAM,GAAGC,WAAU,IAAI;AAC/B,aAAO,OAAO,SAAS,cAAc,iBAAiBA,YAAW,OAAO;AAAA,IAC1E;AAEA,UAAM,EAAE,MAAM,GAAG,UAAU,IAAI;AAC/B,WAAO,OAAO,SAAS,UAAU,OAAO,WAAW,OAAO;AAAA,EAC5D;AACF;AA0BO,SAAS,0BAA0B,QAAiD;AACzF,QAAM,SAAS,IAAI,+BAAa,MAAM;AAEtC,SAAO,OAAO,QAAiC,YAAoD;AACjG,WAAO,OAAO,KAAK,kBAAkB,QAAQ,OAAO;AAAA,EACtD;AACF;AAuDO,SAAS,4BAA4B,QAAmD;AAC7F,QAAM,SAAS,IAAI,+BAAa,MAAM;AAEtC,SAAO,OAAO,OAAe,YAAuC,QAAiB,YAA+C;AAClI,UAAM,WAAW,OAAO,MAAM,KAAK;AACnC,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,eAAO,SAAS,mBAAmB,QAA6D,OAAO;AAAA,MACzG,KAAK;AACH,eAAO,SAAS,mBAAmB,QAA6D,OAAO;AAAA,MACzG,KAAK;AACH,eAAO,SAAS,uBAAuB,QAAiE,OAAO;AAAA,MACjH,KAAK;AACH,eAAO,SAAS,mBAAmB,QAA6D,OAAO;AAAA,MACzG,KAAK;AACH,eAAO,SAAS,qBAAqB,QAA+D,OAAO;AAAA,MAC7G,KAAK;AACH,eAAO,SAAS,wBAAwB,QAAkE,OAAO;AAAA,MACnH,KAAK;AAEH,eAAO,SAAS,QAAQ,MAAM,MAAuB;AAAA,MACvD;AACE,cAAM,IAAI,MAAM,4BAA4B,UAAU,EAAE;AAAA,IAC5D;AAAA,EACF;AACF;AA0BO,SAAS,0BAA0B,QAAiD;AACzF,QAAM,SAAS,IAAI,+BAAa,MAAM;AAEtC,SAAO,OAAO,WAAoD;AAChE,WAAO,OAAO,QAAQ,MAAM,MAAM;AAAA,EACpC;AACF;AAUO,IAAM,yBAAyB;AAS/B,IAAM,2BAA2B;","names":["type","sdkParams"]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side entry point for @waffo/pancake-nextjs.\n *\n * All functions in this module use the private key for RSA signing\n * and must only run on the server (Next.js Server Actions / Route Handlers).\n *\n * @example\n * ```ts\n * // app/lib/waffo.ts\n * \"use server\";\n * import { createCheckoutAction, createCustomerTokenAction, createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * const config = {\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * };\n *\n * export const checkout = createCheckoutAction(config);\n * export const issueCustomerToken = createCustomerTokenAction(config);\n * export const merchantQuery = createMerchantQueryAction(config);\n * ```\n *\n * @example\n * ```ts\n * // app/account/billing-portal/route.ts — customer portal entry from your own login\n * import { createCustomerPortalRedirect } from \"@waffo/pancake-nextjs/server\";\n *\n * export const GET = createCustomerPortalRedirect(config, {\n * resolveAuthenticatedCustomer: async (request) => getSignedInCustomer(request), // your session\n * });\n * ```\n */\nimport { WaffoPancake } from \"@waffo/pancake-ts\";\n\nimport type {\n RequestOptions,\n WaffoPancakeConfig,\n AnonymousCheckoutParams,\n AuthenticatedCheckoutParams,\n AuthenticatedPlanChangeParams,\n CreatePlanChangeSessionParams,\n CheckoutSessionResult,\n AuthenticatedCheckoutResult,\n IssueSessionTokenParams,\n SessionToken,\n GraphQLParams,\n GraphQLResponse,\n} from \"@waffo/pancake-ts\";\n\n// ============================================================\n// Checkout Action\n// ============================================================\n\n/**\n * Parameters for the checkout server action.\n *\n * The `type` field selects the flow: a new purchase (`anonymous` / `authenticated`)\n * or a plan change for an existing subscription (`planChange` /\n * `authenticatedPlanChange`, which carry the required `originOrderId`).\n */\nexport type CheckoutActionParams =\n | ({ type?: \"anonymous\" } & AnonymousCheckoutParams)\n | ({ type: \"authenticated\" } & AuthenticatedCheckoutParams)\n | ({ type: \"planChange\" } & CreatePlanChangeSessionParams)\n | ({ type: \"authenticatedPlanChange\" } & AuthenticatedPlanChangeParams);\n\n/** Result of the checkout server action */\nexport type CheckoutActionResult = CheckoutSessionResult | AuthenticatedCheckoutResult;\n\n/**\n * Server action signature for checkout.\n *\n * The optional `options` carries an `idempotencyKey`; without one no key is sent\n * and a retried call creates a second session.\n */\nexport type CheckoutAction = (params: CheckoutActionParams, options?: RequestOptions) => Promise<CheckoutActionResult>;\n\n/**\n * Create a server action that handles checkout session creation.\n *\n * The private key is captured in the closure and never sent to the client.\n *\n * Handles both new purchases and plan changes; `params.type` selects the flow.\n *\n * @param config - WaffoPancake client configuration (merchantId + privateKey)\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCheckoutAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const checkout = createCheckoutAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n *\n * @example\n * ```ts\n * // Plan change link for an existing subscription — originOrderId is required\n * const session = await checkout({\n * type: \"planChange\",\n * originOrderId: \"ORD_xxx\",\n * productId: \"PROD_target_plan\",\n * currency: \"USD\",\n * });\n * // session.checkoutUrl points at the change confirmation page\n * ```\n */\nexport function createCheckoutAction(config: WaffoPancakeConfig): CheckoutAction {\n const client = new WaffoPancake(config);\n\n return async (params: CheckoutActionParams, options?: RequestOptions): Promise<CheckoutActionResult> => {\n if (params.type === \"authenticated\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.create(sdkParams, options);\n }\n if (params.type === \"planChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.createPlanChangeSession(sdkParams, options);\n }\n if (params.type === \"authenticatedPlanChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.createPlanChange(sdkParams, options);\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.anonymous.create(sdkParams, options);\n };\n}\n\n// ============================================================\n// Customer Token Action\n// ============================================================\n\n/** Server action signature for issuing customer tokens */\nexport type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOptions) => Promise<SessionToken>;\n\n/**\n * Create a server action that issues customer session tokens.\n *\n * This is a generic token primitive: it signs whatever `buyerIdentity` it is\n * handed. Never expose it to the browser as a portal or sign-in entry — a\n * Server Action is callable with arbitrary arguments, so a client-chosen\n * `buyerIdentity` would yield another customer's token. To send a signed-in\n * customer to the customer portal, use {@link createCustomerPortalRedirect},\n * which derives identity from your own session.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerTokenAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const issueCustomerToken = createCustomerTokenAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createCustomerTokenAction(config: WaffoPancakeConfig): CustomerTokenAction {\n const client = new WaffoPancake(config);\n\n return async (params: IssueSessionTokenParams, options?: RequestOptions): Promise<SessionToken> => {\n return client.auth.issueSessionToken(params, options);\n };\n}\n\n// ============================================================\n// Customer Portal Redirect\n// ============================================================\n\n/**\n * Store and customer identity for a customer portal link, resolved by the\n * merchant app from its own authenticated session.\n */\nexport interface AuthenticatedPortalCustomer {\n /** Store ID (Short ID `STO_xxx`) whose portal the customer enters */\n storeId: string;\n /** Customer identity in your system — the same value used for authenticated checkout */\n buyerIdentity: string;\n}\n\n/** Options for {@link createCustomerPortalRedirect}. */\nexport interface CustomerPortalRedirectOptions {\n /**\n * Resolve the signed-in customer from YOUR app's authenticated session\n * (session cookie, auth library, …). Return `null` when nobody is signed in.\n *\n * Must not read `buyerIdentity` or `storeId` from anything the browser\n * controls — query string, body, form fields or custom headers. Whoever\n * controls `buyerIdentity` gets that customer's portal.\n */\n resolveAuthenticatedCustomer: (request: Request) => Promise<AuthenticatedPortalCustomer | null> | AuthenticatedPortalCustomer | null;\n /**\n * Where to send a visitor for whom the resolver returned `null`. A path or\n * absolute URL, resolved against the request URL; a function receives the\n * request (e.g. to add your own `returnTo`). Omit to answer 401 instead.\n *\n * Build it from constants or an allow-list, never by copying request\n * parameters verbatim (e.g. a `?next=` value) — that turns this route into an\n * open redirect.\n */\n loginUrl?: string | ((request: Request) => string);\n /**\n * Build the response when creating the portal link fails (API error, invalid\n * store ID, network error). This is the place for your own logging and error\n * reporting. Defaults to a 502 JSON `{ error }` response with a generic\n * message, after one `console.error` line carrying only the error message;\n * the error itself is not echoed to the browser.\n */\n onError?: (error: unknown, request: Request) => Response | Promise<Response>;\n}\n\n/** GET Route Handler returned by {@link createCustomerPortalRedirect}. */\nexport type CustomerPortalRouteHandler = (request: Request) => Promise<Response>;\n\n/**\n * Build headers for a portal route response. Every response — redirect, 401 or\n * error — is `no-store` and `no-referrer`, so a tokenized `Location` is neither\n * cached nor forwarded as a Referer.\n */\nfunction portalHeaders(extra: Record<string, string>): Headers {\n const headers = new Headers(extra);\n headers.set(\"Cache-Control\", \"no-store\");\n headers.set(\"Referrer-Policy\", \"no-referrer\");\n return headers;\n}\n\n/**\n * One-line, log-safe description of a portal link failure: the error message\n * only (never the error object, which may carry a response body), with any\n * `#token=...` fragment redacted.\n */\nfunction safeErrorMessage(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n return message.replace(/#token=[^\\s\"'&]*/g, \"#token=[redacted]\").replace(/\\s+/g, \" \");\n}\n\n/**\n * Create a GET Route Handler that sends the signed-in customer to your store's\n * Pancake customer portal, without the portal's email verification step.\n *\n * Behavior:\n * - Calls `resolveAuthenticatedCustomer(request)` — your own session check — to\n * get `{ storeId, buyerIdentity }`. Nothing is read from the request's query\n * string, body or headers by the SDK\n * - Signed in: creates a portal link with `client.auth.createCustomerPortalLink()`\n * and answers `302` to `portalUrl` (which carries `#token=...`)\n * - Not signed in: `302` to `loginUrl` when set, otherwise `401`\n * - Link creation fails: `onError(error, request)`, or by default one server-side\n * `console.error` line with the error message and a generic `502 { error }`\n *\n * Side effects:\n * - One signed API request per signed-in visit; each visit issues a new portal session\n * - Every response it builds (not a custom `onError` one) sets `Cache-Control: no-store` and `Referrer-Policy: no-referrer`\n * so the tokenized `Location` is neither cached nor forwarded as a Referer\n *\n * Boundary conditions:\n * - The portal environment is the environment of the API Key in `config`\n * (test key → test portal, prod key → prod portal)\n * - Errors thrown by `resolveAuthenticatedCustomer` propagate (Next.js answers 500)\n * - The default error log is the message only, with any `#token=` fragment redacted;\n * pass `onError` to log or report failures your own way\n * - `loginUrl` must not echo request parameters verbatim, or the route becomes an open redirect\n *\n * A Route Handler rather than a Server Action: the portal entry is a plain link\n * navigation, and only an HTTP redirect keeps the tokenized URL out of client\n * JavaScript while letting the response carry `no-store` / `no-referrer`.\n *\n * @param config - WaffoPancake client configuration (merchantId + privateKey)\n * @param options - Session resolver, login redirect and error handling\n * @returns A Next.js GET Route Handler\n *\n * @example\n * ```ts\n * // app/account/billing-portal/route.ts\n * import { createCustomerPortalRedirect } from \"@waffo/pancake-nextjs/server\";\n * import { auth } from \"@/lib/auth\"; // your auth library\n *\n * export const dynamic = \"force-dynamic\";\n *\n * export const GET = createCustomerPortalRedirect(\n * { merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY! },\n * {\n * resolveAuthenticatedCustomer: async () => {\n * const session = await auth();\n * if (!session?.user) return null;\n * return { storeId: process.env.WAFFO_STORE_ID!, buyerIdentity: session.user.id };\n * },\n * loginUrl: \"/login?returnTo=/account/billing-portal\",\n * },\n * );\n * // In a page: <a href=\"/account/billing-portal\">Manage billing</a>\n * ```\n */\nexport function createCustomerPortalRedirect(\n config: WaffoPancakeConfig,\n options: CustomerPortalRedirectOptions,\n): CustomerPortalRouteHandler {\n const client = new WaffoPancake(config);\n\n return async function GET(request: Request): Promise<Response> {\n const customer = await options.resolveAuthenticatedCustomer(request);\n\n if (!customer) {\n if (options.loginUrl === undefined) {\n return new Response(JSON.stringify({ error: \"Unauthorized\" }), {\n status: 401,\n headers: portalHeaders({ \"Content-Type\": \"application/json\" }),\n });\n }\n const loginUrl = typeof options.loginUrl === \"function\" ? options.loginUrl(request) : options.loginUrl;\n return new Response(null, {\n status: 302,\n headers: portalHeaders({ Location: new URL(loginUrl, request.url).toString() }),\n });\n }\n\n let portalUrl: string;\n try {\n ({ portalUrl } = await client.auth.createCustomerPortalLink({\n storeId: customer.storeId,\n buyerIdentity: customer.buyerIdentity,\n }));\n } catch (error) {\n if (options.onError) {\n return options.onError(error, request);\n }\n console.error(\"[waffo-pancake] createCustomerPortalRedirect failed:\", safeErrorMessage(error));\n return new Response(JSON.stringify({ error: \"Failed to create customer portal link\" }), {\n status: 502,\n headers: portalHeaders({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n return new Response(null, {\n status: 302,\n headers: portalHeaders({ Location: portalUrl }),\n });\n };\n}\n\n// ============================================================\n// Customer Session Action\n// ============================================================\n\n/** Customer action types */\nexport type CustomerSessionActionType =\n | \"cancelSubscription\"\n | \"cancelOnetimeOrder\"\n | \"reactivateSubscription\"\n | \"createRefundTicket\"\n | \"resubmitRefundTicket\"\n | \"createPlanChangeSession\"\n | \"query\";\n\n/** Server action signature for customer session operations */\nexport type CustomerSessionAction = (\n token: string,\n actionType: CustomerSessionActionType,\n params: unknown,\n options?: RequestOptions,\n) => Promise<unknown>;\n\n/**\n * Create a server action that executes customer self-service operations.\n *\n * `config.environment` is required — session tokens carry no environment, and the\n * gateway rejects a session request without the matching header.\n *\n * `createPlanChangeSession` is the customer-driven half of a plan change: it returns\n * a confirmation-page URL, and the platform allows it only when the subscription\n * belongs to that customer, the target plan is in the same product group, and that\n * group's `selfServicePlanChange` is on — otherwise 403. The merchant-only pricing\n * fields are not part of its params; the platform drops them on this path silently.\n *\n * No call sends an idempotency key unless you pass one (`options.idempotencyKey`,\n * the last argument) — the same rule as every other method in the SDK — so a write\n * retried after a timeout executes twice.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerSessionAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const customerAction = createCustomerSessionAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * environment: \"test\",\n * });\n * ```\n */\nexport function createCustomerSessionAction(config: WaffoPancakeConfig): CustomerSessionAction {\n const client = new WaffoPancake(config);\n\n return async (token: string, actionType: CustomerSessionActionType, params: unknown, options?: RequestOptions): Promise<unknown> => {\n const customer = client.buyer(token);\n switch (actionType) {\n case \"cancelSubscription\":\n return customer.cancelSubscription(params as Parameters<typeof customer.cancelSubscription>[0], options);\n case \"cancelOnetimeOrder\":\n return customer.cancelOnetimeOrder(params as Parameters<typeof customer.cancelOnetimeOrder>[0], options);\n case \"reactivateSubscription\":\n return customer.reactivateSubscription(params as Parameters<typeof customer.reactivateSubscription>[0], options);\n case \"createRefundTicket\":\n return customer.createRefundTicket(params as Parameters<typeof customer.createRefundTicket>[0], options);\n case \"resubmitRefundTicket\":\n return customer.resubmitRefundTicket(params as Parameters<typeof customer.resubmitRefundTicket>[0], options);\n case \"createPlanChangeSession\":\n return customer.createPlanChangeSession(params as Parameters<typeof customer.createPlanChangeSession>[0], options);\n case \"query\":\n // Reads take no key: a cached replay would serve stale data.\n return customer.graphql.query(params as GraphQLParams);\n default:\n throw new Error(`Unknown customer action: ${actionType}`);\n }\n };\n}\n\n// ============================================================\n// Merchant Query Action\n// ============================================================\n\n/** Server action signature for merchant GraphQL queries */\nexport type MerchantQueryAction = (params: GraphQLParams) => Promise<GraphQLResponse>;\n\n/**\n * Create a server action that executes merchant GraphQL queries.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const merchantQuery = createMerchantQueryAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createMerchantQueryAction(config: WaffoPancakeConfig): MerchantQueryAction {\n const client = new WaffoPancake(config);\n\n return async (params: GraphQLParams): Promise<GraphQLResponse> => {\n return client.graphql.query(params);\n };\n}\n\n// ============================================================\n// Deprecated Aliases\n// ============================================================\n\n/** @deprecated Use {@link CustomerTokenAction} instead. */\nexport type BuyerTokenAction = CustomerTokenAction;\n\n/** @deprecated Use {@link createCustomerTokenAction} instead. */\nexport const createBuyerTokenAction = createCustomerTokenAction;\n\n/** @deprecated Use {@link CustomerSessionActionType} instead. */\nexport type BuyerSessionActionType = CustomerSessionActionType;\n\n/** @deprecated Use {@link CustomerSessionAction} instead. */\nexport type BuyerSessionAction = CustomerSessionAction;\n\n/** @deprecated Use {@link createCustomerSessionAction} instead. */\nexport const createBuyerSessionAction = createCustomerSessionAction;\n"],"mappings":"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","names":["type","sdkParams"]}
package/dist/server.d.cts CHANGED
@@ -64,6 +64,13 @@ type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOp
64
64
  /**
65
65
  * Create a server action that issues customer session tokens.
66
66
  *
67
+ * This is a generic token primitive: it signs whatever `buyerIdentity` it is
68
+ * handed. Never expose it to the browser as a portal or sign-in entry — a
69
+ * Server Action is callable with arbitrary arguments, so a client-chosen
70
+ * `buyerIdentity` would yield another customer's token. To send a signed-in
71
+ * customer to the customer portal, use {@link createCustomerPortalRedirect},
72
+ * which derives identity from your own session.
73
+ *
67
74
  * @param config - WaffoPancake client configuration
68
75
  * @returns A server action function
69
76
  *
@@ -79,6 +86,106 @@ type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOp
79
86
  * ```
80
87
  */
81
88
  declare function createCustomerTokenAction(config: WaffoPancakeConfig): CustomerTokenAction;
89
+ /**
90
+ * Store and customer identity for a customer portal link, resolved by the
91
+ * merchant app from its own authenticated session.
92
+ */
93
+ interface AuthenticatedPortalCustomer {
94
+ /** Store ID (Short ID `STO_xxx`) whose portal the customer enters */
95
+ storeId: string;
96
+ /** Customer identity in your system — the same value used for authenticated checkout */
97
+ buyerIdentity: string;
98
+ }
99
+ /** Options for {@link createCustomerPortalRedirect}. */
100
+ interface CustomerPortalRedirectOptions {
101
+ /**
102
+ * Resolve the signed-in customer from YOUR app's authenticated session
103
+ * (session cookie, auth library, …). Return `null` when nobody is signed in.
104
+ *
105
+ * Must not read `buyerIdentity` or `storeId` from anything the browser
106
+ * controls — query string, body, form fields or custom headers. Whoever
107
+ * controls `buyerIdentity` gets that customer's portal.
108
+ */
109
+ resolveAuthenticatedCustomer: (request: Request) => Promise<AuthenticatedPortalCustomer | null> | AuthenticatedPortalCustomer | null;
110
+ /**
111
+ * Where to send a visitor for whom the resolver returned `null`. A path or
112
+ * absolute URL, resolved against the request URL; a function receives the
113
+ * request (e.g. to add your own `returnTo`). Omit to answer 401 instead.
114
+ *
115
+ * Build it from constants or an allow-list, never by copying request
116
+ * parameters verbatim (e.g. a `?next=` value) — that turns this route into an
117
+ * open redirect.
118
+ */
119
+ loginUrl?: string | ((request: Request) => string);
120
+ /**
121
+ * Build the response when creating the portal link fails (API error, invalid
122
+ * store ID, network error). This is the place for your own logging and error
123
+ * reporting. Defaults to a 502 JSON `{ error }` response with a generic
124
+ * message, after one `console.error` line carrying only the error message;
125
+ * the error itself is not echoed to the browser.
126
+ */
127
+ onError?: (error: unknown, request: Request) => Response | Promise<Response>;
128
+ }
129
+ /** GET Route Handler returned by {@link createCustomerPortalRedirect}. */
130
+ type CustomerPortalRouteHandler = (request: Request) => Promise<Response>;
131
+ /**
132
+ * Create a GET Route Handler that sends the signed-in customer to your store's
133
+ * Pancake customer portal, without the portal's email verification step.
134
+ *
135
+ * Behavior:
136
+ * - Calls `resolveAuthenticatedCustomer(request)` — your own session check — to
137
+ * get `{ storeId, buyerIdentity }`. Nothing is read from the request's query
138
+ * string, body or headers by the SDK
139
+ * - Signed in: creates a portal link with `client.auth.createCustomerPortalLink()`
140
+ * and answers `302` to `portalUrl` (which carries `#token=...`)
141
+ * - Not signed in: `302` to `loginUrl` when set, otherwise `401`
142
+ * - Link creation fails: `onError(error, request)`, or by default one server-side
143
+ * `console.error` line with the error message and a generic `502 { error }`
144
+ *
145
+ * Side effects:
146
+ * - One signed API request per signed-in visit; each visit issues a new portal session
147
+ * - Every response it builds (not a custom `onError` one) sets `Cache-Control: no-store` and `Referrer-Policy: no-referrer`
148
+ * so the tokenized `Location` is neither cached nor forwarded as a Referer
149
+ *
150
+ * Boundary conditions:
151
+ * - The portal environment is the environment of the API Key in `config`
152
+ * (test key → test portal, prod key → prod portal)
153
+ * - Errors thrown by `resolveAuthenticatedCustomer` propagate (Next.js answers 500)
154
+ * - The default error log is the message only, with any `#token=` fragment redacted;
155
+ * pass `onError` to log or report failures your own way
156
+ * - `loginUrl` must not echo request parameters verbatim, or the route becomes an open redirect
157
+ *
158
+ * A Route Handler rather than a Server Action: the portal entry is a plain link
159
+ * navigation, and only an HTTP redirect keeps the tokenized URL out of client
160
+ * JavaScript while letting the response carry `no-store` / `no-referrer`.
161
+ *
162
+ * @param config - WaffoPancake client configuration (merchantId + privateKey)
163
+ * @param options - Session resolver, login redirect and error handling
164
+ * @returns A Next.js GET Route Handler
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * // app/account/billing-portal/route.ts
169
+ * import { createCustomerPortalRedirect } from "@waffo/pancake-nextjs/server";
170
+ * import { auth } from "@/lib/auth"; // your auth library
171
+ *
172
+ * export const dynamic = "force-dynamic";
173
+ *
174
+ * export const GET = createCustomerPortalRedirect(
175
+ * { merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY! },
176
+ * {
177
+ * resolveAuthenticatedCustomer: async () => {
178
+ * const session = await auth();
179
+ * if (!session?.user) return null;
180
+ * return { storeId: process.env.WAFFO_STORE_ID!, buyerIdentity: session.user.id };
181
+ * },
182
+ * loginUrl: "/login?returnTo=/account/billing-portal",
183
+ * },
184
+ * );
185
+ * // In a page: <a href="/account/billing-portal">Manage billing</a>
186
+ * ```
187
+ */
188
+ declare function createCustomerPortalRedirect(config: WaffoPancakeConfig, options: CustomerPortalRedirectOptions): CustomerPortalRouteHandler;
82
189
  /** Customer action types */
83
190
  type CustomerSessionActionType = "cancelSubscription" | "cancelOnetimeOrder" | "reactivateSubscription" | "createRefundTicket" | "resubmitRefundTicket" | "createPlanChangeSession" | "query";
84
191
  /** Server action signature for customer session operations */
@@ -146,4 +253,4 @@ type BuyerSessionAction = CustomerSessionAction;
146
253
  /** @deprecated Use {@link createCustomerSessionAction} instead. */
147
254
  declare const createBuyerSessionAction: typeof createCustomerSessionAction;
148
255
 
149
- export { type BuyerSessionAction, type BuyerSessionActionType, type BuyerTokenAction, type CheckoutAction, type CheckoutActionParams, type CheckoutActionResult, type CustomerSessionAction, type CustomerSessionActionType, type CustomerTokenAction, type MerchantQueryAction, createBuyerSessionAction, createBuyerTokenAction, createCheckoutAction, createCustomerSessionAction, createCustomerTokenAction, createMerchantQueryAction };
256
+ export { type AuthenticatedPortalCustomer, type BuyerSessionAction, type BuyerSessionActionType, type BuyerTokenAction, type CheckoutAction, type CheckoutActionParams, type CheckoutActionResult, type CustomerPortalRedirectOptions, type CustomerPortalRouteHandler, type CustomerSessionAction, type CustomerSessionActionType, type CustomerTokenAction, type MerchantQueryAction, createBuyerSessionAction, createBuyerTokenAction, createCheckoutAction, createCustomerPortalRedirect, createCustomerSessionAction, createCustomerTokenAction, createMerchantQueryAction };
package/dist/server.d.ts CHANGED
@@ -64,6 +64,13 @@ type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOp
64
64
  /**
65
65
  * Create a server action that issues customer session tokens.
66
66
  *
67
+ * This is a generic token primitive: it signs whatever `buyerIdentity` it is
68
+ * handed. Never expose it to the browser as a portal or sign-in entry — a
69
+ * Server Action is callable with arbitrary arguments, so a client-chosen
70
+ * `buyerIdentity` would yield another customer's token. To send a signed-in
71
+ * customer to the customer portal, use {@link createCustomerPortalRedirect},
72
+ * which derives identity from your own session.
73
+ *
67
74
  * @param config - WaffoPancake client configuration
68
75
  * @returns A server action function
69
76
  *
@@ -79,6 +86,106 @@ type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOp
79
86
  * ```
80
87
  */
81
88
  declare function createCustomerTokenAction(config: WaffoPancakeConfig): CustomerTokenAction;
89
+ /**
90
+ * Store and customer identity for a customer portal link, resolved by the
91
+ * merchant app from its own authenticated session.
92
+ */
93
+ interface AuthenticatedPortalCustomer {
94
+ /** Store ID (Short ID `STO_xxx`) whose portal the customer enters */
95
+ storeId: string;
96
+ /** Customer identity in your system — the same value used for authenticated checkout */
97
+ buyerIdentity: string;
98
+ }
99
+ /** Options for {@link createCustomerPortalRedirect}. */
100
+ interface CustomerPortalRedirectOptions {
101
+ /**
102
+ * Resolve the signed-in customer from YOUR app's authenticated session
103
+ * (session cookie, auth library, …). Return `null` when nobody is signed in.
104
+ *
105
+ * Must not read `buyerIdentity` or `storeId` from anything the browser
106
+ * controls — query string, body, form fields or custom headers. Whoever
107
+ * controls `buyerIdentity` gets that customer's portal.
108
+ */
109
+ resolveAuthenticatedCustomer: (request: Request) => Promise<AuthenticatedPortalCustomer | null> | AuthenticatedPortalCustomer | null;
110
+ /**
111
+ * Where to send a visitor for whom the resolver returned `null`. A path or
112
+ * absolute URL, resolved against the request URL; a function receives the
113
+ * request (e.g. to add your own `returnTo`). Omit to answer 401 instead.
114
+ *
115
+ * Build it from constants or an allow-list, never by copying request
116
+ * parameters verbatim (e.g. a `?next=` value) — that turns this route into an
117
+ * open redirect.
118
+ */
119
+ loginUrl?: string | ((request: Request) => string);
120
+ /**
121
+ * Build the response when creating the portal link fails (API error, invalid
122
+ * store ID, network error). This is the place for your own logging and error
123
+ * reporting. Defaults to a 502 JSON `{ error }` response with a generic
124
+ * message, after one `console.error` line carrying only the error message;
125
+ * the error itself is not echoed to the browser.
126
+ */
127
+ onError?: (error: unknown, request: Request) => Response | Promise<Response>;
128
+ }
129
+ /** GET Route Handler returned by {@link createCustomerPortalRedirect}. */
130
+ type CustomerPortalRouteHandler = (request: Request) => Promise<Response>;
131
+ /**
132
+ * Create a GET Route Handler that sends the signed-in customer to your store's
133
+ * Pancake customer portal, without the portal's email verification step.
134
+ *
135
+ * Behavior:
136
+ * - Calls `resolveAuthenticatedCustomer(request)` — your own session check — to
137
+ * get `{ storeId, buyerIdentity }`. Nothing is read from the request's query
138
+ * string, body or headers by the SDK
139
+ * - Signed in: creates a portal link with `client.auth.createCustomerPortalLink()`
140
+ * and answers `302` to `portalUrl` (which carries `#token=...`)
141
+ * - Not signed in: `302` to `loginUrl` when set, otherwise `401`
142
+ * - Link creation fails: `onError(error, request)`, or by default one server-side
143
+ * `console.error` line with the error message and a generic `502 { error }`
144
+ *
145
+ * Side effects:
146
+ * - One signed API request per signed-in visit; each visit issues a new portal session
147
+ * - Every response it builds (not a custom `onError` one) sets `Cache-Control: no-store` and `Referrer-Policy: no-referrer`
148
+ * so the tokenized `Location` is neither cached nor forwarded as a Referer
149
+ *
150
+ * Boundary conditions:
151
+ * - The portal environment is the environment of the API Key in `config`
152
+ * (test key → test portal, prod key → prod portal)
153
+ * - Errors thrown by `resolveAuthenticatedCustomer` propagate (Next.js answers 500)
154
+ * - The default error log is the message only, with any `#token=` fragment redacted;
155
+ * pass `onError` to log or report failures your own way
156
+ * - `loginUrl` must not echo request parameters verbatim, or the route becomes an open redirect
157
+ *
158
+ * A Route Handler rather than a Server Action: the portal entry is a plain link
159
+ * navigation, and only an HTTP redirect keeps the tokenized URL out of client
160
+ * JavaScript while letting the response carry `no-store` / `no-referrer`.
161
+ *
162
+ * @param config - WaffoPancake client configuration (merchantId + privateKey)
163
+ * @param options - Session resolver, login redirect and error handling
164
+ * @returns A Next.js GET Route Handler
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * // app/account/billing-portal/route.ts
169
+ * import { createCustomerPortalRedirect } from "@waffo/pancake-nextjs/server";
170
+ * import { auth } from "@/lib/auth"; // your auth library
171
+ *
172
+ * export const dynamic = "force-dynamic";
173
+ *
174
+ * export const GET = createCustomerPortalRedirect(
175
+ * { merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY! },
176
+ * {
177
+ * resolveAuthenticatedCustomer: async () => {
178
+ * const session = await auth();
179
+ * if (!session?.user) return null;
180
+ * return { storeId: process.env.WAFFO_STORE_ID!, buyerIdentity: session.user.id };
181
+ * },
182
+ * loginUrl: "/login?returnTo=/account/billing-portal",
183
+ * },
184
+ * );
185
+ * // In a page: <a href="/account/billing-portal">Manage billing</a>
186
+ * ```
187
+ */
188
+ declare function createCustomerPortalRedirect(config: WaffoPancakeConfig, options: CustomerPortalRedirectOptions): CustomerPortalRouteHandler;
82
189
  /** Customer action types */
83
190
  type CustomerSessionActionType = "cancelSubscription" | "cancelOnetimeOrder" | "reactivateSubscription" | "createRefundTicket" | "resubmitRefundTicket" | "createPlanChangeSession" | "query";
84
191
  /** Server action signature for customer session operations */
@@ -146,4 +253,4 @@ type BuyerSessionAction = CustomerSessionAction;
146
253
  /** @deprecated Use {@link createCustomerSessionAction} instead. */
147
254
  declare const createBuyerSessionAction: typeof createCustomerSessionAction;
148
255
 
149
- export { type BuyerSessionAction, type BuyerSessionActionType, type BuyerTokenAction, type CheckoutAction, type CheckoutActionParams, type CheckoutActionResult, type CustomerSessionAction, type CustomerSessionActionType, type CustomerTokenAction, type MerchantQueryAction, createBuyerSessionAction, createBuyerTokenAction, createCheckoutAction, createCustomerSessionAction, createCustomerTokenAction, createMerchantQueryAction };
256
+ export { type AuthenticatedPortalCustomer, type BuyerSessionAction, type BuyerSessionActionType, type BuyerTokenAction, type CheckoutAction, type CheckoutActionParams, type CheckoutActionResult, type CustomerPortalRedirectOptions, type CustomerPortalRouteHandler, type CustomerSessionAction, type CustomerSessionActionType, type CustomerTokenAction, type MerchantQueryAction, createBuyerSessionAction, createBuyerTokenAction, createCheckoutAction, createCustomerPortalRedirect, createCustomerSessionAction, createCustomerTokenAction, createMerchantQueryAction };
package/dist/server.js CHANGED
@@ -25,6 +25,55 @@ function createCustomerTokenAction(config) {
25
25
  return client.auth.issueSessionToken(params, options);
26
26
  };
27
27
  }
28
+ function portalHeaders(extra) {
29
+ const headers = new Headers(extra);
30
+ headers.set("Cache-Control", "no-store");
31
+ headers.set("Referrer-Policy", "no-referrer");
32
+ return headers;
33
+ }
34
+ function safeErrorMessage(error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ return message.replace(/#token=[^\s"'&]*/g, "#token=[redacted]").replace(/\s+/g, " ");
37
+ }
38
+ function createCustomerPortalRedirect(config, options) {
39
+ const client = new WaffoPancake(config);
40
+ return async function GET(request) {
41
+ const customer = await options.resolveAuthenticatedCustomer(request);
42
+ if (!customer) {
43
+ if (options.loginUrl === void 0) {
44
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
45
+ status: 401,
46
+ headers: portalHeaders({ "Content-Type": "application/json" })
47
+ });
48
+ }
49
+ const loginUrl = typeof options.loginUrl === "function" ? options.loginUrl(request) : options.loginUrl;
50
+ return new Response(null, {
51
+ status: 302,
52
+ headers: portalHeaders({ Location: new URL(loginUrl, request.url).toString() })
53
+ });
54
+ }
55
+ let portalUrl;
56
+ try {
57
+ ({ portalUrl } = await client.auth.createCustomerPortalLink({
58
+ storeId: customer.storeId,
59
+ buyerIdentity: customer.buyerIdentity
60
+ }));
61
+ } catch (error) {
62
+ if (options.onError) {
63
+ return options.onError(error, request);
64
+ }
65
+ console.error("[waffo-pancake] createCustomerPortalRedirect failed:", safeErrorMessage(error));
66
+ return new Response(JSON.stringify({ error: "Failed to create customer portal link" }), {
67
+ status: 502,
68
+ headers: portalHeaders({ "Content-Type": "application/json" })
69
+ });
70
+ }
71
+ return new Response(null, {
72
+ status: 302,
73
+ headers: portalHeaders({ Location: portalUrl })
74
+ });
75
+ };
76
+ }
28
77
  function createCustomerSessionAction(config) {
29
78
  const client = new WaffoPancake(config);
30
79
  return async (token, actionType, params, options) => {
@@ -61,6 +110,7 @@ export {
61
110
  createBuyerSessionAction,
62
111
  createBuyerTokenAction,
63
112
  createCheckoutAction,
113
+ createCustomerPortalRedirect,
64
114
  createCustomerSessionAction,
65
115
  createCustomerTokenAction,
66
116
  createMerchantQueryAction
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side entry point for @waffo/pancake-nextjs.\n *\n * All functions in this module use the private key for RSA signing\n * and must only run on the server (Next.js Server Actions / Route Handlers).\n *\n * @example\n * ```ts\n * // app/lib/waffo.ts\n * \"use server\";\n * import { createCheckoutAction, createCustomerTokenAction, createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * const config = {\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * };\n *\n * export const checkout = createCheckoutAction(config);\n * export const issueCustomerToken = createCustomerTokenAction(config);\n * export const merchantQuery = createMerchantQueryAction(config);\n * ```\n */\nimport { WaffoPancake } from \"@waffo/pancake-ts\";\n\nimport type {\n RequestOptions,\n WaffoPancakeConfig,\n AnonymousCheckoutParams,\n AuthenticatedCheckoutParams,\n AuthenticatedPlanChangeParams,\n CreatePlanChangeSessionParams,\n CheckoutSessionResult,\n AuthenticatedCheckoutResult,\n IssueSessionTokenParams,\n SessionToken,\n GraphQLParams,\n GraphQLResponse,\n} from \"@waffo/pancake-ts\";\n\n// ============================================================\n// Checkout Action\n// ============================================================\n\n/**\n * Parameters for the checkout server action.\n *\n * The `type` field selects the flow: a new purchase (`anonymous` / `authenticated`)\n * or a plan change for an existing subscription (`planChange` /\n * `authenticatedPlanChange`, which carry the required `originOrderId`).\n */\nexport type CheckoutActionParams =\n | ({ type?: \"anonymous\" } & AnonymousCheckoutParams)\n | ({ type: \"authenticated\" } & AuthenticatedCheckoutParams)\n | ({ type: \"planChange\" } & CreatePlanChangeSessionParams)\n | ({ type: \"authenticatedPlanChange\" } & AuthenticatedPlanChangeParams);\n\n/** Result of the checkout server action */\nexport type CheckoutActionResult = CheckoutSessionResult | AuthenticatedCheckoutResult;\n\n/**\n * Server action signature for checkout.\n *\n * The optional `options` carries an `idempotencyKey`; without one no key is sent\n * and a retried call creates a second session.\n */\nexport type CheckoutAction = (params: CheckoutActionParams, options?: RequestOptions) => Promise<CheckoutActionResult>;\n\n/**\n * Create a server action that handles checkout session creation.\n *\n * The private key is captured in the closure and never sent to the client.\n *\n * Handles both new purchases and plan changes; `params.type` selects the flow.\n *\n * @param config - WaffoPancake client configuration (merchantId + privateKey)\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCheckoutAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const checkout = createCheckoutAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n *\n * @example\n * ```ts\n * // Plan change link for an existing subscription — originOrderId is required\n * const session = await checkout({\n * type: \"planChange\",\n * originOrderId: \"ORD_xxx\",\n * productId: \"PROD_target_plan\",\n * currency: \"USD\",\n * });\n * // session.checkoutUrl points at the change confirmation page\n * ```\n */\nexport function createCheckoutAction(config: WaffoPancakeConfig): CheckoutAction {\n const client = new WaffoPancake(config);\n\n return async (params: CheckoutActionParams, options?: RequestOptions): Promise<CheckoutActionResult> => {\n if (params.type === \"authenticated\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.create(sdkParams, options);\n }\n if (params.type === \"planChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.createPlanChangeSession(sdkParams, options);\n }\n if (params.type === \"authenticatedPlanChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.createPlanChange(sdkParams, options);\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.anonymous.create(sdkParams, options);\n };\n}\n\n// ============================================================\n// Customer Token Action\n// ============================================================\n\n/** Server action signature for issuing customer tokens */\nexport type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOptions) => Promise<SessionToken>;\n\n/**\n * Create a server action that issues customer session tokens.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerTokenAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const issueCustomerToken = createCustomerTokenAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createCustomerTokenAction(config: WaffoPancakeConfig): CustomerTokenAction {\n const client = new WaffoPancake(config);\n\n return async (params: IssueSessionTokenParams, options?: RequestOptions): Promise<SessionToken> => {\n return client.auth.issueSessionToken(params, options);\n };\n}\n\n// ============================================================\n// Customer Session Action\n// ============================================================\n\n/** Customer action types */\nexport type CustomerSessionActionType =\n | \"cancelSubscription\"\n | \"cancelOnetimeOrder\"\n | \"reactivateSubscription\"\n | \"createRefundTicket\"\n | \"resubmitRefundTicket\"\n | \"createPlanChangeSession\"\n | \"query\";\n\n/** Server action signature for customer session operations */\nexport type CustomerSessionAction = (\n token: string,\n actionType: CustomerSessionActionType,\n params: unknown,\n options?: RequestOptions,\n) => Promise<unknown>;\n\n/**\n * Create a server action that executes customer self-service operations.\n *\n * `config.environment` is required — session tokens carry no environment, and the\n * gateway rejects a session request without the matching header.\n *\n * `createPlanChangeSession` is the customer-driven half of a plan change: it returns\n * a confirmation-page URL, and the platform allows it only when the subscription\n * belongs to that customer, the target plan is in the same product group, and that\n * group's `selfServicePlanChange` is on — otherwise 403. The merchant-only pricing\n * fields are not part of its params; the platform drops them on this path silently.\n *\n * No call sends an idempotency key unless you pass one (`options.idempotencyKey`,\n * the last argument) — the same rule as every other method in the SDK — so a write\n * retried after a timeout executes twice.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerSessionAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const customerAction = createCustomerSessionAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * environment: \"test\",\n * });\n * ```\n */\nexport function createCustomerSessionAction(config: WaffoPancakeConfig): CustomerSessionAction {\n const client = new WaffoPancake(config);\n\n return async (token: string, actionType: CustomerSessionActionType, params: unknown, options?: RequestOptions): Promise<unknown> => {\n const customer = client.buyer(token);\n switch (actionType) {\n case \"cancelSubscription\":\n return customer.cancelSubscription(params as Parameters<typeof customer.cancelSubscription>[0], options);\n case \"cancelOnetimeOrder\":\n return customer.cancelOnetimeOrder(params as Parameters<typeof customer.cancelOnetimeOrder>[0], options);\n case \"reactivateSubscription\":\n return customer.reactivateSubscription(params as Parameters<typeof customer.reactivateSubscription>[0], options);\n case \"createRefundTicket\":\n return customer.createRefundTicket(params as Parameters<typeof customer.createRefundTicket>[0], options);\n case \"resubmitRefundTicket\":\n return customer.resubmitRefundTicket(params as Parameters<typeof customer.resubmitRefundTicket>[0], options);\n case \"createPlanChangeSession\":\n return customer.createPlanChangeSession(params as Parameters<typeof customer.createPlanChangeSession>[0], options);\n case \"query\":\n // Reads take no key: a cached replay would serve stale data.\n return customer.graphql.query(params as GraphQLParams);\n default:\n throw new Error(`Unknown customer action: ${actionType}`);\n }\n };\n}\n\n// ============================================================\n// Merchant Query Action\n// ============================================================\n\n/** Server action signature for merchant GraphQL queries */\nexport type MerchantQueryAction = (params: GraphQLParams) => Promise<GraphQLResponse>;\n\n/**\n * Create a server action that executes merchant GraphQL queries.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const merchantQuery = createMerchantQueryAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createMerchantQueryAction(config: WaffoPancakeConfig): MerchantQueryAction {\n const client = new WaffoPancake(config);\n\n return async (params: GraphQLParams): Promise<GraphQLResponse> => {\n return client.graphql.query(params);\n };\n}\n\n// ============================================================\n// Deprecated Aliases\n// ============================================================\n\n/** @deprecated Use {@link CustomerTokenAction} instead. */\nexport type BuyerTokenAction = CustomerTokenAction;\n\n/** @deprecated Use {@link createCustomerTokenAction} instead. */\nexport const createBuyerTokenAction = createCustomerTokenAction;\n\n/** @deprecated Use {@link CustomerSessionActionType} instead. */\nexport type BuyerSessionActionType = CustomerSessionActionType;\n\n/** @deprecated Use {@link CustomerSessionAction} instead. */\nexport type BuyerSessionAction = CustomerSessionAction;\n\n/** @deprecated Use {@link createCustomerSessionAction} instead. */\nexport const createBuyerSessionAction = createCustomerSessionAction;\n"],"mappings":";AAsBA,SAAS,oBAAoB;AA8EtB,SAAS,qBAAqB,QAA4C;AAC/E,QAAM,SAAS,IAAI,aAAa,MAAM;AAEtC,SAAO,OAAO,QAA8B,YAA4D;AACtG,QAAI,OAAO,SAAS,iBAAiB;AAEnC,YAAM,EAAE,MAAAA,OAAM,GAAGC,WAAU,IAAI;AAC/B,aAAO,OAAO,SAAS,cAAc,OAAOA,YAAW,OAAO;AAAA,IAChE;AACA,QAAI,OAAO,SAAS,cAAc;AAEhC,YAAM,EAAE,MAAAD,OAAM,GAAGC,WAAU,IAAI;AAC/B,aAAO,OAAO,SAAS,wBAAwBA,YAAW,OAAO;AAAA,IACnE;AACA,QAAI,OAAO,SAAS,2BAA2B;AAE7C,YAAM,EAAE,MAAAD,OAAM,GAAGC,WAAU,IAAI;AAC/B,aAAO,OAAO,SAAS,cAAc,iBAAiBA,YAAW,OAAO;AAAA,IAC1E;AAEA,UAAM,EAAE,MAAM,GAAG,UAAU,IAAI;AAC/B,WAAO,OAAO,SAAS,UAAU,OAAO,WAAW,OAAO;AAAA,EAC5D;AACF;AA0BO,SAAS,0BAA0B,QAAiD;AACzF,QAAM,SAAS,IAAI,aAAa,MAAM;AAEtC,SAAO,OAAO,QAAiC,YAAoD;AACjG,WAAO,OAAO,KAAK,kBAAkB,QAAQ,OAAO;AAAA,EACtD;AACF;AAuDO,SAAS,4BAA4B,QAAmD;AAC7F,QAAM,SAAS,IAAI,aAAa,MAAM;AAEtC,SAAO,OAAO,OAAe,YAAuC,QAAiB,YAA+C;AAClI,UAAM,WAAW,OAAO,MAAM,KAAK;AACnC,YAAQ,YAAY;AAAA,MAClB,KAAK;AACH,eAAO,SAAS,mBAAmB,QAA6D,OAAO;AAAA,MACzG,KAAK;AACH,eAAO,SAAS,mBAAmB,QAA6D,OAAO;AAAA,MACzG,KAAK;AACH,eAAO,SAAS,uBAAuB,QAAiE,OAAO;AAAA,MACjH,KAAK;AACH,eAAO,SAAS,mBAAmB,QAA6D,OAAO;AAAA,MACzG,KAAK;AACH,eAAO,SAAS,qBAAqB,QAA+D,OAAO;AAAA,MAC7G,KAAK;AACH,eAAO,SAAS,wBAAwB,QAAkE,OAAO;AAAA,MACnH,KAAK;AAEH,eAAO,SAAS,QAAQ,MAAM,MAAuB;AAAA,MACvD;AACE,cAAM,IAAI,MAAM,4BAA4B,UAAU,EAAE;AAAA,IAC5D;AAAA,EACF;AACF;AA0BO,SAAS,0BAA0B,QAAiD;AACzF,QAAM,SAAS,IAAI,aAAa,MAAM;AAEtC,SAAO,OAAO,WAAoD;AAChE,WAAO,OAAO,QAAQ,MAAM,MAAM;AAAA,EACpC;AACF;AAUO,IAAM,yBAAyB;AAS/B,IAAM,2BAA2B;","names":["type","sdkParams"]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side entry point for @waffo/pancake-nextjs.\n *\n * All functions in this module use the private key for RSA signing\n * and must only run on the server (Next.js Server Actions / Route Handlers).\n *\n * @example\n * ```ts\n * // app/lib/waffo.ts\n * \"use server\";\n * import { createCheckoutAction, createCustomerTokenAction, createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * const config = {\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * };\n *\n * export const checkout = createCheckoutAction(config);\n * export const issueCustomerToken = createCustomerTokenAction(config);\n * export const merchantQuery = createMerchantQueryAction(config);\n * ```\n *\n * @example\n * ```ts\n * // app/account/billing-portal/route.ts — customer portal entry from your own login\n * import { createCustomerPortalRedirect } from \"@waffo/pancake-nextjs/server\";\n *\n * export const GET = createCustomerPortalRedirect(config, {\n * resolveAuthenticatedCustomer: async (request) => getSignedInCustomer(request), // your session\n * });\n * ```\n */\nimport { WaffoPancake } from \"@waffo/pancake-ts\";\n\nimport type {\n RequestOptions,\n WaffoPancakeConfig,\n AnonymousCheckoutParams,\n AuthenticatedCheckoutParams,\n AuthenticatedPlanChangeParams,\n CreatePlanChangeSessionParams,\n CheckoutSessionResult,\n AuthenticatedCheckoutResult,\n IssueSessionTokenParams,\n SessionToken,\n GraphQLParams,\n GraphQLResponse,\n} from \"@waffo/pancake-ts\";\n\n// ============================================================\n// Checkout Action\n// ============================================================\n\n/**\n * Parameters for the checkout server action.\n *\n * The `type` field selects the flow: a new purchase (`anonymous` / `authenticated`)\n * or a plan change for an existing subscription (`planChange` /\n * `authenticatedPlanChange`, which carry the required `originOrderId`).\n */\nexport type CheckoutActionParams =\n | ({ type?: \"anonymous\" } & AnonymousCheckoutParams)\n | ({ type: \"authenticated\" } & AuthenticatedCheckoutParams)\n | ({ type: \"planChange\" } & CreatePlanChangeSessionParams)\n | ({ type: \"authenticatedPlanChange\" } & AuthenticatedPlanChangeParams);\n\n/** Result of the checkout server action */\nexport type CheckoutActionResult = CheckoutSessionResult | AuthenticatedCheckoutResult;\n\n/**\n * Server action signature for checkout.\n *\n * The optional `options` carries an `idempotencyKey`; without one no key is sent\n * and a retried call creates a second session.\n */\nexport type CheckoutAction = (params: CheckoutActionParams, options?: RequestOptions) => Promise<CheckoutActionResult>;\n\n/**\n * Create a server action that handles checkout session creation.\n *\n * The private key is captured in the closure and never sent to the client.\n *\n * Handles both new purchases and plan changes; `params.type` selects the flow.\n *\n * @param config - WaffoPancake client configuration (merchantId + privateKey)\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCheckoutAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const checkout = createCheckoutAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n *\n * @example\n * ```ts\n * // Plan change link for an existing subscription — originOrderId is required\n * const session = await checkout({\n * type: \"planChange\",\n * originOrderId: \"ORD_xxx\",\n * productId: \"PROD_target_plan\",\n * currency: \"USD\",\n * });\n * // session.checkoutUrl points at the change confirmation page\n * ```\n */\nexport function createCheckoutAction(config: WaffoPancakeConfig): CheckoutAction {\n const client = new WaffoPancake(config);\n\n return async (params: CheckoutActionParams, options?: RequestOptions): Promise<CheckoutActionResult> => {\n if (params.type === \"authenticated\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.create(sdkParams, options);\n }\n if (params.type === \"planChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.createPlanChangeSession(sdkParams, options);\n }\n if (params.type === \"authenticatedPlanChange\") {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.authenticated.createPlanChange(sdkParams, options);\n }\n // eslint-disable-next-line @typescript-eslint/no-unused-vars -- remove type field before passing to SDK\n const { type, ...sdkParams } = params;\n return client.checkout.anonymous.create(sdkParams, options);\n };\n}\n\n// ============================================================\n// Customer Token Action\n// ============================================================\n\n/** Server action signature for issuing customer tokens */\nexport type CustomerTokenAction = (params: IssueSessionTokenParams, options?: RequestOptions) => Promise<SessionToken>;\n\n/**\n * Create a server action that issues customer session tokens.\n *\n * This is a generic token primitive: it signs whatever `buyerIdentity` it is\n * handed. Never expose it to the browser as a portal or sign-in entry — a\n * Server Action is callable with arbitrary arguments, so a client-chosen\n * `buyerIdentity` would yield another customer's token. To send a signed-in\n * customer to the customer portal, use {@link createCustomerPortalRedirect},\n * which derives identity from your own session.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerTokenAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const issueCustomerToken = createCustomerTokenAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createCustomerTokenAction(config: WaffoPancakeConfig): CustomerTokenAction {\n const client = new WaffoPancake(config);\n\n return async (params: IssueSessionTokenParams, options?: RequestOptions): Promise<SessionToken> => {\n return client.auth.issueSessionToken(params, options);\n };\n}\n\n// ============================================================\n// Customer Portal Redirect\n// ============================================================\n\n/**\n * Store and customer identity for a customer portal link, resolved by the\n * merchant app from its own authenticated session.\n */\nexport interface AuthenticatedPortalCustomer {\n /** Store ID (Short ID `STO_xxx`) whose portal the customer enters */\n storeId: string;\n /** Customer identity in your system — the same value used for authenticated checkout */\n buyerIdentity: string;\n}\n\n/** Options for {@link createCustomerPortalRedirect}. */\nexport interface CustomerPortalRedirectOptions {\n /**\n * Resolve the signed-in customer from YOUR app's authenticated session\n * (session cookie, auth library, …). Return `null` when nobody is signed in.\n *\n * Must not read `buyerIdentity` or `storeId` from anything the browser\n * controls — query string, body, form fields or custom headers. Whoever\n * controls `buyerIdentity` gets that customer's portal.\n */\n resolveAuthenticatedCustomer: (request: Request) => Promise<AuthenticatedPortalCustomer | null> | AuthenticatedPortalCustomer | null;\n /**\n * Where to send a visitor for whom the resolver returned `null`. A path or\n * absolute URL, resolved against the request URL; a function receives the\n * request (e.g. to add your own `returnTo`). Omit to answer 401 instead.\n *\n * Build it from constants or an allow-list, never by copying request\n * parameters verbatim (e.g. a `?next=` value) — that turns this route into an\n * open redirect.\n */\n loginUrl?: string | ((request: Request) => string);\n /**\n * Build the response when creating the portal link fails (API error, invalid\n * store ID, network error). This is the place for your own logging and error\n * reporting. Defaults to a 502 JSON `{ error }` response with a generic\n * message, after one `console.error` line carrying only the error message;\n * the error itself is not echoed to the browser.\n */\n onError?: (error: unknown, request: Request) => Response | Promise<Response>;\n}\n\n/** GET Route Handler returned by {@link createCustomerPortalRedirect}. */\nexport type CustomerPortalRouteHandler = (request: Request) => Promise<Response>;\n\n/**\n * Build headers for a portal route response. Every response — redirect, 401 or\n * error — is `no-store` and `no-referrer`, so a tokenized `Location` is neither\n * cached nor forwarded as a Referer.\n */\nfunction portalHeaders(extra: Record<string, string>): Headers {\n const headers = new Headers(extra);\n headers.set(\"Cache-Control\", \"no-store\");\n headers.set(\"Referrer-Policy\", \"no-referrer\");\n return headers;\n}\n\n/**\n * One-line, log-safe description of a portal link failure: the error message\n * only (never the error object, which may carry a response body), with any\n * `#token=...` fragment redacted.\n */\nfunction safeErrorMessage(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n return message.replace(/#token=[^\\s\"'&]*/g, \"#token=[redacted]\").replace(/\\s+/g, \" \");\n}\n\n/**\n * Create a GET Route Handler that sends the signed-in customer to your store's\n * Pancake customer portal, without the portal's email verification step.\n *\n * Behavior:\n * - Calls `resolveAuthenticatedCustomer(request)` — your own session check — to\n * get `{ storeId, buyerIdentity }`. Nothing is read from the request's query\n * string, body or headers by the SDK\n * - Signed in: creates a portal link with `client.auth.createCustomerPortalLink()`\n * and answers `302` to `portalUrl` (which carries `#token=...`)\n * - Not signed in: `302` to `loginUrl` when set, otherwise `401`\n * - Link creation fails: `onError(error, request)`, or by default one server-side\n * `console.error` line with the error message and a generic `502 { error }`\n *\n * Side effects:\n * - One signed API request per signed-in visit; each visit issues a new portal session\n * - Every response it builds (not a custom `onError` one) sets `Cache-Control: no-store` and `Referrer-Policy: no-referrer`\n * so the tokenized `Location` is neither cached nor forwarded as a Referer\n *\n * Boundary conditions:\n * - The portal environment is the environment of the API Key in `config`\n * (test key → test portal, prod key → prod portal)\n * - Errors thrown by `resolveAuthenticatedCustomer` propagate (Next.js answers 500)\n * - The default error log is the message only, with any `#token=` fragment redacted;\n * pass `onError` to log or report failures your own way\n * - `loginUrl` must not echo request parameters verbatim, or the route becomes an open redirect\n *\n * A Route Handler rather than a Server Action: the portal entry is a plain link\n * navigation, and only an HTTP redirect keeps the tokenized URL out of client\n * JavaScript while letting the response carry `no-store` / `no-referrer`.\n *\n * @param config - WaffoPancake client configuration (merchantId + privateKey)\n * @param options - Session resolver, login redirect and error handling\n * @returns A Next.js GET Route Handler\n *\n * @example\n * ```ts\n * // app/account/billing-portal/route.ts\n * import { createCustomerPortalRedirect } from \"@waffo/pancake-nextjs/server\";\n * import { auth } from \"@/lib/auth\"; // your auth library\n *\n * export const dynamic = \"force-dynamic\";\n *\n * export const GET = createCustomerPortalRedirect(\n * { merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY! },\n * {\n * resolveAuthenticatedCustomer: async () => {\n * const session = await auth();\n * if (!session?.user) return null;\n * return { storeId: process.env.WAFFO_STORE_ID!, buyerIdentity: session.user.id };\n * },\n * loginUrl: \"/login?returnTo=/account/billing-portal\",\n * },\n * );\n * // In a page: <a href=\"/account/billing-portal\">Manage billing</a>\n * ```\n */\nexport function createCustomerPortalRedirect(\n config: WaffoPancakeConfig,\n options: CustomerPortalRedirectOptions,\n): CustomerPortalRouteHandler {\n const client = new WaffoPancake(config);\n\n return async function GET(request: Request): Promise<Response> {\n const customer = await options.resolveAuthenticatedCustomer(request);\n\n if (!customer) {\n if (options.loginUrl === undefined) {\n return new Response(JSON.stringify({ error: \"Unauthorized\" }), {\n status: 401,\n headers: portalHeaders({ \"Content-Type\": \"application/json\" }),\n });\n }\n const loginUrl = typeof options.loginUrl === \"function\" ? options.loginUrl(request) : options.loginUrl;\n return new Response(null, {\n status: 302,\n headers: portalHeaders({ Location: new URL(loginUrl, request.url).toString() }),\n });\n }\n\n let portalUrl: string;\n try {\n ({ portalUrl } = await client.auth.createCustomerPortalLink({\n storeId: customer.storeId,\n buyerIdentity: customer.buyerIdentity,\n }));\n } catch (error) {\n if (options.onError) {\n return options.onError(error, request);\n }\n console.error(\"[waffo-pancake] createCustomerPortalRedirect failed:\", safeErrorMessage(error));\n return new Response(JSON.stringify({ error: \"Failed to create customer portal link\" }), {\n status: 502,\n headers: portalHeaders({ \"Content-Type\": \"application/json\" }),\n });\n }\n\n return new Response(null, {\n status: 302,\n headers: portalHeaders({ Location: portalUrl }),\n });\n };\n}\n\n// ============================================================\n// Customer Session Action\n// ============================================================\n\n/** Customer action types */\nexport type CustomerSessionActionType =\n | \"cancelSubscription\"\n | \"cancelOnetimeOrder\"\n | \"reactivateSubscription\"\n | \"createRefundTicket\"\n | \"resubmitRefundTicket\"\n | \"createPlanChangeSession\"\n | \"query\";\n\n/** Server action signature for customer session operations */\nexport type CustomerSessionAction = (\n token: string,\n actionType: CustomerSessionActionType,\n params: unknown,\n options?: RequestOptions,\n) => Promise<unknown>;\n\n/**\n * Create a server action that executes customer self-service operations.\n *\n * `config.environment` is required — session tokens carry no environment, and the\n * gateway rejects a session request without the matching header.\n *\n * `createPlanChangeSession` is the customer-driven half of a plan change: it returns\n * a confirmation-page URL, and the platform allows it only when the subscription\n * belongs to that customer, the target plan is in the same product group, and that\n * group's `selfServicePlanChange` is on — otherwise 403. The merchant-only pricing\n * fields are not part of its params; the platform drops them on this path silently.\n *\n * No call sends an idempotency key unless you pass one (`options.idempotencyKey`,\n * the last argument) — the same rule as every other method in the SDK — so a write\n * retried after a timeout executes twice.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createCustomerSessionAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const customerAction = createCustomerSessionAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * environment: \"test\",\n * });\n * ```\n */\nexport function createCustomerSessionAction(config: WaffoPancakeConfig): CustomerSessionAction {\n const client = new WaffoPancake(config);\n\n return async (token: string, actionType: CustomerSessionActionType, params: unknown, options?: RequestOptions): Promise<unknown> => {\n const customer = client.buyer(token);\n switch (actionType) {\n case \"cancelSubscription\":\n return customer.cancelSubscription(params as Parameters<typeof customer.cancelSubscription>[0], options);\n case \"cancelOnetimeOrder\":\n return customer.cancelOnetimeOrder(params as Parameters<typeof customer.cancelOnetimeOrder>[0], options);\n case \"reactivateSubscription\":\n return customer.reactivateSubscription(params as Parameters<typeof customer.reactivateSubscription>[0], options);\n case \"createRefundTicket\":\n return customer.createRefundTicket(params as Parameters<typeof customer.createRefundTicket>[0], options);\n case \"resubmitRefundTicket\":\n return customer.resubmitRefundTicket(params as Parameters<typeof customer.resubmitRefundTicket>[0], options);\n case \"createPlanChangeSession\":\n return customer.createPlanChangeSession(params as Parameters<typeof customer.createPlanChangeSession>[0], options);\n case \"query\":\n // Reads take no key: a cached replay would serve stale data.\n return customer.graphql.query(params as GraphQLParams);\n default:\n throw new Error(`Unknown customer action: ${actionType}`);\n }\n };\n}\n\n// ============================================================\n// Merchant Query Action\n// ============================================================\n\n/** Server action signature for merchant GraphQL queries */\nexport type MerchantQueryAction = (params: GraphQLParams) => Promise<GraphQLResponse>;\n\n/**\n * Create a server action that executes merchant GraphQL queries.\n *\n * @param config - WaffoPancake client configuration\n * @returns A server action function\n *\n * @example\n * ```ts\n * \"use server\";\n * import { createMerchantQueryAction } from \"@waffo/pancake-nextjs/server\";\n *\n * export const merchantQuery = createMerchantQueryAction({\n * merchantId: process.env.WAFFO_MERCHANT_ID!,\n * privateKey: process.env.WAFFO_PRIVATE_KEY!,\n * });\n * ```\n */\nexport function createMerchantQueryAction(config: WaffoPancakeConfig): MerchantQueryAction {\n const client = new WaffoPancake(config);\n\n return async (params: GraphQLParams): Promise<GraphQLResponse> => {\n return client.graphql.query(params);\n };\n}\n\n// ============================================================\n// Deprecated Aliases\n// ============================================================\n\n/** @deprecated Use {@link CustomerTokenAction} instead. */\nexport type BuyerTokenAction = CustomerTokenAction;\n\n/** @deprecated Use {@link createCustomerTokenAction} instead. */\nexport const createBuyerTokenAction = createCustomerTokenAction;\n\n/** @deprecated Use {@link CustomerSessionActionType} instead. */\nexport type BuyerSessionActionType = CustomerSessionActionType;\n\n/** @deprecated Use {@link CustomerSessionAction} instead. */\nexport type BuyerSessionAction = CustomerSessionAction;\n\n/** @deprecated Use {@link createCustomerSessionAction} instead. */\nexport const createBuyerSessionAction = createCustomerSessionAction;\n"],"mappings":"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","names":["type","sdkParams"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waffo/pancake-nextjs",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Next.js SDK for Waffo Pancake — checkout components and hooks",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "license": "MIT",
50
50
  "dependencies": {
51
- "@waffo/pancake-ts": "^0.23.0",
51
+ "@waffo/pancake-ts": "^0.26.0",
52
52
  "next": ">=14.0.0",
53
53
  "react": ">=18.0.0",
54
54
  "react-dom": ">=18.0.0"