@sayren/storefront-sdk 0.2.0 → 0.3.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,5 @@
1
- import { i as ANALYTICS_SESSION_COOKIE, n as safeDecode, o as ANALYTICS_VISITOR_COOKIE } from "../analytics-ids-o0a07Ohr.mjs";
1
+ import { i as ANALYTICS_VISITOR_COOKIE, n as ANALYTICS_SESSION_COOKIE } from "../analytics-CgD70OqH.mjs";
2
+ import { n as safeDecode } from "../analytics-ids-COElZiI3.mjs";
2
3
  //#region src/analytics/index.ts
3
4
  const VISITOR_MAX_AGE_SEC = 31536e3;
4
5
  const SESSION_MAX_AGE_SEC = 1800;
@@ -7,6 +8,11 @@ const SESSION_CAMPAIGN_COOKIE = "sy_scmp";
7
8
  const MAX_BATCH = 50;
8
9
  /** 이벤트 이름당 분당 상한 — 무한 루프 버그가 트래픽을 만들지 못하게 */
9
10
  const PER_NAME_PER_MINUTE = 30;
11
+ /**
12
+ * 전체 분당 상한 — 서버의 방문자당 분당 한도(120)보다 낮게 둬, SDK가 보낸 이벤트가 서버 한도에 걸려
13
+ * 조용히 버려지지 않게 한다(이름당 상한만으로는 이름 5종 × 30 = 150까지 갈 수 있다).
14
+ */
15
+ const TOTAL_PER_MINUTE = 100;
10
16
  const MAX_QUEUE = 500;
11
17
  const DEV_HOST = /^(localhost|127\.\d+\.\d+\.\d+|\[?::1\]?|0\.0\.0\.0|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)$|\.local$/i;
12
18
  function createAnalytics(options) {
@@ -27,9 +33,15 @@ function createAnalyticsWith(options, env) {
27
33
  let visibleMs = 0;
28
34
  let engagementSent = false;
29
35
  let firstPage = true;
36
+ /**
37
+ * 지금 페이지뷰를 보냈는가 — 상한으로 페이지뷰가 버려졌으면 그 페이지의 상품 조회·목록·검색·체류도 보내지 않는다.
38
+ * 그러지 않으면 존재하지 않는 페이지뷰를 가리키는 이벤트가 남는다.
39
+ */
40
+ let pageAccepted = false;
30
41
  const seen = /* @__PURE__ */ new Set();
31
42
  let rateMinute = -1;
32
43
  const perName = /* @__PURE__ */ new Map();
44
+ let totalThisMinute = 0;
33
45
  const cleanups = [];
34
46
  const campaignOf = (href) => {
35
47
  try {
@@ -64,19 +76,23 @@ function createAnalyticsWith(options, env) {
64
76
  if (minute !== rateMinute) {
65
77
  rateMinute = minute;
66
78
  perName.clear();
79
+ totalThisMinute = 0;
67
80
  }
68
81
  const used = perName.get(name) ?? 0;
69
- if (used >= PER_NAME_PER_MINUTE) return false;
82
+ if (used >= PER_NAME_PER_MINUTE || totalThisMinute >= TOTAL_PER_MINUTE) return false;
70
83
  perName.set(name, used + 1);
84
+ totalThisMinute += 1;
71
85
  return true;
72
86
  };
87
+ /** 큐에 넣었으면 true — 중복·상한·거부로 버리면 false */
73
88
  const push = (event, dedupeKey) => {
74
- if (!enabled || consent === "denied") return;
75
- if (seen.has(dedupeKey) || !allowRate(event.name)) return;
89
+ if (!enabled || consent === "denied") return false;
90
+ if (seen.has(dedupeKey) || !allowRate(event.name)) return false;
76
91
  seen.add(dedupeKey);
77
92
  if (queue.length >= MAX_QUEUE) queue.shift();
78
93
  queue.push(event);
79
94
  if (queue.length >= MAX_BATCH) flush();
95
+ return true;
80
96
  };
81
97
  const flush = () => {
82
98
  if (!enabled || queue.length === 0) return;
@@ -96,7 +112,7 @@ function createAnalyticsWith(options, env) {
96
112
  const occurredAt = () => new Date(env.now()).toISOString();
97
113
  /** 지금 페이지의 체류를 한 번 보낸다(이동·숨김·닫힘 중 먼저 오는 때) */
98
114
  const endPage = () => {
99
- if (!pageViewId || engagementSent) return;
115
+ if (!pageViewId || engagementSent || !pageAccepted) return;
100
116
  if (visibleSince !== null) {
101
117
  visibleMs += env.now() - visibleSince;
102
118
  visibleSince = env.visible() ? env.now() : null;
@@ -126,14 +142,14 @@ function createAnalyticsWith(options, env) {
126
142
  visibleSince = env.visible() ? env.now() : null;
127
143
  engagementSent = false;
128
144
  const referrer = firstPage ? env.referrer() : "";
129
- firstPage = false;
130
- push({
145
+ pageAccepted = push({
131
146
  name: "page_view",
132
147
  occurredAt: occurredAt(),
133
148
  url: href,
134
149
  pageViewId,
135
150
  ...referrer ? { referrer } : {}
136
151
  }, `page:${pageViewId}`);
152
+ if (pageAccepted) firstPage = false;
137
153
  };
138
154
  /** 프리렌더·숨은 탭은 화면에 보이는 순간 첫 페이지뷰를 보낸다 */
139
155
  const startWhenVisible = () => {
@@ -164,6 +180,7 @@ function createAnalyticsWith(options, env) {
164
180
  return {
165
181
  track(input) {
166
182
  if (!pageViewId) startPage();
183
+ if (!pageAccepted) return;
167
184
  const common = {
168
185
  occurredAt: occurredAt(),
169
186
  url: env.href(),
@@ -198,7 +215,13 @@ function createAnalyticsWith(options, env) {
198
215
  setConsent(next) {
199
216
  consent = next;
200
217
  if (next === "denied") queue = [];
201
- if (next === "granted") flush();
218
+ if (next === "granted") {
219
+ if (pageViewId && !pageAccepted && options.autoPageViews !== false) {
220
+ pageKey = "";
221
+ startPage();
222
+ }
223
+ flush();
224
+ }
202
225
  },
203
226
  ids: () => enabled ? ensureIds() : null,
204
227
  flush,
@@ -87,40 +87,4 @@ webdriver: z.boolean().optional() }).optional()
87
87
  /** 배치 본문 상한(바이트) — 서버가 넘는 요청을 거부한다 */
88
88
  const ANALYTICS_MAX_BATCH_BYTES = 32768;
89
89
  //#endregion
90
- //#region src/analytics-ids.ts
91
- /**
92
- * 요청의 Cookie 헤더에서 방문자·세션 ID를 읽는다 — SSR 서버가 스토어프론트 API를 부를 때 쓴다.
93
- * 브라우저 모듈(`@sayren/storefront-sdk/analytics`)이 셀러 도메인에 남긴 쿠키다. 형식이 틀린 값은 버린다.
94
- *
95
- * ```ts
96
- * const ids = analyticsIdsFromCookie(request.headers.get("cookie"));
97
- * createStorefrontClient({ baseUrl, storeCode, ...ids });
98
- * ```
99
- */
100
- function analyticsIdsFromCookie(cookieHeader) {
101
- if (!cookieHeader) return {};
102
- const pick = (name) => {
103
- for (const part of cookieHeader.split(";")) {
104
- const [key, ...rest] = part.trim().split("=");
105
- if (key !== name) continue;
106
- const value = safeDecode(rest.join("="));
107
- return value !== null && analyticsIdSchema.safeParse(value).success ? value : void 0;
108
- }
109
- };
110
- const visitorId = pick(ANALYTICS_VISITOR_COOKIE);
111
- const sessionId = pick(ANALYTICS_SESSION_COOKIE);
112
- return {
113
- ...visitorId ? { visitorId } : {},
114
- ...sessionId ? { sessionId } : {}
115
- };
116
- }
117
- /** 잘못된 퍼센트 인코딩(`%E0%A4`)은 URIError를 던진다 — 쿠키 하나 때문에 SSR 전체가 500이 되지 않게 버린다 */
118
- function safeDecode(value) {
119
- try {
120
- return decodeURIComponent(value);
121
- } catch {
122
- return null;
123
- }
124
- }
125
- //#endregion
126
- export { ANALYTICS_SESSION_HEADER as a, analyticsBatchSchema as c, analyticsIdSchema as d, analyticsServerEventNameSchema as f, ANALYTICS_SESSION_COOKIE as i, analyticsClientEventNameSchema as l, safeDecode as n, ANALYTICS_VISITOR_COOKIE as o, ANALYTICS_MAX_BATCH_BYTES as r, ANALYTICS_VISITOR_HEADER as s, analyticsIdsFromCookie as t, analyticsClientEventSchema as u };
90
+ export { ANALYTICS_VISITOR_HEADER as a, analyticsClientEventSchema as c, ANALYTICS_VISITOR_COOKIE as i, analyticsIdSchema as l, ANALYTICS_SESSION_COOKIE as n, analyticsBatchSchema as o, ANALYTICS_SESSION_HEADER as r, analyticsClientEventNameSchema as s, ANALYTICS_MAX_BATCH_BYTES as t, analyticsServerEventNameSchema as u };
@@ -0,0 +1,38 @@
1
+ import { i as ANALYTICS_VISITOR_COOKIE, l as analyticsIdSchema, n as ANALYTICS_SESSION_COOKIE } from "./analytics-CgD70OqH.mjs";
2
+ //#region src/analytics-ids.ts
3
+ /**
4
+ * 요청의 Cookie 헤더에서 방문자·세션 ID를 읽는다 — SSR 서버가 스토어프론트 API를 부를 때 쓴다.
5
+ * 브라우저 모듈(`@sayren/storefront-sdk/analytics`)이 셀러 도메인에 남긴 쿠키다. 형식이 틀린 값은 버린다.
6
+ *
7
+ * ```ts
8
+ * const ids = analyticsIdsFromCookie(request.headers.get("cookie"));
9
+ * createStorefrontClient({ baseUrl, storeCode, ...ids });
10
+ * ```
11
+ */
12
+ function analyticsIdsFromCookie(cookieHeader) {
13
+ if (!cookieHeader) return {};
14
+ const pick = (name) => {
15
+ for (const part of cookieHeader.split(";")) {
16
+ const [key, ...rest] = part.trim().split("=");
17
+ if (key !== name) continue;
18
+ const value = safeDecode(rest.join("="));
19
+ return value !== null && analyticsIdSchema.safeParse(value).success ? value : void 0;
20
+ }
21
+ };
22
+ const visitorId = pick(ANALYTICS_VISITOR_COOKIE);
23
+ const sessionId = pick(ANALYTICS_SESSION_COOKIE);
24
+ return {
25
+ ...visitorId ? { visitorId } : {},
26
+ ...sessionId ? { sessionId } : {}
27
+ };
28
+ }
29
+ /** 잘못된 퍼센트 인코딩(`%E0%A4`)은 URIError를 던진다 — 쿠키 하나 때문에 SSR 전체가 500이 되지 않게 버린다 */
30
+ function safeDecode(value) {
31
+ try {
32
+ return decodeURIComponent(value);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ //#endregion
38
+ export { safeDecode as n, analyticsIdsFromCookie as t };
@@ -0,0 +1,43 @@
1
+ import { a as AnonymousSession, c as TokenPair, o as LoginRequest, s as SignupRequest, t as ApiError } from "../http-DWaRhLfG.mjs";
2
+ //#region src/auth/index.d.ts
3
+ export interface StorefrontAuthOptions {
4
+ /** 예: https://api.sayren.app/storefront/v1 */
5
+ baseUrl: string;
6
+ /** 테넌트 스토어 코드 — `X-Store-Code` 헤더로 보낸다 */
7
+ storeCode: string;
8
+ fetch?: typeof globalThis.fetch;
9
+ }
10
+ /**
11
+ * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
12
+ * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
13
+ *
14
+ * 토큰 보관은 앱 몫이다. 서버(loader·action)에서 호출하고 httpOnly 쿠키에 두기를 권한다.
15
+ *
16
+ * ```ts
17
+ * const auth = createStorefrontAuth({ baseUrl, storeCode });
18
+ * const guest = await auth.anonymous(); // 비회원: 장바구니 토큰
19
+ * const tokens = await auth.session({ email, password, cartToken: guest.cartToken });
20
+ * ```
21
+ */
22
+ export declare function createStorefrontAuth(options: StorefrontAuthOptions): {
23
+ /**
24
+ * 비회원 익명 세션 — 구매자 계정을 만들지 않는다. `cartToken`을 `X-Cart-Token`으로 보내면 장바구니·
25
+ * 비회원 주문이 쌓이고, `session()`·`signUp()`에 실으면 회원 장바구니로 병합된 뒤 닫힌다.
26
+ * 마지막 활동 후 30일이 지나면 만료된다.
27
+ */
28
+ anonymous(): Promise<AnonymousSession>;
29
+ /** 이메일·비밀번호 로그인. `cartToken`을 주면 비회원 장바구니를 병합한다 */
30
+ session(body: LoginRequest): Promise<TokenPair>;
31
+ /** 회원가입 — 가입과 동시에 로그인 토큰을 받는다. `cartToken`을 주면 장바구니를 병합한다 */
32
+ signUp(body: SignupRequest): Promise<TokenPair>;
33
+ /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
34
+ refresh(refreshToken: string): Promise<TokenPair>;
35
+ /**
36
+ * 로그아웃 — 이 구매자의 refreshToken을 모두 폐기한다. 앱은 보관한 토큰을 지운다.
37
+ * 액세스 토큰이 이미 만료됐으면(401) 서버에서 할 일이 없으므로 성공으로 본다.
38
+ */
39
+ signOut(accessToken: string): Promise<void>;
40
+ };
41
+ export type StorefrontAuth = ReturnType<typeof createStorefrontAuth>;
42
+ //#endregion
43
+ export { type AnonymousSession, ApiError, type LoginRequest, type SignupRequest, type TokenPair };
@@ -0,0 +1,58 @@
1
+ import { t as createStorefrontClient, tt as ApiError } from "../client-Xb-2tNKT.mjs";
2
+ //#region src/auth/index.ts
3
+ /**
4
+ * 구매자 인증 — 화면은 스토어프런트가 그리고, 계정 저장과 인증 처리는 sayren이 한다.
5
+ * 어느 방식으로 로그인해도 결과는 같은 구매자 토큰(`TokenPair`)이고 `/me/*`·장바구니·주문 API에 그대로 쓴다.
6
+ *
7
+ * 토큰 보관은 앱 몫이다. 서버(loader·action)에서 호출하고 httpOnly 쿠키에 두기를 권한다.
8
+ *
9
+ * ```ts
10
+ * const auth = createStorefrontAuth({ baseUrl, storeCode });
11
+ * const guest = await auth.anonymous(); // 비회원: 장바구니 토큰
12
+ * const tokens = await auth.session({ email, password, cartToken: guest.cartToken });
13
+ * ```
14
+ */
15
+ function createStorefrontAuth(options) {
16
+ const client = (accessToken) => createStorefrontClient({
17
+ baseUrl: options.baseUrl,
18
+ storeCode: options.storeCode,
19
+ accessToken,
20
+ fetch: options.fetch
21
+ });
22
+ return {
23
+ /**
24
+ * 비회원 익명 세션 — 구매자 계정을 만들지 않는다. `cartToken`을 `X-Cart-Token`으로 보내면 장바구니·
25
+ * 비회원 주문이 쌓이고, `session()`·`signUp()`에 실으면 회원 장바구니로 병합된 뒤 닫힌다.
26
+ * 마지막 활동 후 30일이 지나면 만료된다.
27
+ */
28
+ anonymous() {
29
+ return client().auth.anonymous();
30
+ },
31
+ /** 이메일·비밀번호 로그인. `cartToken`을 주면 비회원 장바구니를 병합한다 */
32
+ session(body) {
33
+ return client().auth.login(body);
34
+ },
35
+ /** 회원가입 — 가입과 동시에 로그인 토큰을 받는다. `cartToken`을 주면 장바구니를 병합한다 */
36
+ signUp(body) {
37
+ return client().auth.signup(body);
38
+ },
39
+ /** 토큰 갱신 — refreshToken은 한 번 쓰면 폐기되고 새 쌍이 온다(회전) */
40
+ refresh(refreshToken) {
41
+ return client().auth.refresh(refreshToken);
42
+ },
43
+ /**
44
+ * 로그아웃 — 이 구매자의 refreshToken을 모두 폐기한다. 앱은 보관한 토큰을 지운다.
45
+ * 액세스 토큰이 이미 만료됐으면(401) 서버에서 할 일이 없으므로 성공으로 본다.
46
+ */
47
+ async signOut(accessToken) {
48
+ try {
49
+ await client(accessToken).auth.logout();
50
+ } catch (error) {
51
+ if (error instanceof ApiError && error.status === 401) return;
52
+ throw error;
53
+ }
54
+ }
55
+ };
56
+ }
57
+ //#endregion
58
+ export { ApiError, createStorefrontAuth };