@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.
- package/dist/analytics/index.mjs +31 -8
- package/dist/{analytics-ids-o0a07Ohr.mjs → analytics-CgD70OqH.mjs} +1 -37
- package/dist/analytics-ids-COElZiI3.mjs +38 -0
- package/dist/auth/index.d.mts +43 -0
- package/dist/auth/index.mjs +58 -0
- package/dist/client-Xb-2tNKT.mjs +776 -0
- package/dist/http-DWaRhLfG.d.mts +55 -0
- package/dist/index.d.mts +7 -43
- package/dist/index.mjs +4 -762
- package/package.json +6 -1
- package/src/analytics/index.ts +32 -8
- package/src/auth/index.ts +74 -0
- package/src/client.ts +8 -1
- package/src/schemas/auth.ts +15 -0
package/dist/analytics/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { i as
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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 };
|