@sayren/store-sdk 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -150,7 +150,7 @@ function createHttp(options) {
150
150
  /**
151
151
  * 방문 분석 조회 (관리 API `/v1/analytics/*`, 스코프 `analytics:r`).
152
152
  *
153
- * 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 서버가 날짜(Asia/Seoul)별로 집계한 값이다. 집계는 주기적으로
153
+ * 스토어프론트가 `@sayren/storefront-sdk`로 수집한 이벤트를 서버가 날짜(Asia/Seoul)별로 집계한 값이다. 집계는 주기적으로
154
154
  * 갱신되므로 오늘 값은 `lastComputedAt` 시점까지 반영돼 있다. 봇·자동화 브라우저의 세션은 모든 집계에서 빠진다.
155
155
  */
156
156
  const reportDate$1 = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "날짜 형식은 YYYY-MM-DD 입니다");
@@ -204,7 +204,8 @@ const analyticsOverviewSchema = z.object({
204
204
  ...rangeMeta,
205
205
  summary: analyticsTotalsSchema.describe("기간 합계"),
206
206
  funnel: z.array(analyticsFunnelStepSchema).describe("세션 기준 구매 퍼널. 각 단계는 그 행동을 한 세션 수이고, 앞 단계를 거쳤는지는 따지지 않는다"),
207
- rows: z.array(analyticsTotalsSchema.extend({ date: z.string().describe("일자 (YYYY-MM-DD)") })).describe("날짜별 값. 세션이 없는 날은 생략된다")
207
+ rows: z.array(analyticsTotalsSchema.extend({ date: z.string().describe("일자 (YYYY-MM-DD)") })).describe("날짜별 값. 세션이 없는 날은 생략된다"),
208
+ excludedBotSessions: z.number().int().describe("집계에서 뺀 봇 세션 수 — 봇 표시 이벤트가 하나라도 있는 세션은 모든 값에서 빠진다")
208
209
  });
209
210
  const analyticsProductSortSchema = z.enum([
210
211
  "VIEWS",
@@ -227,7 +228,8 @@ const analyticsProductRowSchema = z.object({
227
228
  purchases: z.number().int().describe("이 상품이 포함된 구매(주문) 수"),
228
229
  quantity: z.number().int().describe("구매 수량"),
229
230
  revenue: z.number().int().describe("이 상품의 구매 금액 — 주문 상품 금액(할인 반영, 배송비 제외) 합계. 이후 취소·환불은 빼지 않는다"),
230
- purchaseRate: rate("구매율 = 구매 / 조회 세션. 조회 세션이 없으면 null")
231
+ purchaseRate: rate("구매율 = 구매 / 조회 세션. 조회 세션이 없으면 null"),
232
+ clickThroughRate: rate("목록 → 상세 전환율 = 상세 조회 / 목록 노출. 노출이 없으면 null(검색·외부 링크로 바로 들어온 조회는 노출 없이 잡힌다)")
231
233
  });
232
234
  const analyticsProductsSchema = z.object({
233
235
  ...rangeMeta,
@@ -275,6 +277,94 @@ const analyticsSourcesSchema = z.object({
275
277
  groupBy: analyticsSourceGroupBySchema,
276
278
  rows: z.array(analyticsSourceRowSchema).describe("세션 수 내림차순")
277
279
  });
280
+ const analyticsRealtimeSchema = z.object({
281
+ asOf: z.string().describe("기준 시각(ISO 8601)"),
282
+ windowMinutes: z.literal(5).describe("최근 몇 분 동안 활동한 방문을 세는가"),
283
+ visitors: z.number().int().describe("최근 5분 안에 이벤트가 있는 방문자 수(봇 제외)"),
284
+ sessions: z.number().int(),
285
+ pageViews: z.number().int(),
286
+ pages: z.array(z.object({
287
+ path: z.string(),
288
+ visitors: z.number().int(),
289
+ pageViews: z.number().int()
290
+ })).describe("최근 5분 페이지뷰가 있는 경로. 방문자 수 내림차순 최대 10개")
291
+ });
292
+ const analyticsDeviceRowSchema = z.object({
293
+ device: z.enum([
294
+ "desktop",
295
+ "mobile",
296
+ "tablet",
297
+ "unknown"
298
+ ]),
299
+ sessions: z.number().int(),
300
+ productViewSessions: z.number().int(),
301
+ addToCartSessions: z.number().int(),
302
+ beginCheckoutSessions: z.number().int(),
303
+ purchaseSessions: z.number().int(),
304
+ conversionRate: rate("전환율 = 구매 세션 / 세션")
305
+ });
306
+ const analyticsDevicesSchema = z.object({
307
+ ...rangeMeta,
308
+ rows: z.array(analyticsDeviceRowSchema).describe("기기별 세션 기준 퍼널. 세션 수 내림차순")
309
+ });
310
+ const limitParam = z.coerce.number().int().min(1).max(100).optional().describe("최대 행 수. 기본 20, 최대 100");
311
+ const analyticsPagesQuerySchema = analyticsRangeQuerySchema.extend({ limit: limitParam });
312
+ const analyticsPagesSchema = z.object({
313
+ ...rangeMeta,
314
+ landings: z.array(z.object({
315
+ path: z.string(),
316
+ entrances: z.number().int().describe("이 페이지로 시작한 세션 수"),
317
+ bounces: z.number().int().describe("이 페이지로 시작해 참여하지 않고 끝난 세션 수"),
318
+ bounceRate: rate("반송률 = 반송 / 진입")
319
+ })).describe("첫 페이지. 진입 수 내림차순"),
320
+ exits: z.array(z.object({
321
+ path: z.string(),
322
+ pageViews: z.number().int(),
323
+ exits: z.number().int().describe("이 페이지가 세션의 마지막 페이지뷰였던 수"),
324
+ exitRate: rate("이탈률 = 이탈 / 페이지뷰")
325
+ })).describe("이탈 페이지. 이탈 수 내림차순")
326
+ });
327
+ const analyticsSearchQuerySchema = analyticsRangeQuerySchema.extend({ limit: limitParam });
328
+ const analyticsSearchSchema = z.object({
329
+ ...rangeMeta,
330
+ summary: z.object({
331
+ searches: z.number().int(),
332
+ zeroResultSearches: z.number().int().describe("결과가 0개였던 검색 수"),
333
+ searchSessions: z.number().int().describe("검색한 세션 수(세션당 한 번)"),
334
+ purchaseSessions: z.number().int().describe("검색한 뒤 같은 날 구매한 세션 수(세션당 한 번). 검색 전에 끝난 구매는 세지 않는다"),
335
+ conversionRate: rate("검색 후 구매율 = 구매 세션 / 검색 세션")
336
+ }),
337
+ terms: z.array(z.object({
338
+ term: z.string().describe("검색어 — 소문자, 공백 정리"),
339
+ searches: z.number().int(),
340
+ zeroResultSearches: z.number().int(),
341
+ searchSessions: z.number().int().describe("이 검색어로 검색한 세션 수"),
342
+ purchaseSessions: z.number().int().describe("이 검색어로 검색한 뒤 같은 날 구매한 세션 수"),
343
+ conversionRate: rate("검색 후 구매율 = 구매 세션 / 검색 세션")
344
+ })).describe("검색 수 내림차순. 세션 수는 검색어마다 세므로 여러 검색어를 쓴 세션은 검색어마다 잡힌다(더하면 요약보다 크다)"),
345
+ zeroResultTerms: z.array(z.object({
346
+ term: z.string(),
347
+ searches: z.number().int(),
348
+ zeroResultSearches: z.number().int()
349
+ })).describe("결과가 0개였던 적이 있는 검색어. 결과 없음 수 내림차순 — 검색 수 상위(`terms`)에 들지 않는 드문 검색어도 여기 잡힌다")
350
+ });
351
+ const analyticsCheckoutOutcomeSchema = z.enum([
352
+ "COMPLETED",
353
+ "NOT_OPENED",
354
+ "USER_CANCELED",
355
+ "DECLINED",
356
+ "PREPARE_FAILURE",
357
+ "OTHER",
358
+ "IN_PROGRESS"
359
+ ]).describe("결제 세션의 최종 결과 — `COMPLETED`(결제 완료), `NOT_OPENED`(결제창을 열지 않고 끝남), `USER_CANCELED`(결제창에서 취소), `DECLINED`(카드 거절 등 승인 거절), `PREPARE_FAILURE`(결제창을 띄우기 전 장애), `OTHER`(그 밖의 이탈·만료), `IN_PROGRESS`(아직 진행 중 — 만료 전 대기·승인 중). 실패 분류는 세션의 마지막 결제 시도 기준이다");
360
+ const analyticsCheckoutSchema = z.object({
361
+ ...rangeMeta,
362
+ sessions: z.number().int().describe("기간 안에 시작한 결제 세션 수(결제 요청 단위)"),
363
+ outcomes: z.array(z.object({
364
+ outcome: analyticsCheckoutOutcomeSchema,
365
+ sessions: z.number().int()
366
+ })).describe("결과별 결제 세션 수. 결과마다 한 행")
367
+ });
278
368
  //#endregion
279
369
  //#region src/schemas/api-client.ts
280
370
  /**
@@ -654,12 +744,16 @@ const claimSchema = z.object({
654
744
  trackingNumber: z.string().nullable()
655
745
  }).nullable(),
656
746
  requestedAt: z.string(),
657
- completedAt: z.string().nullable()
747
+ completedAt: z.string().nullable(),
748
+ /** 보류 중이면 자동 해제 시각(ISO 8601), 아니면 null */
749
+ holdExpiresAt: z.string().nullable(),
750
+ /** 보류를 이미 썼는지 — 보류는 클레임당 1회라 해제·만료 뒤에도 true */
751
+ holdUsed: z.boolean()
658
752
  });
659
753
  /** `GET /v1/claims` 쿼리 — OpenAPI 문서용 형태(값은 쿼리 문자열) */
660
754
  const claimListQuerySchema = z.object({
661
755
  type: claimTypeSchema.optional().describe("클레임 유형 필터"),
662
- status: claimStatusSchema.optional().describe("클레임 상태 필터"),
756
+ status: z.string().optional().describe("클레임 상태 필터. 쉼표로 여러 개를 줄 수 있다 (예: `REQUESTED,HELD,COLLECTED`)"),
663
757
  orderItemId: z.string().optional().describe("주문상품 id 필터"),
664
758
  requestedFrom: z.string().optional().describe("요청 시각 조회 시작 (ISO 8601, 포함)"),
665
759
  requestedTo: z.string().optional().describe("요청 시각 조회 끝 (ISO 8601, 포함)"),
@@ -798,7 +892,7 @@ const withdrawCustomerRequestSchema = z.object({
798
892
  ]).describe("처리 사유 — 구매자 요청·장기 미이용·부정 이용·기타. 감사 로그에 남는다"),
799
893
  memo: z.string().max(200).optional().describe("사유 보충(선택)")
800
894
  });
801
- /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프런트 탈퇴와 같다) */
895
+ /** `WITHDRAWAL_BLOCKED`의 `error.details` — 탈퇴를 막는 항목 수(스토어프론트 탈퇴와 같다) */
802
896
  const customerWithdrawalBlockedDetailsSchema = z.object({
803
897
  inProgressOrderItems: z.number().int(),
804
898
  openClaims: z.number().int(),
@@ -816,7 +910,7 @@ const loginProviderSchema = z.enum([
816
910
  const loginProviderStatusSchema = z.object({
817
911
  provider: loginProviderSchema,
818
912
  connected: z.boolean(),
819
- /** 스토어프런트에서 사용 중인지. 연결 안 됐으면 false */
913
+ /** 스토어프론트에서 사용 중인지. 연결 안 됐으면 false */
820
914
  enabled: z.boolean(),
821
915
  clientIdPrefix: z.string().nullable().describe("클라이언트 ID(카카오 REST API 키) 앞 4자리"),
822
916
  secretPrefix: z.string().nullable(),
@@ -1600,10 +1694,10 @@ const productSummarySchema = z.object({
1600
1694
  productId: z.string(),
1601
1695
  name: z.string(),
1602
1696
  status: productStatusSchema,
1603
- salePrice: z.number().int(),
1604
- discountedPrice: z.number().int().nullable(),
1697
+ salePrice: z.number().int().describe("판매가. 원(KRW) 단위 정수"),
1698
+ discountedPrice: z.number().int().nullable().describe("즉시 할인 적용가. 원 단위. 할인이 없으면 null"),
1605
1699
  /** usable variant 재고 합계 (서버 계산) */
1606
- stockQuantity: z.number().int(),
1700
+ stockQuantity: z.number().int().describe("판매 가능한 옵션 재고의 합계. 서버가 계산한다"),
1607
1701
  categoryId: z.string().nullable(),
1608
1702
  thumbnailUrl: z.url().optional(),
1609
1703
  /** optionGroups.length > 0 */
@@ -1740,8 +1834,8 @@ const variantStockSchema = z.object({
1740
1834
  const productCreateRequestSchema = z.object({
1741
1835
  name: z.string().min(1).max(100),
1742
1836
  categoryId: z.string().min(1, "카테고리를 선택해주세요"),
1743
- salePrice: z.number().int().min(10).multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)"),
1744
- originalPrice: z.number().int().optional(),
1837
+ salePrice: z.number().int().min(10).multipleOf(10, "판매가는 10원 단위여야 합니다 (INVALID_PRICE_UNIT)").describe("판매가. 원(KRW) 단위 정수, 최소 10원, 10원 단위"),
1838
+ originalPrice: z.number().int().optional().describe("정가(할인 전 표시 가격). 원 단위"),
1745
1839
  description: z.string().optional(),
1746
1840
  /**
1747
1841
  * 상세설명 에디터(@avarlabs/editor)의 직렬화 상태. 재편집 때 이 값으로 문서를 복원한다.
@@ -1753,11 +1847,11 @@ const productCreateRequestSchema = z.object({
1753
1847
  attributeId: z.string(),
1754
1848
  value: z.string()
1755
1849
  })).optional(),
1756
- deliveryType: deliveryTypeSchema.default("PAID"),
1757
- deliveryFee: z.number().int().default(0),
1758
- conditionalFreeAmount: z.number().int().optional(),
1759
- returnFee: z.number().int().default(0),
1760
- exchangeFee: z.number().int().default(0),
1850
+ deliveryType: deliveryTypeSchema.default("PAID").describe("배송비 유형. FREE 무료, PAID 유료, CONDITIONAL_FREE 조건부 무료"),
1851
+ deliveryFee: z.number().int().default(0).describe("배송비. 원 단위"),
1852
+ conditionalFreeAmount: z.number().int().optional().describe("CONDITIONAL_FREE일 때 무료 배송이 되는 주문 금액. 원 단위"),
1853
+ returnFee: z.number().int().default(0).describe("반품 배송비. 원 단위"),
1854
+ exchangeFee: z.number().int().default(0).describe("교환 배송비. 원 단위"),
1761
1855
  shippingAddressId: z.string().optional(),
1762
1856
  returnAddressId: z.string().optional()
1763
1857
  }).extend(optionModelRequestSchema.shape);
@@ -1873,8 +1967,8 @@ function isWebOrigin(value) {
1873
1967
  return false;
1874
1968
  }
1875
1969
  }
1876
- /** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
1877
- const analyticsOriginSchema = z.string().trim().max(255).refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요").describe("스토어프런트 오리진. 예: `https://shop.example.com`");
1970
+ /** 방문 분석을 보낼 수 있는 스토어프론트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
1971
+ const analyticsOriginSchema = z.string().trim().max(255).refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요").describe("스토어프론트 오리진. 예: `https://shop.example.com`");
1878
1972
  /** 허용 오리진 상한 */
1879
1973
  const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
1880
1974
  /** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
@@ -1883,8 +1977,8 @@ const updateStoreRequestSchema = z.object({
1883
1977
  logoUrl: z.url().optional(),
1884
1978
  customerCenterPhone: z.string().optional(),
1885
1979
  businessHours: z.string().max(100).optional(),
1886
- analyticsAllowedOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("@deprecated `storefrontOrigins`를 쓴다(같은 목록). 방문 분석 수집을 받을 스토어프런트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. 넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다"),
1887
- storefrontOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("스토어프런트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. `analyticsAllowedOrigins`와 같은 목록이다(둘 다 주면 이 값이 이긴다)"),
1980
+ analyticsAllowedOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("@deprecated `storefrontOrigins`를 쓴다(같은 목록). 방문 분석 수집을 받을 스토어프론트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. 넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다"),
1981
+ storefrontOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("스토어프론트 도메인(최대 20개) — 소셜 로그인 뒤 돌아갈 주소와 방문 분석 수집을 함께 제한한다. `analyticsAllowedOrigins`와 같은 목록이다(둘 다 주면 이 값이 이긴다)"),
1888
1982
  guestCheckoutEnabled: z.boolean().optional().describe("비회원 주문 허용. false(회원만)면 로그인하지 않은 구매자의 주문서 생성·결제 요청이 403 `GUEST_CHECKOUT_DISABLED`다. 장바구니 담기와 이미 받은 비회원 주문의 조회·취소는 막지 않는다")
1889
1983
  }).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
1890
1984
  const storeSchema = z.object({
@@ -1900,8 +1994,8 @@ const storeSchema = z.object({
1900
1994
  logoUrl: z.url().nullable(),
1901
1995
  businessNumber: z.string().nullable(),
1902
1996
  customerCenterPhone: z.string().nullable(),
1903
- analyticsAllowedOrigins: z.array(z.string()).describe("방문 분석 수집을 받는 스토어프런트 오리진. 비어 있으면 모든 오리진에서 받는다"),
1904
- storefrontOrigins: z.array(z.string()).describe("스토어프런트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)"),
1997
+ analyticsAllowedOrigins: z.array(z.string()).describe("방문 분석 수집을 받는 스토어프론트 오리진. 비어 있으면 모든 오리진에서 받는다"),
1998
+ storefrontOrigins: z.array(z.string()).describe("스토어프론트 도메인 — 소셜 로그인 리디렉트와 방문 분석 수집이 함께 쓴다(`analyticsAllowedOrigins`와 같은 값)"),
1905
1999
  guestCheckoutEnabled: z.boolean().describe("비회원 주문 허용. false면 회원만 주문할 수 있다"),
1906
2000
  memberCount: z.number().int(),
1907
2001
  createdAt: z.string()
@@ -2188,7 +2282,10 @@ function createStoreClient(options) {
2188
2282
  },
2189
2283
  carriers: { list: () => storeHttp.request("GET", "/carriers", z.array(carrierSchema)) },
2190
2284
  claims: {
2191
- list: (params) => storeHttp.request("GET", "/claims", pageSchema(claimSchema), { query: params }),
2285
+ list: (params) => storeHttp.request("GET", "/claims", pageSchema(claimSchema), { query: {
2286
+ ...params,
2287
+ status: Array.isArray(params?.status) ? params.status.join(",") : params?.status
2288
+ } }),
2192
2289
  get: (claimId) => storeHttp.request("GET", path`/claims/${claimId}`, claimDetailSchema),
2193
2290
  approve: (claimId, deductAmount, idempotencyKey) => storeHttp.request("POST", path`/claims/${claimId}/approve`, approveClaimResultSchema, {
2194
2291
  body: deductAmount !== void 0 ? { deductAmount } : {},
@@ -2202,7 +2299,9 @@ function createStoreClient(options) {
2202
2299
  trackingNumber
2203
2300
  } }),
2204
2301
  hold: (claimId, reason) => storeHttp.request("POST", path`/claims/${claimId}/hold`, claimHoldResultSchema, { body: { reason } }),
2205
- releaseHold: (claimId) => storeHttp.requestVoid("DELETE", path`/claims/${claimId}/hold`)
2302
+ releaseHold: (claimId) => storeHttp.requestVoid("DELETE", path`/claims/${claimId}/hold`),
2303
+ /** 교환 완료 — 재배송 중인 교환을 셀러가 명시적으로 끝낸다 */
2304
+ completeExchange: (claimId) => storeHttp.request("POST", path`/claims/${claimId}/complete-exchange`, claimSchema)
2206
2305
  },
2207
2306
  inquiries: {
2208
2307
  list: (params) => storeHttp.request("GET", "/inquiries", pageSchema(inquirySchema), { query: params }),
@@ -2258,7 +2357,7 @@ function createStoreClient(options) {
2258
2357
  */
2259
2358
  get: (params) => storeHttp.request("GET", "/sales-report", salesReportSchema, { query: salesReportQuerySchema.parse(params) }) },
2260
2359
  /**
2261
- * 방문 분석 — 스코프 `analytics:r`. 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
2360
+ * 방문 분석 — 스코프 `analytics:r`. 스토어프론트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
2262
2361
  * 집계한 값이다. 집계는 주기적으로 갱신되고 응답 `lastComputedAt`이 마지막 집계 시각이다. 봇 세션은 빠진다.
2263
2362
  * 기간은 양끝 포함 최대 92일이고, 테스트 결제 구매는 `includeTest: "true"`일 때만 포함한다.
2264
2363
  */
@@ -2268,7 +2367,17 @@ get: (params) => storeHttp.request("GET", "/sales-report", salesReportSchema, {
2268
2367
  /** `GET /v1/analytics/products` — 상품별 노출·조회·장바구니·구매 */
2269
2368
  products: (params) => storeHttp.request("GET", "/analytics/products", analyticsProductsSchema, { query: analyticsProductsQuerySchema.parse(params) }),
2270
2369
  /** `GET /v1/analytics/sources` — 채널·소스·캠페인·기기별 세션과 구매 */
2271
- sources: (params) => storeHttp.request("GET", "/analytics/sources", analyticsSourcesSchema, { query: analyticsSourcesQuerySchema.parse(params) })
2370
+ sources: (params) => storeHttp.request("GET", "/analytics/sources", analyticsSourcesSchema, { query: analyticsSourcesQuerySchema.parse(params) }),
2371
+ /** `GET /v1/analytics/realtime` — 최근 5분 방문자·세션·페이지(원본 이벤트 기준, 봇 제외) */
2372
+ realtime: () => storeHttp.request("GET", "/analytics/realtime", analyticsRealtimeSchema),
2373
+ /** `GET /v1/analytics/devices` — 기기별 세션 퍼널 */
2374
+ devices: (params) => storeHttp.request("GET", "/analytics/devices", analyticsDevicesSchema, { query: analyticsRangeQuerySchema.parse(params) }),
2375
+ /** `GET /v1/analytics/pages` — 첫 페이지(진입·반송)와 이탈 페이지 */
2376
+ pages: (params) => storeHttp.request("GET", "/analytics/pages", analyticsPagesSchema, { query: analyticsPagesQuerySchema.parse(params) }),
2377
+ /** `GET /v1/analytics/search` — 검색어별 검색·결과 0개·검색 후 구매 */
2378
+ search: (params) => storeHttp.request("GET", "/analytics/search", analyticsSearchSchema, { query: analyticsSearchQuerySchema.parse(params) }),
2379
+ /** `GET /v1/analytics/checkout` — 결제 세션의 결과별 수(결제 단계 이탈 이유) */
2380
+ checkout: (params) => storeHttp.request("GET", "/analytics/checkout", analyticsCheckoutSchema, { query: analyticsRangeQuerySchema.parse(params) })
2272
2381
  },
2273
2382
  /**
2274
2383
  * 결제 내역 — 스코프 `settlement:r`. 결제 건(세션) 단위이고 주문 목록과 다르다. 주문이 만들어지지 않은
@@ -2414,7 +2523,7 @@ get: (params) => storeHttp.request("GET", "/sales-report", salesReportSchema, {
2414
2523
  get: () => storeHttp.request("GET", "/store", storeSchema),
2415
2524
  /**
2416
2525
  * `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
2417
- * 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
2526
+ * 스토어프론트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
2418
2527
  */
2419
2528
  update: (body) => storeHttp.request("PATCH", "/store", storeSchema, { body: updateStoreRequestSchema.parse(body) }),
2420
2529
  listMembers: () => storeHttp.request("GET", "/store/members", z.array(storeMemberSchema)),
@@ -2803,4 +2912,4 @@ function createStoreSession(options) {
2803
2912
  };
2804
2913
  }
2805
2914
  //#endregion
2806
- export { ApiError, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_OPTION_GROUPS, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, RESERVED_STORE_CODES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, effectivePaymentRoutingModeSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentRoutingModeSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageUploadResultSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, refreshUserToken, rejectClaimRequestSchema, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema };
2915
+ export { ApiError, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_OPTION_GROUPS, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, RESERVED_STORE_CODES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, effectivePaymentRoutingModeSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentRoutingModeSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageUploadResultSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, refreshUserToken, rejectClaimRequestSchema, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema };
package/dist/server.d.mts CHANGED
@@ -1,4 +1,48 @@
1
1
  import { a as WEBHOOK_HEADERS, i as WEBHOOK_EVENT_TYPE_HEADER, n as WEBHOOK_DELIVERY_ID_HEADER, o as WEBHOOK_SIGNATURE_HEADER, r as WEBHOOK_EVENT_ID_HEADER, s as WEBHOOK_USER_AGENT, t as WEBHOOK_DEFAULT_TOLERANCE_SEC } from "./webhook-headers-D6QmPdEJ.mjs";
2
+ //#region src/member-delegation.d.ts
3
+ /**
4
+ * 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다
5
+ * (token-exchange의 `member-uid`·`member-ref` 확장, auth-spec §12-10). 클라이언트 시크릿을 쓰므로 **서버에서만** 부른다.
6
+ *
7
+ * 스토어에 바인딩된 confidential 클라이언트 + `member:delegate` 스코프가 필요하다. 응답은 구매자 액세스 토큰(30분)이고
8
+ * refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
9
+ */
10
+ interface DelegatedMemberToken {
11
+ accessToken: string;
12
+ expiresIn: number;
13
+ memberId: string;
14
+ }
15
+ interface DelegationOptions {
16
+ /** api 주소(예: `https://api.sayren.app`) — `/v1/oauth2/token`을 붙인다 */
17
+ baseUrl: string;
18
+ clientId: string;
19
+ clientSecret: string;
20
+ fetch?: typeof globalThis.fetch;
21
+ }
22
+ /** 셀러 자체 인증의 사용자 id로 지목한다. 처음 보는 id면 구매자를 만든다(이메일·이름은 그때만 쓴다) */
23
+ export declare function delegateMemberByUid(options: DelegationOptions & {
24
+ uid: string;
25
+ email?: string;
26
+ name?: string;
27
+ /**
28
+ * 사용자가 내 서비스에서 실제로 로그인한 시각. 주면 구매자 토큰의 인증 시각이 되고(탈퇴처럼 최근 인증이 필요한 동작에
29
+ * 쓴다), 주지 않으면 인증 시각이 없는 토큰이 나온다
30
+ */
31
+ authenticatedAt?: Date;
32
+ }): Promise<DelegatedMemberToken>;
33
+ /** 이메일로 지목한다. 처음 보는 이메일이면 구매자를 만든다 */
34
+ export declare function delegateMemberByEmail(options: DelegationOptions & {
35
+ email: string;
36
+ name?: string;
37
+ }): Promise<DelegatedMemberToken>;
38
+ /** 위임 실패 — `error`는 OAuth 오류 코드(`invalid_client`·`invalid_scope`·`invalid_request` 등) */
39
+ export declare class MemberDelegationError extends Error {
40
+ readonly error: string;
41
+ readonly description: string;
42
+ readonly status: number;
43
+ constructor(error: string, description: string, status: number);
44
+ }
45
+ //#endregion
2
46
  //#region src/webhook-signature.d.ts
3
47
  interface SignWebhookPayloadOptions {
4
48
  /** 요청 본문 원문. 전송하는 바이트와 정확히 같아야 한다 */
@@ -56,4 +100,4 @@ interface VerifyWebhookSignatureOptions {
56
100
  */
57
101
  export declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): Promise<WebhookSignatureVerification>;
58
102
  //#endregion
59
- export { type ParsedWebhookSignature, type SignWebhookPayloadOptions, type VerifyWebhookSignatureOptions, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, type WebhookSignatureFailureReason, type WebhookSignatureVerification };
103
+ export { type DelegatedMemberToken, type ParsedWebhookSignature, type SignWebhookPayloadOptions, type VerifyWebhookSignatureOptions, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, type WebhookSignatureFailureReason, type WebhookSignatureVerification };
package/dist/server.mjs CHANGED
@@ -1,4 +1,59 @@
1
1
  import { a as WEBHOOK_HEADERS, i as WEBHOOK_EVENT_TYPE_HEADER, n as WEBHOOK_DELIVERY_ID_HEADER, o as WEBHOOK_SIGNATURE_HEADER, r as WEBHOOK_EVENT_ID_HEADER, s as WEBHOOK_USER_AGENT, t as WEBHOOK_DEFAULT_TOLERANCE_SEC } from "./webhook-headers-ap_exsb9.mjs";
2
+ //#region src/member-delegation.ts
3
+ /** 셀러 자체 인증의 사용자 id로 지목한다. 처음 보는 id면 구매자를 만든다(이메일·이름은 그때만 쓴다) */
4
+ function delegateMemberByUid(options) {
5
+ return delegate(options, {
6
+ subject_token_type: "urn:sayren:params:oauth:token-type:member-uid",
7
+ subject_token: options.uid,
8
+ ...options.authenticatedAt ? { subject_auth_time: String(Math.floor(options.authenticatedAt.getTime() / 1e3)) } : {},
9
+ ...options.email ? { subject_email: options.email } : {},
10
+ ...options.name ? { subject_name: options.name } : {}
11
+ });
12
+ }
13
+ /** 이메일로 지목한다. 처음 보는 이메일이면 구매자를 만든다 */
14
+ function delegateMemberByEmail(options) {
15
+ return delegate(options, {
16
+ subject_token_type: "urn:sayren:params:oauth:token-type:member-ref",
17
+ subject_token: options.email,
18
+ ...options.name ? { subject_name: options.name } : {}
19
+ });
20
+ }
21
+ async function delegate(options, subject) {
22
+ const doFetch = options.fetch ?? globalThis.fetch;
23
+ const basic = btoa(`${options.clientId}:${options.clientSecret}`);
24
+ const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/v1/oauth2/token`, {
25
+ method: "POST",
26
+ headers: {
27
+ "content-type": "application/x-www-form-urlencoded",
28
+ authorization: `Basic ${basic}`
29
+ },
30
+ body: new URLSearchParams({
31
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
32
+ ...subject
33
+ }).toString()
34
+ });
35
+ const body = await res.json().catch(() => ({}));
36
+ if (!res.ok || typeof body.access_token !== "string") throw new MemberDelegationError(typeof body.error === "string" ? body.error : `http_${res.status}`, typeof body.error_description === "string" ? body.error_description : "", res.status);
37
+ return {
38
+ accessToken: body.access_token,
39
+ expiresIn: Number(body.expires_in),
40
+ memberId: String(body.memberId)
41
+ };
42
+ }
43
+ /** 위임 실패 — `error`는 OAuth 오류 코드(`invalid_client`·`invalid_scope`·`invalid_request` 등) */
44
+ var MemberDelegationError = class extends Error {
45
+ error;
46
+ description;
47
+ status;
48
+ constructor(error, description, status) {
49
+ super(description ? `${error}: ${description}` : error);
50
+ this.error = error;
51
+ this.description = description;
52
+ this.status = status;
53
+ this.name = "MemberDelegationError";
54
+ }
55
+ };
56
+ //#endregion
2
57
  //#region src/webhook-signature.ts
3
58
  /**
4
59
  * 웹훅 서명 (`Sayren-Signature`) 생성·파싱·검증.
@@ -122,4 +177,4 @@ async function verifyWebhookSignature(options) {
122
177
  };
123
178
  }
124
179
  //#endregion
125
- export { WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, parseWebhookSignatureHeader, signWebhookPayload, verifyWebhookSignature };
180
+ export { MemberDelegationError, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, delegateMemberByEmail, delegateMemberByUid, parseWebhookSignatureHeader, signWebhookPayload, verifyWebhookSignature };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/client.ts CHANGED
@@ -2,13 +2,22 @@ import { z } from "zod";
2
2
  import { base64Utf8 } from "./base64";
3
3
  import { createHttp, path, type Query, resolveToken, type TokenSource, unwrapData } from "./http";
4
4
  import {
5
+ type AnalyticsPagesParams,
5
6
  type AnalyticsProductsParams,
6
7
  type AnalyticsRangeParams,
8
+ type AnalyticsSearchParams,
7
9
  type AnalyticsSourcesParams,
10
+ analyticsCheckoutSchema,
11
+ analyticsDevicesSchema,
8
12
  analyticsOverviewSchema,
13
+ analyticsPagesQuerySchema,
14
+ analyticsPagesSchema,
9
15
  analyticsProductsQuerySchema,
10
16
  analyticsProductsSchema,
11
17
  analyticsRangeQuerySchema,
18
+ analyticsRealtimeSchema,
19
+ analyticsSearchQuerySchema,
20
+ analyticsSearchSchema,
12
21
  analyticsSourcesQuerySchema,
13
22
  analyticsSourcesSchema,
14
23
  } from "./schemas/analytics";
@@ -533,7 +542,12 @@ export function createStoreClient(options: StoreClientOptions) {
533
542
 
534
543
  claims: {
535
544
  list: (params?: ClaimListParams) =>
536
- storeHttp.request("GET", "/claims", pageSchema(claimSchema), { query: params as Query }),
545
+ storeHttp.request("GET", "/claims", pageSchema(claimSchema), {
546
+ query: {
547
+ ...params,
548
+ status: Array.isArray(params?.status) ? params.status.join(",") : params?.status,
549
+ } as Query,
550
+ }),
537
551
  get: (claimId: string) =>
538
552
  storeHttp.request("GET", path`/claims/${claimId}`, claimDetailSchema),
539
553
  approve: (claimId: string, deductAmount?: number, idempotencyKey?: string) =>
@@ -561,6 +575,9 @@ export function createStoreClient(options: StoreClientOptions) {
561
575
  }),
562
576
  releaseHold: (claimId: string) =>
563
577
  storeHttp.requestVoid("DELETE", path`/claims/${claimId}/hold`),
578
+ /** 교환 완료 — 재배송 중인 교환을 셀러가 명시적으로 끝낸다 */
579
+ completeExchange: (claimId: string) =>
580
+ storeHttp.request("POST", path`/claims/${claimId}/complete-exchange`, claimSchema),
564
581
  },
565
582
 
566
583
  inquiries: {
@@ -691,7 +708,7 @@ export function createStoreClient(options: StoreClientOptions) {
691
708
  },
692
709
 
693
710
  /**
694
- * 방문 분석 — 스코프 `analytics:r`. 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
711
+ * 방문 분석 — 스코프 `analytics:r`. 스토어프론트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
695
712
  * 집계한 값이다. 집계는 주기적으로 갱신되고 응답 `lastComputedAt`이 마지막 집계 시각이다. 봇 세션은 빠진다.
696
713
  * 기간은 양끝 포함 최대 92일이고, 테스트 결제 구매는 `includeTest: "true"`일 때만 포함한다.
697
714
  */
@@ -711,6 +728,28 @@ export function createStoreClient(options: StoreClientOptions) {
711
728
  storeHttp.request("GET", "/analytics/sources", analyticsSourcesSchema, {
712
729
  query: analyticsSourcesQuerySchema.parse(params),
713
730
  }),
731
+ /** `GET /v1/analytics/realtime` — 최근 5분 방문자·세션·페이지(원본 이벤트 기준, 봇 제외) */
732
+ realtime: () => storeHttp.request("GET", "/analytics/realtime", analyticsRealtimeSchema),
733
+ /** `GET /v1/analytics/devices` — 기기별 세션 퍼널 */
734
+ devices: (params: AnalyticsRangeParams) =>
735
+ storeHttp.request("GET", "/analytics/devices", analyticsDevicesSchema, {
736
+ query: analyticsRangeQuerySchema.parse(params),
737
+ }),
738
+ /** `GET /v1/analytics/pages` — 첫 페이지(진입·반송)와 이탈 페이지 */
739
+ pages: (params: AnalyticsPagesParams) =>
740
+ storeHttp.request("GET", "/analytics/pages", analyticsPagesSchema, {
741
+ query: analyticsPagesQuerySchema.parse(params) as unknown as Query,
742
+ }),
743
+ /** `GET /v1/analytics/search` — 검색어별 검색·결과 0개·검색 후 구매 */
744
+ search: (params: AnalyticsSearchParams) =>
745
+ storeHttp.request("GET", "/analytics/search", analyticsSearchSchema, {
746
+ query: analyticsSearchQuerySchema.parse(params) as unknown as Query,
747
+ }),
748
+ /** `GET /v1/analytics/checkout` — 결제 세션의 결과별 수(결제 단계 이탈 이유) */
749
+ checkout: (params: AnalyticsRangeParams) =>
750
+ storeHttp.request("GET", "/analytics/checkout", analyticsCheckoutSchema, {
751
+ query: analyticsRangeQuerySchema.parse(params),
752
+ }),
714
753
  },
715
754
 
716
755
  /**
@@ -944,7 +983,7 @@ export function createStoreClient(options: StoreClientOptions) {
944
983
  get: () => storeHttp.request("GET", "/store", storeSchema),
945
984
  /**
946
985
  * `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
947
- * 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
986
+ * 스토어프론트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
948
987
  */
949
988
  update: (body: UpdateStoreRequest) =>
950
989
  storeHttp.request("PATCH", "/store", storeSchema, {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다
3
+ * (token-exchange의 `member-uid`·`member-ref` 확장, auth-spec §12-10). 클라이언트 시크릿을 쓰므로 **서버에서만** 부른다.
4
+ *
5
+ * 스토어에 바인딩된 confidential 클라이언트 + `member:delegate` 스코프가 필요하다. 응답은 구매자 액세스 토큰(30분)이고
6
+ * refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
7
+ */
8
+
9
+ export interface DelegatedMemberToken {
10
+ accessToken: string;
11
+ expiresIn: number;
12
+ memberId: string;
13
+ }
14
+
15
+ interface DelegationOptions {
16
+ /** api 주소(예: `https://api.sayren.app`) — `/v1/oauth2/token`을 붙인다 */
17
+ baseUrl: string;
18
+ clientId: string;
19
+ clientSecret: string;
20
+ fetch?: typeof globalThis.fetch;
21
+ }
22
+
23
+ /** 셀러 자체 인증의 사용자 id로 지목한다. 처음 보는 id면 구매자를 만든다(이메일·이름은 그때만 쓴다) */
24
+ export function delegateMemberByUid(
25
+ options: DelegationOptions & {
26
+ uid: string;
27
+ email?: string;
28
+ name?: string;
29
+ /**
30
+ * 사용자가 내 서비스에서 실제로 로그인한 시각. 주면 구매자 토큰의 인증 시각이 되고(탈퇴처럼 최근 인증이 필요한 동작에
31
+ * 쓴다), 주지 않으면 인증 시각이 없는 토큰이 나온다
32
+ */
33
+ authenticatedAt?: Date;
34
+ },
35
+ ): Promise<DelegatedMemberToken> {
36
+ return delegate(options, {
37
+ subject_token_type: "urn:sayren:params:oauth:token-type:member-uid",
38
+ subject_token: options.uid,
39
+ ...(options.authenticatedAt
40
+ ? { subject_auth_time: String(Math.floor(options.authenticatedAt.getTime() / 1000)) }
41
+ : {}),
42
+ ...(options.email ? { subject_email: options.email } : {}),
43
+ ...(options.name ? { subject_name: options.name } : {}),
44
+ });
45
+ }
46
+
47
+ /** 이메일로 지목한다. 처음 보는 이메일이면 구매자를 만든다 */
48
+ export function delegateMemberByEmail(
49
+ options: DelegationOptions & { email: string; name?: string },
50
+ ): Promise<DelegatedMemberToken> {
51
+ return delegate(options, {
52
+ subject_token_type: "urn:sayren:params:oauth:token-type:member-ref",
53
+ subject_token: options.email,
54
+ ...(options.name ? { subject_name: options.name } : {}),
55
+ });
56
+ }
57
+
58
+ async function delegate(
59
+ options: DelegationOptions,
60
+ subject: Record<string, string>,
61
+ ): Promise<DelegatedMemberToken> {
62
+ const doFetch = options.fetch ?? globalThis.fetch;
63
+ const basic = btoa(`${options.clientId}:${options.clientSecret}`);
64
+ const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/v1/oauth2/token`, {
65
+ method: "POST",
66
+ headers: {
67
+ "content-type": "application/x-www-form-urlencoded",
68
+ authorization: `Basic ${basic}`,
69
+ },
70
+ body: new URLSearchParams({
71
+ grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
72
+ ...subject,
73
+ }).toString(),
74
+ });
75
+ const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
76
+ if (!res.ok || typeof body.access_token !== "string") {
77
+ const error = typeof body.error === "string" ? body.error : `http_${res.status}`;
78
+ const description = typeof body.error_description === "string" ? body.error_description : "";
79
+ throw new MemberDelegationError(error, description, res.status);
80
+ }
81
+ return {
82
+ accessToken: body.access_token,
83
+ expiresIn: Number(body.expires_in),
84
+ memberId: String(body.memberId),
85
+ };
86
+ }
87
+
88
+ /** 위임 실패 — `error`는 OAuth 오류 코드(`invalid_client`·`invalid_scope`·`invalid_request` 등) */
89
+ export class MemberDelegationError extends Error {
90
+ constructor(
91
+ readonly error: string,
92
+ readonly description: string,
93
+ readonly status: number,
94
+ ) {
95
+ super(description ? `${error}: ${description}` : error);
96
+ this.name = "MemberDelegationError";
97
+ }
98
+ }