@sayren/storefront-sdk 0.6.0 → 0.8.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.
@@ -1,4 +1,4 @@
1
- import { a as AnonymousSession, c as LoginRequest, d as TokenPair, f as WithdrawRequest, l as SignupRequest, p as WithdrawalBlockedDetails, t as ApiError, u as SocialProvider } from "../http-CqAR0_w4.mjs";
1
+ import { a as AnonymousSession, d as SignupRequest, f as SocialProvider, h as WithdrawalBlockedDetails, l as LoginRequest, m as WithdrawRequest, p as TokenPair, t as ApiError, u as MemberIdentities } from "../http-BbFHVGBf.mjs";
2
2
  //#region src/auth/index.d.ts
3
3
  /** 소셜 로그인 시작 결과 — `codeVerifier`와 `state`는 돌아올 때까지 서버 세션(httpOnly 쿠키 등)에 보관한다 */
4
4
  export interface IdpStart {
@@ -31,7 +31,7 @@ export interface StorefrontAuthOptions {
31
31
  fetch?: typeof globalThis.fetch;
32
32
  }
33
33
  /**
34
- * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
34
+ * 구매자 인증 — 화면은 스토어프론트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
35
35
  * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
36
36
  *
37
37
  * 토큰 보관은 앱 몫이다. 서버(loader·action)에서 호출하고 httpOnly 쿠키에 두기를 권한다.
@@ -56,7 +56,7 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
56
56
  /**
57
57
  * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
58
58
  * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
59
- * `redirectUri`는 스토어프런트 도메인 목록 안이어야 한다
59
+ * `redirectUri`는 스토어프론트 도메인 목록 안이어야 한다
60
60
  */
61
61
  idp(provider: SocialProvider, options: {
62
62
  redirectUri: string;
@@ -68,6 +68,24 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
68
68
  idpCallback(input: IdpCallbackInput & {
69
69
  error?: string | null;
70
70
  }): Promise<TokenPair>;
71
+ /** 로그인 수단 — 비밀번호가 있는지, 어떤 소셜 계정이 연결됐는지 */
72
+ identities(accessToken: string): Promise<MemberIdentities>;
73
+ /**
74
+ * 소셜 계정 연결 시작 — 로그인한 구매자에 공급자 계정을 붙인다. `idp()`처럼 `state`·`codeVerifier`를 보관하고
75
+ * 구매자를 `url`로 보낸다. 돌아오면 `linkIdpCallback()`으로 마무리한다. 연결은 흐름을 시작한 구매자에게만 된다
76
+ */
77
+ linkIdp(accessToken: string, provider: SocialProvider, options: {
78
+ redirectUri: string;
79
+ }): Promise<IdpStart>;
80
+ /**
81
+ * 소셜 계정 연결 마무리 — state를 비교하고 code를 바꿔 연결한다. 다른 구매자에 이미 연결된 계정이면
82
+ * 409 `IDENTITY_IN_USE`, 같은 공급자의 다른 계정을 이미 연결했으면 409 `PROVIDER_ALREADY_LINKED`
83
+ */
84
+ linkIdpCallback(accessToken: string, provider: SocialProvider, input: Omit<IdpCallbackInput, "cartToken"> & {
85
+ error?: string | null;
86
+ }): Promise<MemberIdentities>;
87
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
88
+ unlinkIdp(accessToken: string, provider: SocialProvider): Promise<MemberIdentities>;
71
89
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
72
90
  refresh(refreshToken: string): Promise<TokenPair>;
73
91
  /**
@@ -84,4 +102,4 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
84
102
  };
85
103
  export type StorefrontAuth = ReturnType<typeof createStorefrontAuth>;
86
104
  //#endregion
87
- export { type AnonymousSession, ApiError, type LoginRequest, type SignupRequest, type SocialProvider, type TokenPair, type WithdrawRequest, type WithdrawalBlockedDetails };
105
+ export { type AnonymousSession, ApiError, type LoginRequest, type MemberIdentities, type SignupRequest, type SocialProvider, type TokenPair, type WithdrawRequest, type WithdrawalBlockedDetails };
@@ -1,4 +1,4 @@
1
- import { ct as ApiError, t as createStorefrontClient } from "../client-Do9-LG5X.mjs";
1
+ import { it as ApiError, t as createStorefrontClient } from "../client-DwqExvZN.mjs";
2
2
  //#region src/auth/index.ts
3
3
  /** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
4
4
  var IdpCallbackError = class extends Error {
@@ -22,7 +22,7 @@ async function s256(verifier) {
22
22
  return base64url(new Uint8Array(digest));
23
23
  }
24
24
  /**
25
- * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
25
+ * 구매자 인증 — 화면은 스토어프론트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
26
26
  * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
27
27
  *
28
28
  * 토큰 보관은 앱 몫이다. 서버(loader·action)에서 호출하고 httpOnly 쿠키에 두기를 권한다.
@@ -60,7 +60,7 @@ function createStorefrontAuth(options) {
60
60
  /**
61
61
  * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
62
62
  * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
63
- * `redirectUri`는 스토어프런트 도메인 목록 안이어야 한다
63
+ * `redirectUri`는 스토어프론트 도메인 목록 안이어야 한다
64
64
  */
65
65
  async idp(provider, options) {
66
66
  const state = randomToken(24);
@@ -90,6 +90,45 @@ function createStorefrontAuth(options) {
90
90
  cartToken: input.cartToken
91
91
  });
92
92
  },
93
+ /** 로그인 수단 — 비밀번호가 있는지, 어떤 소셜 계정이 연결됐는지 */
94
+ identities(accessToken) {
95
+ return client(accessToken).auth.identities();
96
+ },
97
+ /**
98
+ * 소셜 계정 연결 시작 — 로그인한 구매자에 공급자 계정을 붙인다. `idp()`처럼 `state`·`codeVerifier`를 보관하고
99
+ * 구매자를 `url`로 보낸다. 돌아오면 `linkIdpCallback()`으로 마무리한다. 연결은 흐름을 시작한 구매자에게만 된다
100
+ */
101
+ async linkIdp(accessToken, provider, options) {
102
+ const state = randomToken(24);
103
+ const codeVerifier = randomToken(32);
104
+ const { url } = await client(accessToken).auth.linkIdentityAuthorize(provider, {
105
+ redirectUri: options.redirectUri,
106
+ codeChallenge: await s256(codeVerifier),
107
+ state
108
+ });
109
+ return {
110
+ url,
111
+ state,
112
+ codeVerifier
113
+ };
114
+ },
115
+ /**
116
+ * 소셜 계정 연결 마무리 — state를 비교하고 code를 바꿔 연결한다. 다른 구매자에 이미 연결된 계정이면
117
+ * 409 `IDENTITY_IN_USE`, 같은 공급자의 다른 계정을 이미 연결했으면 409 `PROVIDER_ALREADY_LINKED`
118
+ */
119
+ linkIdpCallback(accessToken, provider, input) {
120
+ if (input.error) return Promise.reject(new IdpCallbackError(input.error));
121
+ if (!input.state || input.state !== input.expectedState) return Promise.reject(new IdpCallbackError("state_mismatch"));
122
+ if (!input.code) return Promise.reject(new IdpCallbackError("missing_code"));
123
+ return client(accessToken).auth.linkIdentity(provider, {
124
+ code: input.code,
125
+ codeVerifier: input.codeVerifier
126
+ });
127
+ },
128
+ /** 소셜 계정 연결 해제 — 마지막 로그인 수단이면 409 `LAST_LOGIN_METHOD` */
129
+ unlinkIdp(accessToken, provider) {
130
+ return client(accessToken).auth.unlinkIdentity(provider);
131
+ },
93
132
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
94
133
  refresh(refreshToken) {
95
134
  return client().auth.refresh(refreshToken);
@@ -0,0 +1,189 @@
1
+ import { z } from "zod";
2
+ //#region src/schemas/checkout.ts
3
+ const createCheckoutRequestSchema = z.object({
4
+ cartItemIds: z.array(z.string()).min(1).optional(),
5
+ directItem: z.object({
6
+ productId: z.string(),
7
+ optionId: z.string().optional(),
8
+ quantity: z.number().int().min(1)
9
+ }).optional()
10
+ }).refine((value) => value.cartItemIds ? !value.directItem : !!value.directItem, { message: "cartItemIds와 directItem 중 하나만 전달해야 합니다" });
11
+ const checkoutSessionSchema = z.object({
12
+ checkoutId: z.string(),
13
+ items: z.array(z.object({
14
+ productId: z.string(),
15
+ productName: z.string(),
16
+ /** variantId. 옵션 없는 상품은 default variant id */
17
+ optionId: z.string().nullable(),
18
+ optionName: z.string().nullable(),
19
+ quantity: z.number().int(),
20
+ unitPrice: z.number().int(),
21
+ totalPrice: z.number().int()
22
+ })),
23
+ amounts: z.object({
24
+ productAmount: z.number().int(),
25
+ deliveryFee: z.number().int(),
26
+ totalAmount: z.number().int()
27
+ }),
28
+ expiresAt: z.string(),
29
+ paymentOptions: z.lazy(() => paymentOptionListSchema).describe("이 주문서에서 고를 수 있는 결제 옵션, PG 우선순위순. 셀러가 켠 PG·결제수단·간편결제만 담긴다. 비어 있으면 지금 결제할 수 없다"),
30
+ testPayment: z.boolean().describe("지금 결제하면 테스트 결제(샌드박스)인가. true면 실제로 돈이 나가지 않아요 — 주문서에 테스트 결제 안내를 표시해요. 결제 요청 시점에 스토어 결제 설정으로 다시 정해지니 최종 값은 결제 요청 응답의 `testPayment`예요")
31
+ });
32
+ const paymentMethodSchema = z.enum([
33
+ "CARD",
34
+ "BANK_TRANSFER",
35
+ "VIRTUAL_ACCOUNT",
36
+ "MOBILE",
37
+ "EASY_PAY"
38
+ ]);
39
+ /** 결제를 처리하는 PG. 셀러가 직접 계약하고 콘솔 설정 › 결제에서 연결한다 */
40
+ const pgProviderSchema = z.enum(["tosspayments", "portone"]);
41
+ /** 간편결제사. 토스페이먼츠·포트원 모두 같은 코드를 쓴다 */
42
+ const easyPayProviderSchema = z.enum([
43
+ "NAVERPAY",
44
+ "KAKAOPAY",
45
+ "TOSSPAY",
46
+ "PAYCO"
47
+ ]);
48
+ /**
49
+ * 응답에 실리는 간편결제사 — 서버가 간편결제사를 추가해도 파싱이 깨지지 않게 문자열로 받는다
50
+ * (`pgProvider`·결제 상태와 같은 방식). 요청과 결제 옵션은 값을 골라 보내는 자리라 enum 그대로다.
51
+ */
52
+ const easyPayProviderResponseSchema = z.string();
53
+ const pg = pgProviderSchema.describe("결제를 처리할 PG");
54
+ /**
55
+ * 결제 옵션 — 어느 PG로 어떤 결제수단을 쓸지. `method`로 좁히면 필요한 필드가 타입으로 정해진다
56
+ * (`EASY_PAY`만 `provider`가 있다). 주문서의 `paymentOptions` 항목을 그대로 넘기거나 직접 써도 된다.
57
+ * 셀러가 켜지 않은 조합이면 서버가 409 `PAYMENT_OPTION_UNAVAILABLE`로 거절한다.
58
+ */
59
+ const paymentOptionSchema = z.discriminatedUnion("method", [
60
+ z.object({
61
+ pg,
62
+ method: z.literal("CARD")
63
+ }),
64
+ z.object({
65
+ pg,
66
+ method: z.literal("EASY_PAY"),
67
+ provider: easyPayProviderSchema.describe("간편결제사 — 이 간편결제 결제창을 바로 연다")
68
+ }),
69
+ z.object({
70
+ pg,
71
+ method: z.literal("BANK_TRANSFER")
72
+ }),
73
+ z.object({
74
+ pg,
75
+ method: z.literal("VIRTUAL_ACCOUNT")
76
+ }),
77
+ z.object({
78
+ pg,
79
+ method: z.literal("MOBILE")
80
+ })
81
+ ]);
82
+ /** 주문서가 내려주는 결제 옵션 — 결제 옵션 + 표시 이름 */
83
+ const availablePaymentOptionSchema = paymentOptionSchema.and(z.object({
84
+ label: z.string().describe("결제수단 표시 이름 (예: 신용·체크카드, 네이버페이)"),
85
+ pgName: z.string().describe("PG 표시 이름 (예: 토스페이먼츠)")
86
+ }));
87
+ /**
88
+ * 결제 옵션 목록 — 서버가 나중에 추가한 결제수단·PG를 이 SDK가 모르면 **그 항목만 빼고** 읽는다(목록 파싱은 깨지지 않는다).
89
+ *
90
+ * 모르는 항목을 버리는 일은 `preprocess`가 하고 항목 타입은 그대로 남는다 — OpenAPI 문서에 `items` 없는
91
+ * 배열로 나가지 않게 하기 위해서다(`z.array(z.unknown()).transform(...)`은 출력 타입을 표현하지 못한다).
92
+ */
93
+ const paymentOptionListSchema = z.preprocess((value) => Array.isArray(value) ? value.filter((item) => availablePaymentOptionSchema.safeParse(item).success) : value, z.array(availablePaymentOptionSchema));
94
+ /** 결제 옵션을 화면 키로 — 라디오 value·React key. 예: `tosspayments:CARD`, `portone:EASY_PAY:NAVERPAY` */
95
+ function paymentOptionKey(option) {
96
+ return option.method === "EASY_PAY" ? `${option.pg}:EASY_PAY:${option.provider}` : `${option.pg}:${option.method}`;
97
+ }
98
+ const shippingAddressInputSchema = z.object({
99
+ addressId: z.string().optional(),
100
+ receiverName: z.string().min(1, "수령인명을 입력해주세요").max(50),
101
+ phone: z.string().regex(/^01[0-9]{8,9}$/, "연락처 형식이 아닙니다 (예: 01012345678)"),
102
+ zipCode: z.string().regex(/^[0-9]{5}$/, "우편번호는 5자리 숫자입니다"),
103
+ address1: z.string().min(1, "기본 주소를 입력해주세요"),
104
+ address2: z.string().optional(),
105
+ deliveryMemo: z.string().max(100).optional(),
106
+ entranceCode: z.string().max(20).optional()
107
+ });
108
+ const guestInfoSchema = z.object({
109
+ name: z.string().min(1),
110
+ phone: z.string().regex(/^01[0-9]{8,9}$/),
111
+ email: z.email(),
112
+ orderPassword: z.string().min(6, "주문 조회 비밀번호는 6자 이상이어야 합니다")
113
+ });
114
+ /**
115
+ * 결제 시작 — 배송지를 확정하고 고른 결제 옵션으로 결제 시도를 연다. 응답의 `payUrl`(결제 서비스)을 팝업(`mode=popup`)이나
116
+ * 전체 페이지로 열면 결제 서비스가 PG 결제창을 띄우고 승인까지 한다. 리다이렉트 결제가 끝나면 구매자는 `returnUrl`로 돌아온다.
117
+ * `returnUrl`의 도메인은 스토어 결제 도메인(콘솔 설정 › 결제)에 등록돼 있어야 한다(테스트 결제는 localhost 허용).
118
+ */
119
+ const startPaymentRequestSchema = z.object({
120
+ option: paymentOptionSchema,
121
+ returnUrl: z.url().max(2048).describe("결제를 마치면 돌아올 스토어프론트 주소. 스토어 결제 도메인에 등록된 도메인이어야 한다"),
122
+ shippingAddress: shippingAddressInputSchema,
123
+ guest: guestInfoSchema.optional()
124
+ });
125
+ /** 같은 결제에서 다른 결제 옵션으로 다시 시도. `returnUrl`을 생략하면 결제 시작 때의 복귀 주소를 쓴다 */
126
+ const retryPaymentRequestSchema = z.object({
127
+ option: paymentOptionSchema,
128
+ returnUrl: startPaymentRequestSchema.shape.returnUrl.optional()
129
+ });
130
+ const paymentStartSchema = z.object({
131
+ paymentId: z.string().describe("결제 id. 상태 조회·재시도의 키다"),
132
+ attemptId: z.string().describe("결제 시도 id. PG 주문번호(토스 orderId, 포트원 paymentId)다"),
133
+ option: paymentOptionSchema.describe("이 시도의 결제 옵션"),
134
+ payUrl: z.string().describe("결제 서비스 주소. 팝업으로 열려면 쿼리 `mode=popup`을 붙이고, 그대로 이동하면 리다이렉트 결제다"),
135
+ expiresAt: z.string().describe("결제 요청 만료 시각"),
136
+ testPayment: z.boolean().describe("테스트 결제(샌드박스) 여부. true면 실제로 돈이 나가지 않는다 — 결제 화면에 테스트 결제 안내를 표시한다")
137
+ });
138
+ /** 결제창에서 결제하지 못한 이유 */
139
+ const paymentFailureCategorySchema = z.enum([
140
+ "PREPARE_FAILURE",
141
+ "DECLINED",
142
+ "USER_CANCELED"
143
+ ]);
144
+ /**
145
+ * 결제 상태 조회의 현재 상태 값 — 응답 스키마는 서버가 상태를 추가해도 파싱이 깨지지 않게 문자열로 받는다
146
+ * (`pgProvider`와 같은 방식). 화면 분기는 이 목록으로 하고 모르는 값은 확인 중으로 다룬다.
147
+ */
148
+ const PAYMENT_STATUS_VALUES = [
149
+ "pending",
150
+ "processing",
151
+ "completed",
152
+ "failed",
153
+ "expired"
154
+ ];
155
+ /** 결제 결과 구분 — 결제 서비스가 스토어프론트에 돌려주는 값과 같다 */
156
+ const PAYMENT_RESULT_VALUES = [
157
+ "COMPLETED",
158
+ "PROCESSING",
159
+ "CANCELED",
160
+ "FAILED",
161
+ "EXPIRED",
162
+ "PENDING"
163
+ ];
164
+ const paymentStatusSchema = z.object({
165
+ paymentId: z.string(),
166
+ status: z.string().describe("결제 상태. 현재 값은 `pending` 결제 대기 · `processing` 승인 확인 중 · `completed` 결제 완료(주문 생성) · `failed` 실패 · `expired` 만료예요. 상태가 추가될 수 있어 문자열로 받아요 — 모르는 값은 `processing`처럼 다루고 계속 조회하세요"),
167
+ result: z.string().describe("화면 분기용 결과. `COMPLETED` 결제 완료 · `PROCESSING` 결과 확인 중 · `CANCELED` 구매자가 결제창을 닫음 · `FAILED` 결제 실패(다른 결제 옵션으로 재시도 가능) · `EXPIRED` 결제 요청 만료 · `PENDING` 결제 전이에요. 값이 추가될 수 있어 문자열로 받아요 — 모르는 값은 `PROCESSING`처럼 다루세요"),
168
+ orderId: z.string().nullable().describe("결제가 완료됐으면 주문 번호, 그 외에는 null"),
169
+ lastFailure: z.object({
170
+ attemptId: z.string(),
171
+ category: z.string().describe("`PREPARE_FAILURE`·`DECLINED`·`USER_CANCELED`(값이 추가될 수 있다)"),
172
+ code: z.string().nullable().describe("PG가 준 실패 코드. 내부 사유는 null이에요. 화면 분기는 `category`로 하세요"),
173
+ message: z.string().nullable().describe("구매자에게 보여 줄 안내 문구")
174
+ }).nullable().describe("현재 결제 시도가 실패·취소로 끝났으면 그 내용, 아니면 null"),
175
+ testPayment: z.boolean().describe("테스트 결제(샌드박스)인가. true면 실제로 돈이 나가지 않았어요"),
176
+ expiresAt: z.string().describe("결제 요청 만료 시각")
177
+ });
178
+ /**
179
+ * 결제 서비스가 팝업 opener에 보내는 메시지 — `type`으로 거르고 결과의 원천은 결제 상태 조회다.
180
+ * 안내 문구는 싣지 않는다. 화면 문구는 상태 조회의 `lastFailure`를 쓴다
181
+ */
182
+ const paymentMessageSchema = z.object({
183
+ type: z.literal("sayren:payment"),
184
+ paymentId: z.string(),
185
+ result: z.string(),
186
+ orderId: z.string().optional()
187
+ });
188
+ //#endregion
189
+ export { pgProviderSchema as _, createCheckoutRequestSchema as a, startPaymentRequestSchema as b, guestInfoSchema as c, paymentMethodSchema as d, paymentOptionKey as f, paymentStatusSchema as g, paymentStartSchema as h, checkoutSessionSchema as i, paymentFailureCategorySchema as l, paymentOptionSchema as m, PAYMENT_STATUS_VALUES as n, easyPayProviderResponseSchema as o, paymentOptionListSchema as p, availablePaymentOptionSchema as r, easyPayProviderSchema as s, PAYMENT_RESULT_VALUES as t, paymentMessageSchema as u, retryPaymentRequestSchema as v, shippingAddressInputSchema as y };