@01.software/init 0.9.2 → 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 (116) hide show
  1. package/dist/ai-docs.d.ts +13 -0
  2. package/dist/ai-docs.js +0 -0
  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-R4GGO33X.js → chunk-NJ4X7VNK.js} +1 -1
  7. package/dist/{chunk-R4GGO33X.js.map → chunk-NJ4X7VNK.js.map} +1 -1
  8. package/dist/chunk-Q6MSORYN.js +0 -0
  9. package/dist/chunk-STM4DKVZ.js +183 -0
  10. package/dist/chunk-STM4DKVZ.js.map +1 -0
  11. package/dist/{chunk-ENQSB4OF.js → chunk-WDWJ73KP.js} +40 -214
  12. package/dist/chunk-WDWJ73KP.js.map +1 -0
  13. package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
  14. package/dist/create-app-templates/ecommerce/CHANGELOG.md +30 -0
  15. package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
  16. package/dist/create-app-templates/ecommerce/README.md +139 -0
  17. package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
  18. package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
  19. package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
  20. package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
  21. package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
  22. package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
  23. package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
  24. package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
  25. package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
  26. package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
  27. package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
  28. package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
  29. package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
  30. package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
  31. package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
  32. package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
  33. package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
  34. package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
  35. package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
  36. package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
  37. package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
  38. package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
  39. package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
  40. package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +119 -0
  41. package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +267 -0
  42. package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
  43. package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
  44. package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
  45. package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
  46. package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
  47. package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
  48. package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
  49. package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
  50. package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
  51. package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
  52. package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
  53. package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
  54. package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
  55. package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
  56. package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
  57. package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
  58. package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +96 -0
  59. package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
  60. package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
  61. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
  62. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
  63. package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +76 -0
  64. package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +63 -0
  65. package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +3 -0
  66. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +336 -0
  67. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
  68. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +913 -0
  69. package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
  70. package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
  71. package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
  72. package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
  73. package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +206 -0
  74. package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
  75. package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
  76. package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
  77. package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
  78. package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
  79. package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
  80. package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
  81. package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
  82. package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
  83. package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
  84. package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
  85. package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
  86. package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
  87. package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
  88. package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
  89. package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
  90. package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
  91. package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
  92. package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
  93. package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
  94. package/dist/create-app-templates/ecommerce/package.json +33 -0
  95. package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
  96. package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
  97. package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +392 -0
  98. package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1537 -0
  99. package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
  100. package/dist/create-app-templates/registry.json +66 -0
  101. package/dist/create-app.d.ts +40 -0
  102. package/dist/create-app.js +652 -0
  103. package/dist/create-app.js.map +1 -0
  104. package/dist/detect-Bjxp9wcS.d.ts +13 -0
  105. package/dist/file-ops.d.ts +21 -0
  106. package/dist/file-ops.js +1 -1
  107. package/dist/index.d.ts +2 -0
  108. package/dist/index.js +4 -3
  109. package/dist/index.js.map +1 -1
  110. package/dist/init.d.ts +40 -0
  111. package/dist/init.js +4 -3
  112. package/dist/templates.d.ts +27 -0
  113. package/dist/templates.js +1 -1
  114. package/package.json +31 -15
  115. package/dist/chunk-ENQSB4OF.js.map +0 -1
  116. package/dist/chunk-UA7WNT2F.js.map +0 -1
@@ -0,0 +1,96 @@
1
+ import "../server-only-guard.ts";
2
+ import type { CommerceProvider } from "../commerce/provider.ts";
3
+ import { assertAmountEquals } from "./amount-gate.ts";
4
+ import type { PaymentProvider } from "./provider.ts";
5
+ import type { SyncOrderPaymentResult } from "./types.ts";
6
+
7
+ /**
8
+ * Reconcile a PG payment into a placed, paid order. In the checkout/placement
9
+ * model no order exists until this runs: a verified-paid payment promotes the
10
+ * open Checkout to a paid Order (`orders.confirmPayment`). The order is then
11
+ * resolved by PG payment id for idempotent retries (return + webhook collapse
12
+ * to one `paid`).
13
+ */
14
+ export async function syncOrderPayment(input: {
15
+ paymentId: string;
16
+ orderNumber?: string;
17
+ providerEventId?: string;
18
+ commerceProvider: CommerceProvider;
19
+ paymentProvider: PaymentProvider;
20
+ }): Promise<SyncOrderPaymentResult> {
21
+ const provider = input.paymentProvider.provider;
22
+
23
+ // Idempotency: once placed, the order resolves by PG payment id; a repeat
24
+ // return/webhook for an already-final order is a no-op.
25
+ const existing = await input.commerceProvider.getOrderByPaymentId({
26
+ paymentId: input.paymentId,
27
+ provider,
28
+ });
29
+ if (existing?.displayStatus === "paid") {
30
+ return { status: "paid", orderId: existing.id };
31
+ }
32
+ if (existing?.displayStatus === "canceled") {
33
+ return { status: "canceled", orderId: existing.id };
34
+ }
35
+
36
+ // Verify against the PG by server re-fetch — never a client-supplied amount.
37
+ const providerPayment = await input.paymentProvider.getPayment(
38
+ input.paymentId,
39
+ );
40
+ if (!providerPayment) {
41
+ return { status: "payment_not_found", orderId: existing?.id };
42
+ }
43
+ if (providerPayment.status === "pending") {
44
+ return { status: "pending", orderId: existing?.id };
45
+ }
46
+ if (providerPayment.status === "failed") {
47
+ await input.commerceProvider.markPaymentFailed({
48
+ paymentId: input.paymentId,
49
+ orderNumber: input.orderNumber,
50
+ provider,
51
+ providerEventId: input.providerEventId,
52
+ });
53
+ return { status: "failed", orderId: existing?.id };
54
+ }
55
+ if (providerPayment.status === "canceled") {
56
+ await input.commerceProvider.cancelPendingOrder({
57
+ paymentId: input.paymentId,
58
+ orderNumber: input.orderNumber,
59
+ provider,
60
+ providerEventId: input.providerEventId,
61
+ });
62
+ return { status: "canceled", orderId: existing?.id };
63
+ }
64
+
65
+ // paid: place the open checkout into a paid order with the PG-verified
66
+ // amount. Placement resolves the checkout by `orderNumber` (→ `checkoutToken`).
67
+ // The return path supplies the orderNumber from the redirect query; a webhook
68
+ // that arrives first (or instead, e.g. the buyer closed the tab) recovers it
69
+ // from the PG payment itself — the durable `orderNumber↔pgPaymentId` mapping
70
+ // threaded into `customData`/`metadata` at request time (#1544 / I6). Only if
71
+ // neither source has it does the webhook stay pending and defer to the return.
72
+ const orderNumber = input.orderNumber ?? providerPayment.orderNumber;
73
+ if (!orderNumber) {
74
+ return { status: "pending", orderId: existing?.id };
75
+ }
76
+
77
+ // Unconditional amount-equality gate before confirm. The amount we forward is
78
+ // always the PG re-fetch (`providerPayment.amount`), never a client value;
79
+ // when a prior placed order is already on record we cross-check the two
80
+ // independent sources here, and the Console `confirmPayment` endpoint is the
81
+ // authoritative quote check against the open checkout total in every case.
82
+ assertAmountEquals({
83
+ context: `sync:${provider}:${input.paymentId}`,
84
+ expected: existing?.totalAmount ?? providerPayment.amount,
85
+ actual: providerPayment.amount,
86
+ });
87
+
88
+ const placed = await input.commerceProvider.confirmOrderPayment({
89
+ paymentId: input.paymentId,
90
+ orderNumber,
91
+ provider,
92
+ amount: providerPayment.amount,
93
+ providerEventId: input.providerEventId,
94
+ });
95
+ return { status: "paid", orderId: placed.id };
96
+ }
@@ -0,0 +1,71 @@
1
+ export type PaymentRequestInput = {
2
+ paymentId: string;
3
+ orderNumber: string;
4
+ orderName: string;
5
+ amount: number;
6
+ currency: string;
7
+ customer: {
8
+ name: string;
9
+ email: string;
10
+ phone: string;
11
+ };
12
+ };
13
+
14
+ export type PaymentRequestResult = {
15
+ ok: boolean;
16
+ paymentId: string;
17
+ redirectUrl?: string;
18
+ clientPayment?: ClientPaymentRequest;
19
+ reason?: "canceled" | "failed";
20
+ };
21
+
22
+ export type ClientPaymentRequest =
23
+ | {
24
+ provider: "portone";
25
+ storeId: string;
26
+ channelKey: string;
27
+ payMethod: string;
28
+ paymentId: string;
29
+ orderNumber: string;
30
+ orderName: string;
31
+ amount: number;
32
+ currency: string;
33
+ customer: PaymentRequestInput["customer"];
34
+ }
35
+ | {
36
+ provider: "tosspayments";
37
+ clientKey: string;
38
+ customerKey?: string;
39
+ paymentId: string;
40
+ orderNumber: string;
41
+ orderName: string;
42
+ amount: number;
43
+ currency: string;
44
+ customer: PaymentRequestInput["customer"];
45
+ };
46
+
47
+ export type ProviderPayment = {
48
+ paymentId: string;
49
+ status: "pending" | "paid" | "failed" | "canceled";
50
+ amount: number;
51
+ provider?: string;
52
+ /**
53
+ * Merchant order number recovered from the PG payment itself (PortOne
54
+ * `customData`, Toss `metadata`), threaded in at `requestPayment` time. This
55
+ * is the durable `orderNumber↔pgPaymentId` mapping — there is no local index —
56
+ * and lets a webhook that arrives before (or instead of) the synchronous
57
+ * return path resolve and place the open checkout (#1544 / I6).
58
+ */
59
+ orderNumber?: string;
60
+ };
61
+
62
+ export type PaymentWebhookEvent = {
63
+ type: "payment.updated";
64
+ paymentId: string;
65
+ eventId: string;
66
+ };
67
+
68
+ export type SyncOrderPaymentResult = {
69
+ status: "paid" | "pending" | "failed" | "canceled" | "payment_not_found";
70
+ orderId?: string;
71
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Runtime server-only guard (#1544 / I6).
3
+ *
4
+ * Importing this module from browser-executed code throws immediately — a hard
5
+ * runtime error, not merely a bundler boundary — so a runtime-bearing module
6
+ * (PG secret keys, commerce/payment lifecycle, server re-fetch) can never
7
+ * silently ship to and execute in the client. In Node (the template's
8
+ * `node --test` domain tests) and in the Next.js server runtime `window` is
9
+ * `undefined`, so the import is a no-op.
10
+ *
11
+ * We deliberately do NOT use the `server-only` npm package: its `default`
12
+ * export throws under `node --test` (the `react-server` export condition is not
13
+ * set there), which would break the template's domain tests. A `typeof window`
14
+ * check gives the same browser protection without that resolution hazard.
15
+ */
16
+ if (typeof window !== "undefined") {
17
+ throw new Error(
18
+ "This module is server-only and must never be imported into client/browser code.",
19
+ );
20
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,16 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ devIndicators: false,
5
+ transpilePackages: ["@01.software/sdk"],
6
+ images: {
7
+ remotePatterns: [
8
+ {
9
+ protocol: "https",
10
+ hostname: "images.unsplash.com",
11
+ },
12
+ ],
13
+ },
14
+ };
15
+
16
+ export default nextConfig;
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "ecommerce",
3
+ "version": "0.1.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "lint": "eslint",
11
+ "test": "node --test --experimental-strip-types tests/*.test.ts",
12
+ "check-types": "next typegen && tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "@01.software/sdk": "^0.42.0",
16
+ "@portone/browser-sdk": "^0.1.8",
17
+ "@portone/server-sdk": "^0.19.0",
18
+ "@tosspayments/tosspayments-sdk": "^2.7.0",
19
+ "next": "16.2.9",
20
+ "react": "19.2.4",
21
+ "react-dom": "19.2.4"
22
+ },
23
+ "devDependencies": {
24
+ "@tailwindcss/postcss": "^4.3.0",
25
+ "@types/node": "^20",
26
+ "@types/react": "^19",
27
+ "@types/react-dom": "^19",
28
+ "eslint": "^9",
29
+ "eslint-config-next": "16.2.9",
30
+ "tailwindcss": "^4.3.0",
31
+ "typescript": "^5"
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,263 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ clearSessionToken,
6
+ readSessionToken,
7
+ sessionCookieOptions,
8
+ writeSessionToken,
9
+ type SessionCookieStore,
10
+ } from "../lib/customer/session.ts";
11
+ import {
12
+ assertSameOriginJson,
13
+ CsrfError,
14
+ } from "../lib/customer/route-guard.ts";
15
+ import {
16
+ loadCustomer,
17
+ login,
18
+ register,
19
+ type CustomerClientFactory,
20
+ } from "../lib/customer/auth-actions.ts";
21
+ import { appConfig } from "../app-config.ts";
22
+
23
+ // ---- session cookie read/write/clear -------------------------------------
24
+
25
+ function fakeCookieStore(): SessionCookieStore & {
26
+ entries: Map<string, string>;
27
+ } {
28
+ const entries = new Map<string, string>();
29
+ return {
30
+ entries,
31
+ get(name) {
32
+ const value = entries.get(name);
33
+ return value === undefined ? undefined : { value };
34
+ },
35
+ set(name, value) {
36
+ entries.set(name, value);
37
+ },
38
+ delete(name) {
39
+ entries.delete(name);
40
+ },
41
+ };
42
+ }
43
+
44
+ test("session: write then read returns the stored token", () => {
45
+ const store = fakeCookieStore();
46
+ assert.equal(readSessionToken(store), null);
47
+ writeSessionToken(store, "jwt-123");
48
+ assert.equal(store.entries.get(appConfig.customerKey), "jwt-123");
49
+ assert.equal(readSessionToken(store), "jwt-123");
50
+ });
51
+
52
+ test("session: clear removes the token", () => {
53
+ const store = fakeCookieStore();
54
+ writeSessionToken(store, "jwt-123");
55
+ clearSessionToken(store);
56
+ assert.equal(readSessionToken(store), null);
57
+ });
58
+
59
+ test("session: cookie options are hardened (HttpOnly + SameSite=lax)", () => {
60
+ const options = sessionCookieOptions();
61
+ assert.equal(options.httpOnly, true);
62
+ assert.equal(options.sameSite, "lax");
63
+ assert.equal(options.path, "/");
64
+ assert.ok(options.maxAge > 0);
65
+ // Secure follows NODE_ENV; under test it is not production.
66
+ assert.equal(options.secure, process.env.NODE_ENV === "production");
67
+ });
68
+
69
+ // ---- CSRF guard -----------------------------------------------------------
70
+
71
+ function fakeRequest(headers: Record<string, string>): Request {
72
+ return { headers: new Headers(headers) } as unknown as Request;
73
+ }
74
+
75
+ test("csrf: same-origin JSON request passes", () => {
76
+ assert.doesNotThrow(() =>
77
+ assertSameOriginJson(
78
+ fakeRequest({
79
+ "content-type": "application/json",
80
+ origin: "https://shop.example",
81
+ "x-forwarded-host": "shop.example",
82
+ }),
83
+ ),
84
+ );
85
+ });
86
+
87
+ test("csrf: same-origin without Origin header passes (non-browser)", () => {
88
+ assert.doesNotThrow(() =>
89
+ assertSameOriginJson(
90
+ fakeRequest({ "content-type": "application/json; charset=utf-8" }),
91
+ ),
92
+ );
93
+ });
94
+
95
+ test("csrf: non-JSON content-type is rejected hard", () => {
96
+ assert.throws(
97
+ () =>
98
+ assertSameOriginJson(
99
+ fakeRequest({
100
+ "content-type": "application/x-www-form-urlencoded",
101
+ origin: "https://shop.example",
102
+ "x-forwarded-host": "shop.example",
103
+ }),
104
+ ),
105
+ CsrfError,
106
+ );
107
+ });
108
+
109
+ test("csrf: cross-origin request is rejected", () => {
110
+ assert.throws(
111
+ () =>
112
+ assertSameOriginJson(
113
+ fakeRequest({
114
+ "content-type": "application/json",
115
+ origin: "https://evil.example",
116
+ "x-forwarded-host": "shop.example",
117
+ }),
118
+ ),
119
+ CsrfError,
120
+ );
121
+ });
122
+
123
+ // ---- auth actions (SDK mocked) -------------------------------------------
124
+
125
+ function fakeFactory(
126
+ client: unknown | null,
127
+ ): CustomerClientFactory {
128
+ return async () => client as never;
129
+ }
130
+
131
+ const SAMPLE_CUSTOMER = { id: "cust_1", name: "Ada", email: "ada@example.com" };
132
+
133
+ test("login: returns token + customer on success", async () => {
134
+ const factory = fakeFactory({
135
+ customer: {
136
+ auth: {
137
+ async login() {
138
+ return { token: "jwt-xyz", customer: SAMPLE_CUSTOMER };
139
+ },
140
+ },
141
+ },
142
+ });
143
+ const result = await login(
144
+ { email: "ada@example.com", password: "pw" },
145
+ factory,
146
+ );
147
+ assert.equal(result.ok, true);
148
+ if (result.ok) {
149
+ assert.equal(result.token, "jwt-xyz");
150
+ assert.deepEqual(result.customer, SAMPLE_CUSTOMER);
151
+ }
152
+ });
153
+
154
+ test("login: maps SDK error status to a failure result", async () => {
155
+ const factory = fakeFactory({
156
+ customer: {
157
+ auth: {
158
+ async login() {
159
+ throw Object.assign(new Error("Invalid credentials"), {
160
+ status: 401,
161
+ });
162
+ },
163
+ },
164
+ },
165
+ });
166
+ const result = await login(
167
+ { email: "ada@example.com", password: "wrong" },
168
+ factory,
169
+ );
170
+ assert.equal(result.ok, false);
171
+ if (!result.ok) {
172
+ assert.equal(result.status, 401);
173
+ assert.equal(result.message, "Invalid credentials");
174
+ }
175
+ });
176
+
177
+ test("login: returns 503 when customer auth is unconfigured", async () => {
178
+ const result = await login(
179
+ { email: "ada@example.com", password: "pw" },
180
+ fakeFactory(null),
181
+ );
182
+ assert.equal(result.ok, false);
183
+ if (!result.ok) assert.equal(result.status, 503);
184
+ });
185
+
186
+ test("register: returns customer with no token", async () => {
187
+ const factory = fakeFactory({
188
+ customer: {
189
+ auth: {
190
+ async register() {
191
+ return { customer: SAMPLE_CUSTOMER };
192
+ },
193
+ },
194
+ },
195
+ });
196
+ const result = await register(
197
+ { name: "Ada", email: "ada@example.com", password: "pw" },
198
+ factory,
199
+ );
200
+ assert.equal(result.ok, true);
201
+ if (result.ok) {
202
+ assert.equal(result.token, null);
203
+ assert.deepEqual(result.customer, SAMPLE_CUSTOMER);
204
+ }
205
+ });
206
+
207
+ // ---- 401 -> clear-cookie fallback ----------------------------------------
208
+
209
+ test("loadCustomer: no token resolves to guest without calling the SDK", async () => {
210
+ let called = false;
211
+ const factory: CustomerClientFactory = async () => {
212
+ called = true;
213
+ return null;
214
+ };
215
+ const result = await loadCustomer(null, factory);
216
+ assert.equal(result, null);
217
+ assert.equal(called, false);
218
+ });
219
+
220
+ test("loadCustomer: valid token resolves the customer", async () => {
221
+ const factory = fakeFactory({
222
+ customer: {
223
+ auth: {
224
+ async me() {
225
+ return SAMPLE_CUSTOMER;
226
+ },
227
+ },
228
+ },
229
+ });
230
+ const result = await loadCustomer("jwt-ok", factory);
231
+ assert.deepEqual(result, SAMPLE_CUSTOMER);
232
+ });
233
+
234
+ test("loadCustomer: expired token (me() -> null on 401) falls back to guest", async () => {
235
+ const factory = fakeFactory({
236
+ customer: {
237
+ auth: {
238
+ // SDK me() returns null on 401 instead of throwing.
239
+ async me() {
240
+ return null;
241
+ },
242
+ },
243
+ },
244
+ });
245
+ const result = await loadCustomer("jwt-expired", factory);
246
+ assert.equal(result, null);
247
+ });
248
+
249
+ test("loadCustomer: backend error falls back to guest (never throws in render)", async () => {
250
+ const factory = fakeFactory({
251
+ customer: {
252
+ auth: {
253
+ async me() {
254
+ throw Object.assign(new Error("Service unavailable"), {
255
+ status: 503,
256
+ });
257
+ },
258
+ },
259
+ },
260
+ });
261
+ const result = await loadCustomer("jwt-ok", factory);
262
+ assert.equal(result, null);
263
+ });