@sayren/storefront-sdk 0.5.0 → 0.6.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,5 +1,28 @@
1
- import { a as AnonymousSession, c as TokenPair, l as WithdrawRequest, o as LoginRequest, s as SignupRequest, t as ApiError, u as WithdrawalBlockedDetails } from "../http-CxqZxTfE.mjs";
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";
2
2
  //#region src/auth/index.d.ts
3
+ /** 소셜 로그인 시작 결과 — `codeVerifier`와 `state`는 돌아올 때까지 서버 세션(httpOnly 쿠키 등)에 보관한다 */
4
+ export interface IdpStart {
5
+ /** 구매자를 보낼 공급자 로그인 주소 */
6
+ url: string;
7
+ state: string;
8
+ codeVerifier: string;
9
+ }
10
+ export interface IdpCallbackInput {
11
+ /** 돌아온 주소의 `code`(성공) — 실패면 `error`가 온다 */
12
+ code: string | null;
13
+ /** 돌아온 주소의 `state` */
14
+ state: string | null;
15
+ /** `idp()`가 준 값 — 보관해 둔 것 */
16
+ expectedState: string;
17
+ codeVerifier: string;
18
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
19
+ cartToken?: string;
20
+ }
21
+ /** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
22
+ export declare class IdpCallbackError extends Error {
23
+ readonly reason: "access_denied" | "state_mismatch" | "missing_code" | string;
24
+ constructor(reason: "access_denied" | "state_mismatch" | "missing_code" | string);
25
+ }
3
26
  export interface StorefrontAuthOptions {
4
27
  /** 예: https://api.sayren.app/storefront/v1 */
5
28
  baseUrl: string;
@@ -30,6 +53,21 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
30
53
  session(body: LoginRequest): Promise<TokenPair>;
31
54
  /** 회원가입 — 가입과 동시에 로그인 토큰을 받는다. `cartToken`을 주면 장바구니를 병합한다 */
32
55
  signUp(body: SignupRequest): Promise<TokenPair>;
56
+ /**
57
+ * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
58
+ * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
59
+ * `redirectUri`는 스토어프런트 도메인 목록 안이어야 한다
60
+ */
61
+ idp(provider: SocialProvider, options: {
62
+ redirectUri: string;
63
+ }): Promise<IdpStart>;
64
+ /**
65
+ * 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
66
+ * `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다
67
+ */
68
+ idpCallback(input: IdpCallbackInput & {
69
+ error?: string | null;
70
+ }): Promise<TokenPair>;
33
71
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
34
72
  refresh(refreshToken: string): Promise<TokenPair>;
35
73
  /**
@@ -46,4 +84,4 @@ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
46
84
  };
47
85
  export type StorefrontAuth = ReturnType<typeof createStorefrontAuth>;
48
86
  //#endregion
49
- export { type AnonymousSession, ApiError, type LoginRequest, type SignupRequest, type TokenPair, type WithdrawRequest, type WithdrawalBlockedDetails };
87
+ export { type AnonymousSession, ApiError, type LoginRequest, type SignupRequest, type SocialProvider, type TokenPair, type WithdrawRequest, type WithdrawalBlockedDetails };
@@ -1,5 +1,26 @@
1
- import { it as ApiError, t as createStorefrontClient } from "../client-D-w3LIU2.mjs";
1
+ import { ct as ApiError, t as createStorefrontClient } from "../client-Do9-LG5X.mjs";
2
2
  //#region src/auth/index.ts
3
+ /** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
4
+ var IdpCallbackError = class extends Error {
5
+ reason;
6
+ constructor(reason) {
7
+ super(`소셜 로그인을 마치지 못했어요 (${reason})`);
8
+ this.reason = reason;
9
+ this.name = "IdpCallbackError";
10
+ }
11
+ };
12
+ function base64url(bytes) {
13
+ let binary = "";
14
+ for (const byte of bytes) binary += String.fromCharCode(byte);
15
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
16
+ }
17
+ function randomToken(bytes = 32) {
18
+ return base64url(globalThis.crypto.getRandomValues(new Uint8Array(bytes)));
19
+ }
20
+ async function s256(verifier) {
21
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
22
+ return base64url(new Uint8Array(digest));
23
+ }
3
24
  /**
4
25
  * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
5
26
  * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
@@ -36,6 +57,39 @@ function createStorefrontAuth(options) {
36
57
  signUp(body) {
37
58
  return client().auth.signup(body);
38
59
  },
60
+ /**
61
+ * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
62
+ * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
63
+ * `redirectUri`는 스토어프런트 도메인 목록 안이어야 한다
64
+ */
65
+ async idp(provider, options) {
66
+ const state = randomToken(24);
67
+ const codeVerifier = randomToken(32);
68
+ const { url } = await client().auth.idpAuthorize(provider, {
69
+ redirectUri: options.redirectUri,
70
+ codeChallenge: await s256(codeVerifier),
71
+ state
72
+ });
73
+ return {
74
+ url,
75
+ state,
76
+ codeVerifier
77
+ };
78
+ },
79
+ /**
80
+ * 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
81
+ * `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다
82
+ */
83
+ idpCallback(input) {
84
+ if (input.error) return Promise.reject(new IdpCallbackError(input.error));
85
+ if (!input.state || input.state !== input.expectedState) return Promise.reject(new IdpCallbackError("state_mismatch"));
86
+ if (!input.code) return Promise.reject(new IdpCallbackError("missing_code"));
87
+ return client().auth.idpToken({
88
+ code: input.code,
89
+ codeVerifier: input.codeVerifier,
90
+ cartToken: input.cartToken
91
+ });
92
+ },
39
93
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
40
94
  refresh(refreshToken) {
41
95
  return client().auth.refresh(refreshToken);
@@ -63,4 +117,4 @@ function createStorefrontAuth(options) {
63
117
  };
64
118
  }
65
119
  //#endregion
66
- export { ApiError, createStorefrontAuth };
120
+ export { ApiError, IdpCallbackError, createStorefrontAuth };
@@ -155,7 +155,12 @@ const anonymousSessionSchema = z.object({
155
155
  * 진행 중인 주문(결제 대기~배송 중)·클레임·결제가 있으면 409 `WITHDRAWAL_BLOCKED`, 비밀번호가 틀리면
156
156
  * 403 `PASSWORD_MISMATCH`다(로그인과 같은 5회 잠금).
157
157
  */
158
- const withdrawRequestSchema = z.object({ password: z.string().min(1) });
158
+ const withdrawRequestSchema = z.object({
159
+ /**
160
+ * 비밀번호로 가입한 구매자는 필수. 소셜 로그인으로만 가입한 구매자는 비밀번호가 없어 생략하고, 대신 최근 10분 안에
161
+ * 로그인한 액세스 토큰이어야 한다(아니면 403 `REAUTH_REQUIRED` — 다시 로그인한 뒤 호출)
162
+ */
163
+ password: z.string().min(1).optional() });
159
164
  /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수 */
160
165
  const withdrawalBlockedDetailsSchema = z.object({
161
166
  /** 결제 대기~배송 중인 주문 상품 */
@@ -165,6 +170,31 @@ const withdrawalBlockedDetailsSchema = z.object({
165
170
  /** 승인 중이거나 결제창이 열려 있을 수 있는 결제(결제창은 만료되면 풀린다) */
166
171
  pendingPayments: z.number().int()
167
172
  });
173
+ /** 소셜 로그인 공급자 */
174
+ const socialProviderSchema = z.enum([
175
+ "kakao",
176
+ "naver",
177
+ "google"
178
+ ]);
179
+ /** `POST /auth/idp/{provider}/authorize` 본문 — PKCE(S256)와 state는 SDK가 만든다 */
180
+ const idpAuthorizeRequestSchema = z.object({
181
+ /** 로그인을 마치고 돌아올 스토어프런트 주소. 스토어프런트 도메인 목록 안이어야 한다 */
182
+ redirectUri: z.url(),
183
+ /** base64url(SHA-256(codeVerifier)) */
184
+ codeChallenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
185
+ /** 스토어프런트가 만든 값 — 돌아올 때 그대로 붙어 온다. 세션에 저장해 두고 비교한다 */
186
+ state: z.string().min(16).max(128)
187
+ });
188
+ const idpAuthorizeResponseSchema = z.object({
189
+ /** 구매자를 보낼 공급자 로그인 주소 */
190
+ url: z.url() });
191
+ /** `POST /auth/idp/token` 본문 — 돌아온 1회용 code를 구매자 토큰으로 바꾼다 */
192
+ const idpTokenRequestSchema = z.object({
193
+ code: z.string().min(1),
194
+ codeVerifier: z.string().regex(/^[A-Za-z0-9._~-]{43,128}$/),
195
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
196
+ cartToken: z.string().optional()
197
+ });
168
198
  //#endregion
169
199
  //#region src/schemas/cart.ts
170
200
  const cartItemSchema = z.object({
@@ -512,7 +542,8 @@ const createInquiryRequestSchema = z.object({
512
542
  //#region src/schemas/member.ts
513
543
  const memberSchema = z.object({
514
544
  memberId: z.string(),
515
- email: z.email(),
545
+ /** 소셜 가입에서 공급자가 이메일을 주지 않았으면 null */
546
+ email: z.email().nullable(),
516
547
  name: z.string(),
517
548
  phone: z.string().nullable(),
518
549
  point: z.number().int(),
@@ -656,7 +687,16 @@ const storefrontStoreSchema = z.object({
656
687
  * 비회원 주문 허용. false면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다.
657
688
  * 장바구니 담기는 된다 — 주문 버튼을 로그인 안내로 바꾸면 된다
658
689
  */
659
- guestCheckout: z.boolean()
690
+ guestCheckout: z.boolean(),
691
+ /**
692
+ * 켜진 로그인 방법 — 화면에 이 순서대로 버튼을 그리면 된다. `password`가 없으면 이메일·비밀번호 가입·로그인이 꺼져 있다
693
+ */
694
+ loginMethods: z.array(z.enum([
695
+ "password",
696
+ "kakao",
697
+ "naver",
698
+ "google"
699
+ ]))
660
700
  });
661
701
  //#endregion
662
702
  //#region src/client.ts
@@ -712,6 +752,10 @@ function createStorefrontClient(options) {
712
752
  login: (body) => http.request("POST", "/auth/login", tokenPairSchema, { body: loginRequestSchema.parse(body) }),
713
753
  refresh: (refreshToken) => http.request("POST", "/auth/token/refresh", tokenPairSchema, { body: { refreshToken } }),
714
754
  logout: () => http.requestVoid("POST", "/auth/logout"),
755
+ /** 소셜 로그인 시작 — 공급자 로그인 주소를 받는다. PKCE·state는 `@sayren/storefront-sdk/auth`의 `idp()`가 만든다 */
756
+ idpAuthorize: (provider, body) => http.request("POST", path`/auth/idp/${provider}/authorize`, idpAuthorizeResponseSchema, { body: idpAuthorizeRequestSchema.parse(body) }),
757
+ /** 소셜 로그인 마무리 — 돌아온 1회용 code + codeVerifier를 구매자 토큰으로 바꾼다 */
758
+ idpToken: (body) => http.request("POST", "/auth/idp/token", tokenPairSchema, { body: idpTokenRequestSchema.parse(body) }),
715
759
  /** 구매자 탈퇴 — 로그인 상태(accessToken)에서 비밀번호로 본인 확인. 성공하면 토큰은 모두 무효 */
716
760
  withdraw: (body) => http.requestVoid("POST", "/auth/withdraw", { body: withdrawRequestSchema.parse(body) })
717
761
  },
@@ -810,4 +854,4 @@ getStatus: (paymentId) => http.request("GET", path`/payments/${paymentId}`, paym
810
854
  };
811
855
  }
812
856
  //#endregion
813
- export { loginRequestSchema as $, pageSchema as A, pgParamsSchema as B, claimTypeSchema as C, claimStatusSchema as D, myClaimSchema as E, createCheckoutRequestSchema as F, productDetailSchema as G, shippingAddressInputSchema as H, guestInfoSchema as I, addCartItemRequestSchema as J, productFacetsSchema as K, paymentMethodSchema as L, checkoutSessionSchema as M, confirmPaymentRequestSchema as N, orderItemStatusSchema as O, confirmPaymentResultSchema as P, anonymousSessionSchema as Q, paymentParamsSchema as R, claimReasonSchema as S, createClaimResultSchema as T, categoryNodeSchema as U, requestPaymentRequestSchema as V, productCardSchema as W, cartSchema as X, cartItemSchema as Y, updateCartItemRequestSchema as Z, createInquiryRequestSchema as _, publicReviewSchema as a, buildQuery as at, customerInquiryCategorySchema as b, writableReviewSchema as c, myOrderSchema as d, signupRequestSchema as et, orderShippingAddressSchema as f, updateProfileRequestSchema as g, memberSchema as h, createReviewResultSchema as i, ApiError as it, PAYMENT_STATUS_VALUES as j, pageInfoSchema as k, deliveryTrackingSchema as l, memberAddressSchema as m, storefrontStoreSchema as n, withdrawRequestSchema as nt, reviewSummarySchema as o, toApiError as ot, memberAddressRequestSchema as p, productSortSchema as q, createReviewRequestSchema as r, withdrawalBlockedDetailsSchema as rt, updateReviewRequestSchema as s, unwrapData as st, createStorefrontClient as t, tokenPairSchema as tt, myOrderItemSchema as u, publicInquirySchema as v, createClaimRequestSchema as w, customerInquirySchema as x, createCustomerInquiryRequestSchema as y, paymentStatusSchema as z };
857
+ export { idpAuthorizeRequestSchema as $, pageSchema as A, pgParamsSchema as B, claimTypeSchema as C, claimStatusSchema as D, myClaimSchema as E, createCheckoutRequestSchema as F, productDetailSchema as G, shippingAddressInputSchema as H, guestInfoSchema as I, addCartItemRequestSchema as J, productFacetsSchema as K, paymentMethodSchema as L, checkoutSessionSchema as M, confirmPaymentRequestSchema as N, orderItemStatusSchema as O, confirmPaymentResultSchema as P, anonymousSessionSchema as Q, paymentParamsSchema as R, claimReasonSchema as S, createClaimResultSchema as T, categoryNodeSchema as U, requestPaymentRequestSchema as V, productCardSchema as W, cartSchema as X, cartItemSchema as Y, updateCartItemRequestSchema as Z, createInquiryRequestSchema as _, publicReviewSchema as a, tokenPairSchema as at, customerInquiryCategorySchema as b, writableReviewSchema as c, ApiError as ct, myOrderSchema as d, unwrapData as dt, idpAuthorizeResponseSchema as et, orderShippingAddressSchema as f, updateProfileRequestSchema as g, memberSchema as h, createReviewResultSchema as i, socialProviderSchema as it, PAYMENT_STATUS_VALUES as j, pageInfoSchema as k, deliveryTrackingSchema as l, buildQuery as lt, memberAddressSchema as m, storefrontStoreSchema as n, loginRequestSchema as nt, reviewSummarySchema as o, withdrawRequestSchema as ot, memberAddressRequestSchema as p, productSortSchema as q, createReviewRequestSchema as r, signupRequestSchema as rt, updateReviewRequestSchema as s, withdrawalBlockedDetailsSchema as st, createStorefrontClient as t, idpTokenRequestSchema as tt, myOrderItemSchema as u, toApiError as ut, publicInquirySchema as v, createClaimRequestSchema as w, customerInquirySchema as x, createCustomerInquiryRequestSchema as y, paymentStatusSchema as z };
@@ -41,7 +41,7 @@ type AnonymousSession = z.infer<typeof anonymousSessionSchema>;
41
41
  * 403 `PASSWORD_MISMATCH`다(로그인과 같은 5회 잠금).
42
42
  */
43
43
  declare const withdrawRequestSchema: z.ZodObject<{
44
- password: z.ZodString;
44
+ password: z.ZodOptional<z.ZodString>;
45
45
  }, z.core.$strip>;
46
46
  type WithdrawRequest = z.infer<typeof withdrawRequestSchema>;
47
47
  /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수 */
@@ -51,6 +51,30 @@ declare const withdrawalBlockedDetailsSchema: z.ZodObject<{
51
51
  pendingPayments: z.ZodNumber;
52
52
  }, z.core.$strip>;
53
53
  type WithdrawalBlockedDetails = z.infer<typeof withdrawalBlockedDetailsSchema>;
54
+ /** 소셜 로그인 공급자 */
55
+ declare const socialProviderSchema: z.ZodEnum<{
56
+ kakao: "kakao";
57
+ naver: "naver";
58
+ google: "google";
59
+ }>;
60
+ type SocialProvider = z.infer<typeof socialProviderSchema>;
61
+ /** `POST /auth/idp/{provider}/authorize` 본문 — PKCE(S256)와 state는 SDK가 만든다 */
62
+ declare const idpAuthorizeRequestSchema: z.ZodObject<{
63
+ redirectUri: z.ZodURL;
64
+ codeChallenge: z.ZodString;
65
+ state: z.ZodString;
66
+ }, z.core.$strip>;
67
+ type IdpAuthorizeRequest = z.infer<typeof idpAuthorizeRequestSchema>;
68
+ declare const idpAuthorizeResponseSchema: z.ZodObject<{
69
+ url: z.ZodURL;
70
+ }, z.core.$strip>;
71
+ /** `POST /auth/idp/token` 본문 — 돌아온 1회용 code를 구매자 토큰으로 바꾼다 */
72
+ declare const idpTokenRequestSchema: z.ZodObject<{
73
+ code: z.ZodString;
74
+ codeVerifier: z.ZodString;
75
+ cartToken: z.ZodOptional<z.ZodString>;
76
+ }, z.core.$strip>;
77
+ type IdpTokenRequest = z.infer<typeof idpTokenRequestSchema>;
54
78
  //#endregion
55
79
  //#region src/http.d.ts
56
80
  declare class ApiError extends Error {
@@ -68,4 +92,4 @@ type TokenSource = string | null | undefined | (() => string | null | undefined)
68
92
  type Query = Record<string, string | number | boolean | undefined>;
69
93
  declare function buildQuery(query?: Query): string;
70
94
  //#endregion
71
- export { AnonymousSession as a, TokenPair as c, anonymousSessionSchema as d, loginRequestSchema as f, withdrawalBlockedDetailsSchema as g, withdrawRequestSchema as h, buildQuery as i, WithdrawRequest as l, tokenPairSchema as m, Query as n, LoginRequest as o, signupRequestSchema as p, TokenSource as r, SignupRequest as s, ApiError as t, WithdrawalBlockedDetails as u };
95
+ export { withdrawalBlockedDetailsSchema as C, withdrawRequestSchema as S, idpTokenRequestSchema as _, AnonymousSession as a, socialProviderSchema as b, LoginRequest as c, TokenPair as d, WithdrawRequest as f, idpAuthorizeResponseSchema as g, idpAuthorizeRequestSchema as h, buildQuery as i, SignupRequest as l, anonymousSessionSchema as m, Query as n, IdpAuthorizeRequest as o, WithdrawalBlockedDetails as p, TokenSource as r, IdpTokenRequest as s, ApiError as t, SocialProvider as u, loginRequestSchema as v, tokenPairSchema as x, signupRequestSchema as y };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AnonymousSession, c as TokenPair, d as anonymousSessionSchema, f as loginRequestSchema, g as withdrawalBlockedDetailsSchema, h as withdrawRequestSchema, i as buildQuery, l as WithdrawRequest, m as tokenPairSchema, n as Query, o as LoginRequest, p as signupRequestSchema, r as TokenSource, s as SignupRequest, t as ApiError, u as WithdrawalBlockedDetails } from "./http-CxqZxTfE.mjs";
1
+ import { C as withdrawalBlockedDetailsSchema, S as withdrawRequestSchema, _ as idpTokenRequestSchema, a as AnonymousSession, b as socialProviderSchema, c as LoginRequest, d as TokenPair, f as WithdrawRequest, g as idpAuthorizeResponseSchema, h as idpAuthorizeRequestSchema, i as buildQuery, l as SignupRequest, m as anonymousSessionSchema, n as Query, o as IdpAuthorizeRequest, p as WithdrawalBlockedDetails, r as TokenSource, s as IdpTokenRequest, t as ApiError, u as SocialProvider, v as loginRequestSchema, x as tokenPairSchema, y as signupRequestSchema } from "./http-CqAR0_w4.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/analytics-ids.d.ts
4
4
  /**
@@ -489,7 +489,7 @@ export type CreateInquiryRequest = z.infer<typeof createInquiryRequestSchema>;
489
489
  //#region src/schemas/member.d.ts
490
490
  export declare const memberSchema: z.ZodObject<{
491
491
  memberId: z.ZodString;
492
- email: z.ZodEmail;
492
+ email: z.ZodNullable<z.ZodEmail>;
493
493
  name: z.ZodString;
494
494
  phone: z.ZodNullable<z.ZodString>;
495
495
  point: z.ZodNumber;
@@ -672,7 +672,7 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
672
672
  /** session.userinfo() 위임 — session 미전달 시 에러 */
673
673
  userinfo(): Promise<{
674
674
  memberId: string;
675
- email: string;
675
+ email: string | null;
676
676
  name: string;
677
677
  phone: string | null;
678
678
  point: number;
@@ -701,6 +701,16 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
701
701
  expiresIn: number;
702
702
  }>;
703
703
  logout: () => Promise<void>;
704
+ /** 소셜 로그인 시작 — 공급자 로그인 주소를 받는다. PKCE·state는 `@sayren/storefront-sdk/auth`의 `idp()`가 만든다 */
705
+ idpAuthorize: (provider: SocialProvider, body: IdpAuthorizeRequest) => Promise<{
706
+ url: string;
707
+ }>;
708
+ /** 소셜 로그인 마무리 — 돌아온 1회용 code + codeVerifier를 구매자 토큰으로 바꾼다 */
709
+ idpToken: (body: IdpTokenRequest) => Promise<{
710
+ accessToken: string;
711
+ refreshToken: string;
712
+ expiresIn: number;
713
+ }>;
704
714
  /** 구매자 탈퇴 — 로그인 상태(accessToken)에서 비밀번호로 본인 확인. 성공하면 토큰은 모두 무효 */
705
715
  withdraw: (body: WithdrawRequest) => Promise<void>;
706
716
  };
@@ -713,6 +723,7 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
713
723
  customerCenterPhone: string | null;
714
724
  businessHours: string | null;
715
725
  guestCheckout: boolean;
726
+ loginMethods: ("password" | "kakao" | "naver" | "google")[];
716
727
  }>;
717
728
  };
718
729
  catalog: {
@@ -1140,7 +1151,7 @@ export declare function createStorefrontClient(options: StorefrontClientOptions)
1140
1151
  member: {
1141
1152
  me: () => Promise<{
1142
1153
  memberId: string;
1143
- email: string;
1154
+ email: string | null;
1144
1155
  name: string;
1145
1156
  phone: string | null;
1146
1157
  point: number;
@@ -1507,7 +1518,13 @@ export declare const storefrontStoreSchema: z.ZodObject<{
1507
1518
  customerCenterPhone: z.ZodNullable<z.ZodString>;
1508
1519
  businessHours: z.ZodNullable<z.ZodString>;
1509
1520
  guestCheckout: z.ZodBoolean;
1521
+ loginMethods: z.ZodArray<z.ZodEnum<{
1522
+ password: "password";
1523
+ kakao: "kakao";
1524
+ naver: "naver";
1525
+ google: "google";
1526
+ }>>;
1510
1527
  }, z.core.$strip>;
1511
1528
  export type StorefrontStore = z.infer<typeof storefrontStoreSchema>;
1512
1529
  //#endregion
1513
- export { AnonymousSession, ApiError, LoginRequest, type Query, type SessionStorage, SignupRequest, type StorefrontClient, type StorefrontClientOptions, type StorefrontSession, type StorefrontSessionOptions, TokenPair, type TokenSource, WithdrawRequest, WithdrawalBlockedDetails, anonymousSessionSchema, buildQuery, loginRequestSchema, signupRequestSchema, tokenPairSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema };
1530
+ export { AnonymousSession, ApiError, IdpAuthorizeRequest, IdpTokenRequest, LoginRequest, type Query, type SessionStorage, SignupRequest, SocialProvider, type StorefrontClient, type StorefrontClientOptions, type StorefrontSession, type StorefrontSessionOptions, TokenPair, type TokenSource, WithdrawRequest, WithdrawalBlockedDetails, anonymousSessionSchema, buildQuery, idpAuthorizeRequestSchema, idpAuthorizeResponseSchema, idpTokenRequestSchema, loginRequestSchema, signupRequestSchema, socialProviderSchema, tokenPairSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as ANALYTICS_VISITOR_HEADER, c as analyticsClientEventSchema, i as ANALYTICS_VISITOR_COOKIE, l as analyticsIdSchema, n as ANALYTICS_SESSION_COOKIE, o as analyticsBatchSchema, r as ANALYTICS_SESSION_HEADER, s as analyticsClientEventNameSchema, t as ANALYTICS_MAX_BATCH_BYTES, u as analyticsServerEventNameSchema } from "./analytics-CgD70OqH.mjs";
2
2
  import { t as analyticsIdsFromCookie } from "./analytics-ids-COElZiI3.mjs";
3
- import { $ as loginRequestSchema, A as pageSchema, B as pgParamsSchema, C as claimTypeSchema, D as claimStatusSchema, E as myClaimSchema, F as createCheckoutRequestSchema, G as productDetailSchema, H as shippingAddressInputSchema, I as guestInfoSchema, J as addCartItemRequestSchema, K as productFacetsSchema, L as paymentMethodSchema, M as checkoutSessionSchema, N as confirmPaymentRequestSchema, O as orderItemStatusSchema, P as confirmPaymentResultSchema, Q as anonymousSessionSchema, R as paymentParamsSchema, S as claimReasonSchema, T as createClaimResultSchema, U as categoryNodeSchema, V as requestPaymentRequestSchema, W as productCardSchema, X as cartSchema, Y as cartItemSchema, Z as updateCartItemRequestSchema, _ as createInquiryRequestSchema, a as publicReviewSchema, at as buildQuery, b as customerInquiryCategorySchema, c as writableReviewSchema, d as myOrderSchema, et as signupRequestSchema, f as orderShippingAddressSchema, g as updateProfileRequestSchema, h as memberSchema, i as createReviewResultSchema, it as ApiError, j as PAYMENT_STATUS_VALUES, k as pageInfoSchema, l as deliveryTrackingSchema, m as memberAddressSchema, n as storefrontStoreSchema, nt as withdrawRequestSchema, o as reviewSummarySchema, ot as toApiError, p as memberAddressRequestSchema, q as productSortSchema, r as createReviewRequestSchema, rt as withdrawalBlockedDetailsSchema, s as updateReviewRequestSchema, st as unwrapData, t as createStorefrontClient, tt as tokenPairSchema, u as myOrderItemSchema, v as publicInquirySchema, w as createClaimRequestSchema, x as customerInquirySchema, y as createCustomerInquiryRequestSchema, z as paymentStatusSchema } from "./client-D-w3LIU2.mjs";
3
+ import { $ as idpAuthorizeRequestSchema, A as pageSchema, B as pgParamsSchema, C as claimTypeSchema, D as claimStatusSchema, E as myClaimSchema, F as createCheckoutRequestSchema, G as productDetailSchema, H as shippingAddressInputSchema, I as guestInfoSchema, J as addCartItemRequestSchema, K as productFacetsSchema, L as paymentMethodSchema, M as checkoutSessionSchema, N as confirmPaymentRequestSchema, O as orderItemStatusSchema, P as confirmPaymentResultSchema, Q as anonymousSessionSchema, R as paymentParamsSchema, S as claimReasonSchema, T as createClaimResultSchema, U as categoryNodeSchema, V as requestPaymentRequestSchema, W as productCardSchema, X as cartSchema, Y as cartItemSchema, Z as updateCartItemRequestSchema, _ as createInquiryRequestSchema, a as publicReviewSchema, at as tokenPairSchema, b as customerInquiryCategorySchema, c as writableReviewSchema, ct as ApiError, d as myOrderSchema, dt as unwrapData, et as idpAuthorizeResponseSchema, f as orderShippingAddressSchema, g as updateProfileRequestSchema, h as memberSchema, i as createReviewResultSchema, it as socialProviderSchema, j as PAYMENT_STATUS_VALUES, k as pageInfoSchema, l as deliveryTrackingSchema, lt as buildQuery, m as memberAddressSchema, n as storefrontStoreSchema, nt as loginRequestSchema, o as reviewSummarySchema, ot as withdrawRequestSchema, p as memberAddressRequestSchema, q as productSortSchema, r as createReviewRequestSchema, rt as signupRequestSchema, s as updateReviewRequestSchema, st as withdrawalBlockedDetailsSchema, t as createStorefrontClient, tt as idpTokenRequestSchema, u as myOrderItemSchema, ut as toApiError, v as publicInquirySchema, w as createClaimRequestSchema, x as customerInquirySchema, y as createCustomerInquiryRequestSchema, z as paymentStatusSchema } from "./client-Do9-LG5X.mjs";
4
4
  //#region src/session.ts
5
5
  /** 세션 키 상수 — SDK가 소유한다. store-sdk와 겹치지 않는 별도 네임스페이스. */
6
6
  const STOREFRONT_SESSION_KEYS = {
@@ -124,4 +124,4 @@ function createStorefrontSession(options) {
124
124
  };
125
125
  }
126
126
  //#endregion
127
- export { ANALYTICS_MAX_BATCH_BYTES, ANALYTICS_SESSION_COOKIE, ANALYTICS_SESSION_HEADER, ANALYTICS_VISITOR_COOKIE, ANALYTICS_VISITOR_HEADER, ApiError, PAYMENT_STATUS_VALUES, STOREFRONT_SESSION_KEYS, addCartItemRequestSchema, analyticsBatchSchema, analyticsClientEventNameSchema, analyticsClientEventSchema, analyticsIdSchema, analyticsIdsFromCookie, analyticsServerEventNameSchema, anonymousSessionSchema, buildQuery, cartItemSchema, cartSchema, categoryNodeSchema, checkoutSessionSchema, claimReasonSchema, claimStatusSchema, claimTypeSchema, confirmPaymentRequestSchema, confirmPaymentResultSchema, createCheckoutRequestSchema, createClaimRequestSchema, createClaimResultSchema, createCustomerInquiryRequestSchema, createInquiryRequestSchema, createReviewRequestSchema, createReviewResultSchema, createStorefrontClient, createStorefrontSession, customerInquiryCategorySchema, customerInquirySchema, deliveryTrackingSchema, guestInfoSchema, loginRequestSchema, memberAddressRequestSchema, memberAddressSchema, memberSchema, myClaimSchema, myOrderItemSchema, myOrderSchema, orderItemStatusSchema, orderShippingAddressSchema, pageInfoSchema, pageSchema, paymentMethodSchema, paymentParamsSchema, paymentStatusSchema, pgParamsSchema, productCardSchema, productDetailSchema, productFacetsSchema, productSortSchema, publicInquirySchema, publicReviewSchema, requestPaymentRequestSchema, reviewSummarySchema, shippingAddressInputSchema, signupRequestSchema, storefrontStoreSchema, tokenPairSchema, updateCartItemRequestSchema, updateProfileRequestSchema, updateReviewRequestSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema, writableReviewSchema };
127
+ export { ANALYTICS_MAX_BATCH_BYTES, ANALYTICS_SESSION_COOKIE, ANALYTICS_SESSION_HEADER, ANALYTICS_VISITOR_COOKIE, ANALYTICS_VISITOR_HEADER, ApiError, PAYMENT_STATUS_VALUES, STOREFRONT_SESSION_KEYS, addCartItemRequestSchema, analyticsBatchSchema, analyticsClientEventNameSchema, analyticsClientEventSchema, analyticsIdSchema, analyticsIdsFromCookie, analyticsServerEventNameSchema, anonymousSessionSchema, buildQuery, cartItemSchema, cartSchema, categoryNodeSchema, checkoutSessionSchema, claimReasonSchema, claimStatusSchema, claimTypeSchema, confirmPaymentRequestSchema, confirmPaymentResultSchema, createCheckoutRequestSchema, createClaimRequestSchema, createClaimResultSchema, createCustomerInquiryRequestSchema, createInquiryRequestSchema, createReviewRequestSchema, createReviewResultSchema, createStorefrontClient, createStorefrontSession, customerInquiryCategorySchema, customerInquirySchema, deliveryTrackingSchema, guestInfoSchema, idpAuthorizeRequestSchema, idpAuthorizeResponseSchema, idpTokenRequestSchema, loginRequestSchema, memberAddressRequestSchema, memberAddressSchema, memberSchema, myClaimSchema, myOrderItemSchema, myOrderSchema, orderItemStatusSchema, orderShippingAddressSchema, pageInfoSchema, pageSchema, paymentMethodSchema, paymentParamsSchema, paymentStatusSchema, pgParamsSchema, productCardSchema, productDetailSchema, productFacetsSchema, productSortSchema, publicInquirySchema, publicReviewSchema, requestPaymentRequestSchema, reviewSummarySchema, shippingAddressInputSchema, signupRequestSchema, socialProviderSchema, storefrontStoreSchema, tokenPairSchema, updateCartItemRequestSchema, updateProfileRequestSchema, updateReviewRequestSchema, withdrawRequestSchema, withdrawalBlockedDetailsSchema, writableReviewSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/storefront-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/auth/index.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  AnonymousSession,
5
5
  LoginRequest,
6
6
  SignupRequest,
7
+ SocialProvider,
7
8
  TokenPair,
8
9
  WithdrawRequest,
9
10
  } from "../schemas/auth";
@@ -14,11 +15,60 @@ export type {
14
15
  AnonymousSession,
15
16
  LoginRequest,
16
17
  SignupRequest,
18
+ SocialProvider,
17
19
  TokenPair,
18
20
  WithdrawalBlockedDetails,
19
21
  WithdrawRequest,
20
22
  } from "../schemas/auth";
21
23
 
24
+ /** 소셜 로그인 시작 결과 — `codeVerifier`와 `state`는 돌아올 때까지 서버 세션(httpOnly 쿠키 등)에 보관한다 */
25
+ export interface IdpStart {
26
+ /** 구매자를 보낼 공급자 로그인 주소 */
27
+ url: string;
28
+ state: string;
29
+ codeVerifier: string;
30
+ }
31
+
32
+ export interface IdpCallbackInput {
33
+ /** 돌아온 주소의 `code`(성공) — 실패면 `error`가 온다 */
34
+ code: string | null;
35
+ /** 돌아온 주소의 `state` */
36
+ state: string | null;
37
+ /** `idp()`가 준 값 — 보관해 둔 것 */
38
+ expectedState: string;
39
+ codeVerifier: string;
40
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
41
+ cartToken?: string;
42
+ }
43
+
44
+ /** 소셜 로그인 실패 — 구매자가 취소했거나(`access_denied`) state가 맞지 않는다 */
45
+ export class IdpCallbackError extends Error {
46
+ constructor(
47
+ public readonly reason: "access_denied" | "state_mismatch" | "missing_code" | string,
48
+ ) {
49
+ super(`소셜 로그인을 마치지 못했어요 (${reason})`);
50
+ this.name = "IdpCallbackError";
51
+ }
52
+ }
53
+
54
+ function base64url(bytes: Uint8Array): string {
55
+ let binary = "";
56
+ for (const byte of bytes) binary += String.fromCharCode(byte);
57
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
58
+ }
59
+
60
+ function randomToken(bytes = 32): string {
61
+ return base64url(globalThis.crypto.getRandomValues(new Uint8Array(bytes)));
62
+ }
63
+
64
+ async function s256(verifier: string): Promise<string> {
65
+ const digest = await globalThis.crypto.subtle.digest(
66
+ "SHA-256",
67
+ new TextEncoder().encode(verifier),
68
+ );
69
+ return base64url(new Uint8Array(digest));
70
+ }
71
+
22
72
  export interface StorefrontAuthOptions {
23
73
  /** 예: https://api.sayren.app/storefront/v1 */
24
74
  baseUrl: string;
@@ -65,6 +115,37 @@ export function createStorefrontAuth(options: StorefrontAuthOptions) {
65
115
  signUp(body: SignupRequest): Promise<TokenPair> {
66
116
  return client().auth.signup(body);
67
117
  },
118
+ /**
119
+ * 소셜 로그인 시작 — PKCE(S256)와 state를 만들고 공급자 로그인 주소를 받는다. 반환한 `state`·`codeVerifier`를
120
+ * 서버 세션에 보관하고 구매자를 `url`로 보낸다. 로그인을 마치면 `redirectUri`로 `?code=&state=`가 붙어 돌아온다.
121
+ * `redirectUri`는 스토어프런트 도메인 목록 안이어야 한다
122
+ */
123
+ async idp(provider: SocialProvider, options: { redirectUri: string }): Promise<IdpStart> {
124
+ const state = randomToken(24);
125
+ const codeVerifier = randomToken(32);
126
+ const { url } = await client().auth.idpAuthorize(provider, {
127
+ redirectUri: options.redirectUri,
128
+ codeChallenge: await s256(codeVerifier),
129
+ state,
130
+ });
131
+ return { url, state, codeVerifier };
132
+ },
133
+ /**
134
+ * 소셜 로그인 마무리 — 돌아온 `code`를 구매자 토큰으로 바꾼다. state가 다르거나 구매자가 취소했으면
135
+ * `IdpCallbackError`. code는 1회용이고 1분 안에 바꿔야 한다
136
+ */
137
+ idpCallback(input: IdpCallbackInput & { error?: string | null }): Promise<TokenPair> {
138
+ if (input.error) return Promise.reject(new IdpCallbackError(input.error));
139
+ if (!input.state || input.state !== input.expectedState) {
140
+ return Promise.reject(new IdpCallbackError("state_mismatch"));
141
+ }
142
+ if (!input.code) return Promise.reject(new IdpCallbackError("missing_code"));
143
+ return client().auth.idpToken({
144
+ code: input.code,
145
+ codeVerifier: input.codeVerifier,
146
+ cartToken: input.cartToken,
147
+ });
148
+ },
68
149
  /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
69
150
  refresh(refreshToken: string): Promise<TokenPair> {
70
151
  return client().auth.refresh(refreshToken);
package/src/client.ts CHANGED
@@ -4,7 +4,13 @@ import { ANALYTICS_SESSION_HEADER, ANALYTICS_VISITOR_HEADER } from "./schemas/an
4
4
  import type { LoginRequest, SignupRequest } from "./schemas/auth";
5
5
  import {
6
6
  anonymousSessionSchema,
7
+ type IdpAuthorizeRequest,
8
+ type IdpTokenRequest,
9
+ idpAuthorizeRequestSchema,
10
+ idpAuthorizeResponseSchema,
11
+ idpTokenRequestSchema,
7
12
  loginRequestSchema,
13
+ type SocialProvider,
8
14
  signupRequestSchema,
9
15
  tokenPairSchema,
10
16
  type WithdrawRequest,
@@ -184,6 +190,16 @@ export function createStorefrontClient(options: StorefrontClientOptions) {
184
190
  refresh: (refreshToken: string) =>
185
191
  http.request("POST", "/auth/token/refresh", tokenPairSchema, { body: { refreshToken } }),
186
192
  logout: () => http.requestVoid("POST", "/auth/logout"),
193
+ /** 소셜 로그인 시작 — 공급자 로그인 주소를 받는다. PKCE·state는 `@sayren/storefront-sdk/auth`의 `idp()`가 만든다 */
194
+ idpAuthorize: (provider: SocialProvider, body: IdpAuthorizeRequest) =>
195
+ http.request("POST", path`/auth/idp/${provider}/authorize`, idpAuthorizeResponseSchema, {
196
+ body: idpAuthorizeRequestSchema.parse(body),
197
+ }),
198
+ /** 소셜 로그인 마무리 — 돌아온 1회용 code + codeVerifier를 구매자 토큰으로 바꾼다 */
199
+ idpToken: (body: IdpTokenRequest) =>
200
+ http.request("POST", "/auth/idp/token", tokenPairSchema, {
201
+ body: idpTokenRequestSchema.parse(body),
202
+ }),
187
203
  /** 구매자 탈퇴 — 로그인 상태(accessToken)에서 비밀번호로 본인 확인. 성공하면 토큰은 모두 무효 */
188
204
  withdraw: (body: WithdrawRequest) =>
189
205
  http.requestVoid("POST", "/auth/withdraw", { body: withdrawRequestSchema.parse(body) }),
@@ -61,7 +61,11 @@ export type AnonymousSession = z.infer<typeof anonymousSessionSchema>;
61
61
  * 403 `PASSWORD_MISMATCH`다(로그인과 같은 5회 잠금).
62
62
  */
63
63
  export const withdrawRequestSchema = z.object({
64
- password: z.string().min(1),
64
+ /**
65
+ * 비밀번호로 가입한 구매자는 필수. 소셜 로그인으로만 가입한 구매자는 비밀번호가 없어 생략하고, 대신 최근 10분 안에
66
+ * 로그인한 액세스 토큰이어야 한다(아니면 403 `REAUTH_REQUIRED` — 다시 로그인한 뒤 호출)
67
+ */
68
+ password: z.string().min(1).optional(),
65
69
  });
66
70
 
67
71
  export type WithdrawRequest = z.infer<typeof withdrawRequestSchema>;
@@ -77,3 +81,32 @@ export const withdrawalBlockedDetailsSchema = z.object({
77
81
  });
78
82
 
79
83
  export type WithdrawalBlockedDetails = z.infer<typeof withdrawalBlockedDetailsSchema>;
84
+
85
+ /** 소셜 로그인 공급자 */
86
+ export const socialProviderSchema = z.enum(["kakao", "naver", "google"]);
87
+ export type SocialProvider = z.infer<typeof socialProviderSchema>;
88
+
89
+ /** `POST /auth/idp/{provider}/authorize` 본문 — PKCE(S256)와 state는 SDK가 만든다 */
90
+ export const idpAuthorizeRequestSchema = z.object({
91
+ /** 로그인을 마치고 돌아올 스토어프런트 주소. 스토어프런트 도메인 목록 안이어야 한다 */
92
+ redirectUri: z.url(),
93
+ /** base64url(SHA-256(codeVerifier)) */
94
+ codeChallenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
95
+ /** 스토어프런트가 만든 값 — 돌아올 때 그대로 붙어 온다. 세션에 저장해 두고 비교한다 */
96
+ state: z.string().min(16).max(128),
97
+ });
98
+ export type IdpAuthorizeRequest = z.infer<typeof idpAuthorizeRequestSchema>;
99
+
100
+ export const idpAuthorizeResponseSchema = z.object({
101
+ /** 구매자를 보낼 공급자 로그인 주소 */
102
+ url: z.url(),
103
+ });
104
+
105
+ /** `POST /auth/idp/token` 본문 — 돌아온 1회용 code를 구매자 토큰으로 바꾼다 */
106
+ export const idpTokenRequestSchema = z.object({
107
+ code: z.string().min(1),
108
+ codeVerifier: z.string().regex(/^[A-Za-z0-9._~-]{43,128}$/),
109
+ /** 비회원 장바구니 토큰 — 주면 회원 장바구니로 합친다 */
110
+ cartToken: z.string().optional(),
111
+ });
112
+ export type IdpTokenRequest = z.infer<typeof idpTokenRequestSchema>;
@@ -2,7 +2,8 @@ import { z } from "zod";
2
2
 
3
3
  export const memberSchema = z.object({
4
4
  memberId: z.string(),
5
- email: z.email(),
5
+ /** 소셜 가입에서 공급자가 이메일을 주지 않았으면 null */
6
+ email: z.email().nullable(),
6
7
  name: z.string(),
7
8
  phone: z.string().nullable(),
8
9
  point: z.number().int(),
@@ -14,6 +14,10 @@ export const storefrontStoreSchema = z.object({
14
14
  * 장바구니 담기는 된다 — 주문 버튼을 로그인 안내로 바꾸면 된다
15
15
  */
16
16
  guestCheckout: z.boolean(),
17
+ /**
18
+ * 켜진 로그인 방법 — 화면에 이 순서대로 버튼을 그리면 된다. `password`가 없으면 이메일·비밀번호 가입·로그인이 꺼져 있다
19
+ */
20
+ loginMethods: z.array(z.enum(["password", "kakao", "naver", "google"])),
17
21
  });
18
22
 
19
23
  export type StorefrontStore = z.infer<typeof storefrontStoreSchema>;