@01.software/init 0.9.1 → 0.10.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.
Files changed (119) hide show
  1. package/dist/ai-docs.d.ts +13 -0
  2. package/dist/ai-docs.js +1 -1
  3. package/dist/browser-auth-CJDrpp5T.d.ts +11 -0
  4. package/dist/{chunk-UA7WNT2F.js → chunk-4LHYICUL.js} +1 -1
  5. package/dist/chunk-4LHYICUL.js.map +1 -0
  6. package/dist/{chunk-TBGKXE3Q.js → chunk-NJ4X7VNK.js} +5 -5
  7. package/dist/chunk-NJ4X7VNK.js.map +1 -0
  8. package/dist/{chunk-5K2CB2Y5.js → chunk-Q6MSORYN.js} +14 -37
  9. package/dist/chunk-Q6MSORYN.js.map +1 -0
  10. package/dist/chunk-STM4DKVZ.js +183 -0
  11. package/dist/chunk-STM4DKVZ.js.map +1 -0
  12. package/dist/{chunk-2IGKOSK7.js → chunk-WDWJ73KP.js} +41 -215
  13. package/dist/chunk-WDWJ73KP.js.map +1 -0
  14. package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
  15. package/dist/create-app-templates/ecommerce/CHANGELOG.md +30 -0
  16. package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
  17. package/dist/create-app-templates/ecommerce/README.md +139 -0
  18. package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
  19. package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
  20. package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
  21. package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
  22. package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
  23. package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
  24. package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
  25. package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
  26. package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
  27. package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
  28. package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
  29. package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
  30. package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
  31. package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
  32. package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
  33. package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
  34. package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
  35. package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
  36. package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
  37. package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
  38. package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
  39. package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
  40. package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
  41. package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +119 -0
  42. package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +267 -0
  43. package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
  44. package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
  45. package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
  46. package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
  47. package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
  48. package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
  49. package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
  50. package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
  51. package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
  52. package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
  53. package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
  54. package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
  55. package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
  56. package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
  57. package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
  58. package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
  59. package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +96 -0
  60. package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
  61. package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
  62. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
  63. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
  64. package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +76 -0
  65. package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +63 -0
  66. package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +3 -0
  67. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +336 -0
  68. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
  69. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +913 -0
  70. package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
  71. package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
  72. package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
  73. package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
  74. package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +206 -0
  75. package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
  76. package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
  77. package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
  78. package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
  79. package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
  80. package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
  81. package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
  82. package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
  83. package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
  84. package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
  85. package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
  86. package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
  87. package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
  88. package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
  89. package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
  90. package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
  91. package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
  92. package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
  93. package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
  94. package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
  95. package/dist/create-app-templates/ecommerce/package.json +33 -0
  96. package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
  97. package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
  98. package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +392 -0
  99. package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1537 -0
  100. package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
  101. package/dist/create-app-templates/registry.json +66 -0
  102. package/dist/create-app.d.ts +40 -0
  103. package/dist/create-app.js +652 -0
  104. package/dist/create-app.js.map +1 -0
  105. package/dist/detect-Bjxp9wcS.d.ts +13 -0
  106. package/dist/file-ops.d.ts +21 -0
  107. package/dist/file-ops.js +1 -1
  108. package/dist/index.d.ts +2 -0
  109. package/dist/index.js +6 -5
  110. package/dist/index.js.map +1 -1
  111. package/dist/init.d.ts +40 -0
  112. package/dist/init.js +5 -4
  113. package/dist/templates.d.ts +27 -0
  114. package/dist/templates.js +1 -1
  115. package/package.json +31 -15
  116. package/dist/chunk-2IGKOSK7.js.map +0 -1
  117. package/dist/chunk-5K2CB2Y5.js.map +0 -1
  118. package/dist/chunk-TBGKXE3Q.js.map +0 -1
  119. package/dist/chunk-UA7WNT2F.js.map +0 -1
@@ -0,0 +1,109 @@
1
+ import "../server-only-guard.ts";
2
+ import type { CartSyncCommerce } from "./cart-sync.ts";
3
+
4
+ /**
5
+ * Per-request customer SDK client.
6
+ *
7
+ * Customer auth uses the **browser** SDK client (`createClient`, root export),
8
+ * not the server client — there is **no secret key** involved. The customer JWT
9
+ * is the only credential; the publishable key routes the tenant. The browser
10
+ * client is server-safe with `customer: { persist: false, token }`: `window` is
11
+ * `typeof`-guarded and the `localStorage` path is skipped when `persist` is
12
+ * false, so it runs inside a Next.js route handler without throwing.
13
+ *
14
+ * A fresh client is built per request because the JWT comes from the request
15
+ * cookie — clients are not shared across requests. The SDK is imported
16
+ * dynamically so this module stays importable under `node --test` (mirrors the
17
+ * commerce adapter's `await import('@01.software/sdk/server')`).
18
+ */
19
+
20
+ export interface CustomerClientConfig {
21
+ publishableKey: string;
22
+ apiUrl: string | undefined;
23
+ }
24
+
25
+ /** Minimal shape of the SDK customer-auth surface this template uses. */
26
+ export interface CustomerAuthClient {
27
+ customer: {
28
+ auth: {
29
+ login(data: { email: string; password: string }): Promise<{
30
+ token: string;
31
+ customer: unknown;
32
+ }>;
33
+ register(data: {
34
+ name: string;
35
+ email: string;
36
+ password: string;
37
+ phone?: string;
38
+ }): Promise<{ customer: unknown }>;
39
+ me(): Promise<unknown | null>;
40
+ };
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Minimal shape of the customer-JWT commerce client the login cart-sync uses;
46
+ * its `commerce` namespace is the {@link CartSyncCommerce} merge/mine surface.
47
+ */
48
+ export interface CustomerCommerceClient {
49
+ commerce: CartSyncCommerce;
50
+ }
51
+
52
+ /**
53
+ * Resolve customer-client config from the environment, or `null` when 01.software
54
+ * credentials are not configured (e.g. the zero-backend mock demo). Only the
55
+ * publishable key is required — customer auth never uses the secret key.
56
+ */
57
+ export function getCustomerClientConfig(
58
+ env: NodeJS.ProcessEnv = process.env,
59
+ ): CustomerClientConfig | null {
60
+ const publishableKey = env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY?.trim();
61
+ if (!publishableKey) return null;
62
+ const apiUrl =
63
+ env.SOFTWARE_API_URL?.trim() || env.NEXT_PUBLIC_SOFTWARE_API_URL?.trim();
64
+ return { publishableKey, apiUrl: apiUrl || undefined };
65
+ }
66
+
67
+ /** Whether customer auth is available in this deployment. */
68
+ export function isCustomerAuthEnabled(
69
+ env: NodeJS.ProcessEnv = process.env,
70
+ ): boolean {
71
+ return getCustomerClientConfig(env) !== null;
72
+ }
73
+
74
+ /**
75
+ * Build a per-request customer SDK client bound to `token` (the JWT from the
76
+ * session cookie, or `null` for an anonymous client used only for login/
77
+ * register). Returns `null` when credentials are not configured.
78
+ */
79
+ export async function createCustomerClient(
80
+ token: string | null,
81
+ config: CustomerClientConfig | null = getCustomerClientConfig(),
82
+ ): Promise<CustomerAuthClient | null> {
83
+ if (!config) return null;
84
+ const { createClient } = await import("@01.software/sdk");
85
+ return createClient({
86
+ publishableKey: config.publishableKey,
87
+ apiUrl: config.apiUrl,
88
+ customer: { persist: false, token: token ?? undefined },
89
+ }) as unknown as CustomerAuthClient;
90
+ }
91
+
92
+ /**
93
+ * Build a per-request customer-JWT commerce client for the login cart-sync
94
+ * (merge/mine). Same browser SDK client family as {@link createCustomerClient}
95
+ * (publishable key + JWT, no secret); returns `null` when credentials are not
96
+ * configured.
97
+ */
98
+ export async function createCustomerCommerceClient(
99
+ token: string,
100
+ config: CustomerClientConfig | null = getCustomerClientConfig(),
101
+ ): Promise<CustomerCommerceClient | null> {
102
+ if (!config) return null;
103
+ const { createClient } = await import("@01.software/sdk");
104
+ return createClient({
105
+ publishableKey: config.publishableKey,
106
+ apiUrl: config.apiUrl,
107
+ customer: { persist: false, token },
108
+ }) as unknown as CustomerCommerceClient;
109
+ }
@@ -0,0 +1,15 @@
1
+ import "../server-only-guard.ts";
2
+ import type { CustomerSummary } from "./auth-actions.ts";
3
+ import { loadCustomer } from "./auth-actions.ts";
4
+ import { getSessionToken } from "./session.ts";
5
+
6
+ /**
7
+ * Resolve the current customer for render paths (Server Components). Read-only
8
+ * and crash-proof: an expired/invalid token or a backend error resolves to
9
+ * `null` (guest), and the cookie is **not** mutated here (cookies are not
10
+ * writable during RSC render). Returns `null` with no network call when customer
11
+ * auth is not configured.
12
+ */
13
+ export async function getCurrentCustomer(): Promise<CustomerSummary | null> {
14
+ return loadCustomer(await getSessionToken());
15
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * CSRF guard for the customer auth mutation routes.
3
+ *
4
+ * The session JWT lives in a SameSite=lax cookie, so a cross-site form POST
5
+ * could otherwise ride the cookie. Two cheap, layered checks defeat that without
6
+ * a token round-trip:
7
+ *
8
+ * 1. **`application/json` content-type, rejected hard.** A cross-site HTML form
9
+ * can only send `application/x-www-form-urlencoded`, `multipart/form-data`, or
10
+ * `text/plain` — never `application/json` (that would require a CORS preflight
11
+ * the server never allows). Same-origin `fetch` sets it freely.
12
+ * 2. **Same-origin `Origin`.** When the browser sends an `Origin` header it must
13
+ * match the request host; a mismatch is a cross-site request and is rejected.
14
+ *
15
+ * Both checks are pure over the `Request`, so they unit-test without a server.
16
+ */
17
+
18
+ export class CsrfError extends Error {
19
+ constructor(message: string) {
20
+ super(message);
21
+ this.name = "CsrfError";
22
+ }
23
+ }
24
+
25
+ function requestHost(request: Request): string | null {
26
+ // `x-forwarded-host` is the deployed host behind Vercel/most proxies. It is a
27
+ // forbidden header for browser `fetch`, so a real cross-site request cannot
28
+ // set it; the JSON content-type check above is the primary defense regardless.
29
+ // If you self-host without a trusted proxy that sets this, compare `host`.
30
+ const forwarded = request.headers.get("x-forwarded-host");
31
+ if (forwarded) return forwarded.split(",")[0]!.trim();
32
+ return request.headers.get("host");
33
+ }
34
+
35
+ /**
36
+ * Throw `CsrfError` unless the request is a same-origin JSON request. Call at the
37
+ * top of every state-changing auth route handler.
38
+ */
39
+ export function assertSameOriginJson(request: Request): void {
40
+ const contentType = request.headers.get("content-type") ?? "";
41
+ if (!contentType.toLowerCase().includes("application/json")) {
42
+ throw new CsrfError("Request must be application/json");
43
+ }
44
+
45
+ const origin = request.headers.get("origin");
46
+ if (origin) {
47
+ let originHost: string;
48
+ try {
49
+ originHost = new URL(origin).host;
50
+ } catch {
51
+ throw new CsrfError("Invalid Origin header");
52
+ }
53
+ const host = requestHost(request);
54
+ if (!host || originHost !== host) {
55
+ throw new CsrfError("Cross-origin request rejected");
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,75 @@
1
+ import { CsrfError } from "./route-guard.ts";
2
+
3
+ /** Map an auth-route error to a public JSON response with a sensible status. */
4
+ export function authErrorResponse(error: unknown): Response {
5
+ if (error instanceof CsrfError) {
6
+ return Response.json(
7
+ { code: "forbidden", message: error.message },
8
+ { status: 403 },
9
+ );
10
+ }
11
+ if (error instanceof SyntaxError) {
12
+ // request.json() on a non-JSON / malformed body.
13
+ return Response.json(
14
+ { code: "invalid_request", message: "Request body must be JSON" },
15
+ { status: 400 },
16
+ );
17
+ }
18
+ // Validation errors from the `parse*` helpers carry safe, caller-facing
19
+ // messages; anything else is internal and must not leak verbatim.
20
+ const message = error instanceof Error ? error.message : "Request failed";
21
+ if (/required|must be/.test(message)) {
22
+ return Response.json({ code: "invalid_request", message }, { status: 400 });
23
+ }
24
+ return Response.json(
25
+ { code: "auth_failed", message: "Request failed" },
26
+ { status: 500 },
27
+ );
28
+ }
29
+
30
+ function asRecord(input: unknown): Record<string, unknown> {
31
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
32
+ throw new Error("Request body must be an object");
33
+ }
34
+ return input as Record<string, unknown>;
35
+ }
36
+
37
+ function requiredString(input: unknown, field: string): string {
38
+ if (typeof input !== "string" || input.trim() === "") {
39
+ throw new Error(`${field} is required`);
40
+ }
41
+ return input.trim();
42
+ }
43
+
44
+ function optionalString(input: unknown): string | undefined {
45
+ if (input === undefined || input === null || input === "") return undefined;
46
+ if (typeof input !== "string") return undefined;
47
+ return input.trim() || undefined;
48
+ }
49
+
50
+ export function parseLogin(input: unknown): {
51
+ email: string;
52
+ password: string;
53
+ } {
54
+ const body = asRecord(input);
55
+ return {
56
+ email: requiredString(body.email, "email"),
57
+ password: requiredString(body.password, "password"),
58
+ };
59
+ }
60
+
61
+ export function parseRegister(input: unknown): {
62
+ name: string;
63
+ email: string;
64
+ password: string;
65
+ phone?: string;
66
+ } {
67
+ const body = asRecord(input);
68
+ const phone = optionalString(body.phone);
69
+ return {
70
+ name: requiredString(body.name, "name"),
71
+ email: requiredString(body.email, "email"),
72
+ password: requiredString(body.password, "password"),
73
+ ...(phone ? { phone } : {}),
74
+ };
75
+ }
@@ -0,0 +1,108 @@
1
+ import "../server-only-guard.ts";
2
+ // Relative (not the `@/` alias) so the pure cookie helpers stay importable under
3
+ // `node --test`, which does not resolve tsconfig path aliases.
4
+ import { appConfig } from "../../app-config.ts";
5
+
6
+ /**
7
+ * Customer session cookie. It holds the customer JWT issued by
8
+ * `client.customer.auth.*` and is **HttpOnly + Secure (prod) + SameSite=lax** so
9
+ * it is invisible to client JS (XSS) and only travels on same-site requests. The
10
+ * JWT is the session credential the server attaches to per-request SDK calls
11
+ * (`lib/customer/client.server.ts`); the browser never sees it.
12
+ *
13
+ * `.set`/`.delete` are only valid in Route Handlers / Server Functions, never
14
+ * during Server Component render — keep cookie writes in the `app/api/auth/*`
15
+ * routes. Render paths read this cookie but must not mutate it.
16
+ *
17
+ * The cookie read/write/clear logic is split into pure functions over a minimal
18
+ * cookie-store shape so it stays unit-testable; `next/headers` is imported
19
+ * dynamically inside the async wrappers (the `server-only` npm package is
20
+ * avoided — its default export throws under `node --test`).
21
+ */
22
+
23
+ const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; // 7 days — matches a typical JWT lifetime
24
+
25
+ /** The subset of the Next.js cookie store this module needs. */
26
+ export interface SessionCookieStore {
27
+ get(name: string): { value: string } | undefined;
28
+ set(name: string, value: string, options: SessionCookieOptions): void;
29
+ delete(name: string): void;
30
+ }
31
+
32
+ export interface SessionCookieOptions {
33
+ httpOnly: boolean;
34
+ secure: boolean;
35
+ sameSite: "lax";
36
+ path: string;
37
+ maxAge: number;
38
+ }
39
+
40
+ /** Build the hardened cookie options for the customer session JWT. */
41
+ export function sessionCookieOptions(): SessionCookieOptions {
42
+ return {
43
+ httpOnly: true,
44
+ // Secure cookies are dropped by browsers over plain http, so allow http in
45
+ // local dev and enforce Secure in production.
46
+ secure: process.env.NODE_ENV === "production",
47
+ sameSite: "lax",
48
+ path: "/",
49
+ maxAge: COOKIE_MAX_AGE_SECONDS,
50
+ };
51
+ }
52
+
53
+ /** Read the customer JWT from a cookie store, or `null` when none is set. */
54
+ export function readSessionToken(store: SessionCookieStore): string | null {
55
+ return store.get(appConfig.customerKey)?.value ?? null;
56
+ }
57
+
58
+ /** Write the customer JWT to a cookie store with hardened options. */
59
+ export function writeSessionToken(
60
+ store: SessionCookieStore,
61
+ token: string,
62
+ ): void {
63
+ store.set(appConfig.customerKey, token, sessionCookieOptions());
64
+ }
65
+
66
+ /** Remove the customer JWT from a cookie store. */
67
+ export function clearSessionToken(store: SessionCookieStore): void {
68
+ store.delete(appConfig.customerKey);
69
+ }
70
+
71
+ /**
72
+ * Clear both the customer session JWT and the active-cart cookie on a store. The
73
+ * single cart-token handle is slaved to the session, so logout drops both
74
+ * atomically — otherwise the next anonymous (or different) session would inherit
75
+ * the prior identity's customer-cart handle.
76
+ */
77
+ export function clearSessionAndCartTokens(store: SessionCookieStore): void {
78
+ store.delete(appConfig.customerKey);
79
+ store.delete(appConfig.cartKey);
80
+ }
81
+
82
+ async function cookieStore(): Promise<SessionCookieStore> {
83
+ const { cookies } = await import("next/headers");
84
+ return (await cookies()) as unknown as SessionCookieStore;
85
+ }
86
+
87
+ /** Read the current customer JWT from the request cookies (Route Handler / RSC). */
88
+ export async function getSessionToken(): Promise<string | null> {
89
+ return readSessionToken(await cookieStore());
90
+ }
91
+
92
+ /** Set the customer JWT cookie. Route Handlers / Server Functions only. */
93
+ export async function setSessionToken(token: string): Promise<void> {
94
+ writeSessionToken(await cookieStore(), token);
95
+ }
96
+
97
+ /** Clear the customer JWT cookie. Route Handlers / Server Functions only. */
98
+ export async function clearSession(): Promise<void> {
99
+ clearSessionToken(await cookieStore());
100
+ }
101
+
102
+ /**
103
+ * Clear the customer JWT cookie and the active-cart cookie together (logout).
104
+ * Route Handlers / Server Functions only.
105
+ */
106
+ export async function clearSessionAndCart(): Promise<void> {
107
+ clearSessionAndCartTokens(await cookieStore());
108
+ }
@@ -0,0 +1,7 @@
1
+ export function formatMoney(amount: number, currency = "KRW"): string {
2
+ return new Intl.NumberFormat("ko-KR", {
3
+ style: "currency",
4
+ currency,
5
+ maximumFractionDigits: 0,
6
+ }).format(amount);
7
+ }
@@ -0,0 +1,84 @@
1
+ import '../../server-only-guard.ts'
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import type { PaymentProvider } from '../provider.ts'
5
+ import type { ProviderPayment } from '../types.ts'
6
+
7
+ const paymentStorePath = join(process.cwd(), '.mock-payments.json')
8
+
9
+ export function createMockPaymentProvider(): PaymentProvider {
10
+ return {
11
+ provider: 'mock',
12
+
13
+ async requestPayment(input) {
14
+ writeMockPayment({
15
+ paymentId: input.paymentId,
16
+ status: 'paid',
17
+ amount: input.amount,
18
+ provider: 'mock',
19
+ orderNumber: input.orderNumber,
20
+ })
21
+
22
+ const params = new URLSearchParams({
23
+ paymentId: input.paymentId,
24
+ orderNumber: input.orderNumber,
25
+ })
26
+
27
+ return {
28
+ ok: true,
29
+ paymentId: input.paymentId,
30
+ redirectUrl: `/api/checkout/payment-return?${params.toString()}`,
31
+ }
32
+ },
33
+
34
+ async getPayment(paymentId) {
35
+ return readMockPayment(paymentId)
36
+ },
37
+
38
+ async confirmPayment(input) {
39
+ const payment = await this.getPayment(input.paymentId)
40
+ if (!payment) throw new Error('Payment not found')
41
+ return payment
42
+ },
43
+
44
+ async verifyWebhook(request) {
45
+ const body = (await request.json()) as {
46
+ paymentId?: string
47
+ eventId?: string
48
+ }
49
+
50
+ if (!body.paymentId || !body.eventId) {
51
+ throw new Error('Invalid mock webhook')
52
+ }
53
+
54
+ return {
55
+ type: 'payment.updated',
56
+ paymentId: body.paymentId,
57
+ eventId: body.eventId,
58
+ }
59
+ },
60
+ }
61
+ }
62
+
63
+ function readMockPayment(paymentId: string): ProviderPayment | null {
64
+ return readMockPayments()[paymentId] ?? null
65
+ }
66
+
67
+ function writeMockPayment(payment: ProviderPayment): void {
68
+ const payments = readMockPayments()
69
+ payments[payment.paymentId] = payment
70
+ writeFileSync(paymentStorePath, JSON.stringify(payments, null, 2))
71
+ }
72
+
73
+ function readMockPayments(): Record<string, ProviderPayment> {
74
+ if (!existsSync(paymentStorePath)) return {}
75
+
76
+ try {
77
+ return JSON.parse(readFileSync(paymentStorePath, 'utf8')) as Record<
78
+ string,
79
+ ProviderPayment
80
+ >
81
+ } catch {
82
+ return {}
83
+ }
84
+ }