@sayren/store-sdk 0.23.0 → 0.28.1
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.d.mts +636 -482
- package/dist/index.mjs +370 -347
- package/dist/server.d.mts +19 -28
- package/dist/server.mjs +29 -42
- package/package.json +1 -1
- package/src/client.ts +55 -153
- package/src/http.ts +1 -1
- package/src/index.ts +3 -2
- package/src/member-delegation.ts +51 -77
- package/src/oauth.ts +28 -33
- package/src/schemas/api-log.ts +7 -5
- package/src/schemas/audit.ts +5 -5
- package/src/schemas/auth.ts +0 -64
- package/src/schemas/claim.ts +4 -4
- package/src/schemas/coupon.ts +2 -2
- package/src/schemas/customer-delegation.ts +48 -0
- package/src/schemas/customer.ts +3 -3
- package/src/schemas/fulfillment.ts +9 -11
- package/src/schemas/order.ts +1 -1
- package/src/schemas/payment.ts +7 -5
- package/src/schemas/product.ts +12 -5
- package/src/schemas/promotion.ts +4 -4
- package/src/schemas/refund-policy.ts +12 -12
- package/src/schemas/saved-option-set.ts +1 -1
- package/src/schemas/scope.ts +3 -4
- package/src/schemas/shipping-policy.ts +18 -18
- package/src/schemas/store.ts +9 -9
- package/src/schemas/storefront-hosting.ts +62 -38
- package/src/schemas/subscription.ts +222 -0
- package/src/schemas/telemetry.ts +2 -2
- package/src/schemas/upload.ts +1 -1
- package/src/schemas/webhook-event.ts +1 -1
- package/src/server.ts +1 -2
- package/src/session.ts +46 -29
- package/src/schemas/api-client.ts +0 -121
package/src/member-delegation.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로
|
|
3
|
-
* (
|
|
2
|
+
* 구매자 위임 — 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰으로 바꾼다.
|
|
3
|
+
* 계정 토큰(개인 액세스 토큰 등)을 쓰므로 **서버에서만** 부른다.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
|
|
5
|
+
* 응답은 구매자 액세스 토큰(30분)이고 refresh는 없다 — 만료되면 같은 호출로 다시 받는다.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
export interface DelegatedMemberToken {
|
|
@@ -12,87 +11,62 @@ export interface DelegatedMemberToken {
|
|
|
12
11
|
memberId: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
});
|
|
14
|
+
/** 위임 실패 — `error`는 api 오류 코드(엔벨로프 `error.code`, 없으면 `http_{status}`) */
|
|
15
|
+
export class MemberDelegationError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
readonly error: string,
|
|
18
|
+
readonly description: string,
|
|
19
|
+
readonly status: number,
|
|
20
|
+
) {
|
|
21
|
+
super(description ? `${error}: ${description}` : error);
|
|
22
|
+
this.name = "MemberDelegationError";
|
|
23
|
+
}
|
|
56
24
|
}
|
|
57
25
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
26
|
+
/**
|
|
27
|
+
* 계정 토큰으로 구매자 위임(`POST /v1/customers/delegations`) — 계정 토큰(개인 액세스 토큰 등)과 상점 id를 쓴다.
|
|
28
|
+
* `customer:rw`가 필요하다. `uid`(셀러 자체 인증의 사용자 id) 또는 `email`로 지목하고, 처음 보는 사용자면 구매자를 만든다.
|
|
29
|
+
*/
|
|
30
|
+
export async function delegateMember(options: {
|
|
31
|
+
/** api 주소(예: `https://api.sayren.app`) */
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
accessToken: string;
|
|
34
|
+
storeId: string;
|
|
35
|
+
uid?: string;
|
|
36
|
+
email?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
authenticatedAt?: Date;
|
|
39
|
+
fetch?: typeof globalThis.fetch;
|
|
40
|
+
}): Promise<DelegatedMemberToken> {
|
|
62
41
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
63
|
-
const
|
|
64
|
-
const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/v1/oauth2/token`, {
|
|
42
|
+
const res = await doFetch(`${options.baseUrl.replace(/\/+$/, "")}/v1/customers/delegations`, {
|
|
65
43
|
method: "POST",
|
|
66
44
|
headers: {
|
|
67
|
-
"content-type": "application/
|
|
68
|
-
authorization: `
|
|
45
|
+
"content-type": "application/json",
|
|
46
|
+
authorization: `Bearer ${options.accessToken}`,
|
|
47
|
+
"x-store-id": options.storeId,
|
|
69
48
|
},
|
|
70
|
-
body:
|
|
71
|
-
|
|
72
|
-
...
|
|
73
|
-
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
...(options.uid ? { uid: options.uid } : {}),
|
|
51
|
+
...(options.email ? { email: options.email } : {}),
|
|
52
|
+
...(options.name ? { name: options.name } : {}),
|
|
53
|
+
...(options.authenticatedAt
|
|
54
|
+
? { authenticatedAt: options.authenticatedAt.toISOString() }
|
|
55
|
+
: {}),
|
|
56
|
+
}),
|
|
74
57
|
});
|
|
75
|
-
const body = (await res.json().catch(() => ({}))) as
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
58
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
59
|
+
data?: { accessToken?: unknown; expiresIn?: unknown; memberId?: unknown };
|
|
60
|
+
error?: { code?: unknown; message?: unknown };
|
|
61
|
+
};
|
|
62
|
+
if (!res.ok || typeof body.data?.accessToken !== "string") {
|
|
63
|
+
const code = typeof body.error?.code === "string" ? body.error.code : `http_${res.status}`;
|
|
64
|
+
const message = typeof body.error?.message === "string" ? body.error.message : "";
|
|
65
|
+
throw new MemberDelegationError(code, message, res.status);
|
|
80
66
|
}
|
|
81
67
|
return {
|
|
82
|
-
accessToken: body.
|
|
83
|
-
expiresIn: Number(body.
|
|
84
|
-
memberId: String(body.memberId),
|
|
68
|
+
accessToken: body.data.accessToken,
|
|
69
|
+
expiresIn: Number(body.data.expiresIn),
|
|
70
|
+
memberId: String(body.data.memberId),
|
|
85
71
|
};
|
|
86
72
|
}
|
|
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
|
-
}
|
package/src/oauth.ts
CHANGED
|
@@ -4,10 +4,11 @@ import { unwrapData } from "./http";
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* OAuth 2.1 브라우저 플로우 헬퍼 (authorization_code + PKCE).
|
|
7
|
+
* 발급자는 Sayren 계정(accounts)이다 — 엔드포인트 주소는 호출부가 준다.
|
|
7
8
|
* Web Crypto만 사용 — 브라우저/Node 모두에서 동작하며 **시크릿을 다루지 않는다**.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
/** RFC 6749 §5.1 토큰 응답
|
|
11
|
+
/** RFC 6749 §5.1 토큰 응답 */
|
|
11
12
|
export const oauthTokenResponseSchema = z
|
|
12
13
|
.object({
|
|
13
14
|
access_token: z.string(),
|
|
@@ -29,19 +30,23 @@ export async function createPkcePair(): Promise<{ verifier: string; challenge: s
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export interface AuthorizeUrlOptions {
|
|
32
|
-
/** 예: https://
|
|
33
|
-
|
|
33
|
+
/** authorize 엔드포인트 전체 주소(예: `https://accounts.sayren.app/api/auth/oauth2/authorize`) */
|
|
34
|
+
authorizeUrl: string;
|
|
34
35
|
clientId: string;
|
|
35
36
|
redirectUri: string;
|
|
36
37
|
state: string;
|
|
37
38
|
codeChallenge: string;
|
|
38
|
-
/** 생략 시
|
|
39
|
+
/** 생략 시 발급자 기본. refresh가 필요하면 offline_access 포함 */
|
|
39
40
|
scope?: string;
|
|
41
|
+
/** RFC 8707 리소스 지시자 — 주면 JWT access token(`aud` = 리소스)을 받는다 */
|
|
42
|
+
resource?: string;
|
|
43
|
+
/** OIDC `prompt` — `login`(재인증)·`create`(가입)·`consent`(동의 다시 받기) */
|
|
44
|
+
prompt?: "login" | "create" | "consent";
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
/** authorize 엔드포인트 URL 구성 — 브라우저는 이 URL로 이동(top-level navigation) */
|
|
43
48
|
export function buildAuthorizeUrl(options: AuthorizeUrlOptions): string {
|
|
44
|
-
const url = new URL(
|
|
49
|
+
const url = new URL(options.authorizeUrl);
|
|
45
50
|
url.searchParams.set("response_type", "code");
|
|
46
51
|
url.searchParams.set("client_id", options.clientId);
|
|
47
52
|
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
@@ -49,15 +54,18 @@ export function buildAuthorizeUrl(options: AuthorizeUrlOptions): string {
|
|
|
49
54
|
url.searchParams.set("code_challenge", options.codeChallenge);
|
|
50
55
|
url.searchParams.set("code_challenge_method", "S256");
|
|
51
56
|
if (options.scope) url.searchParams.set("scope", options.scope);
|
|
57
|
+
if (options.resource) url.searchParams.set("resource", options.resource);
|
|
58
|
+
if (options.prompt) url.searchParams.set("prompt", options.prompt);
|
|
52
59
|
return url.toString();
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
async function tokenRequest(
|
|
56
|
-
|
|
63
|
+
endpoint: string,
|
|
57
64
|
form: Record<string, string>,
|
|
58
65
|
fetchFn: typeof globalThis.fetch = globalThis.fetch,
|
|
59
66
|
): Promise<OAuthTokenResponse> {
|
|
60
|
-
|
|
67
|
+
// 자격 증명(쿠키)을 싣지 않는다 — public 클라이언트 + PKCE·refresh로만 인증한다(다른 origin 발급자도 같다)
|
|
68
|
+
const response = await fetchFn(endpoint, {
|
|
61
69
|
method: "POST",
|
|
62
70
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
63
71
|
body: new URLSearchParams(form).toString(),
|
|
@@ -70,23 +78,27 @@ async function tokenRequest(
|
|
|
70
78
|
return oauthTokenResponseSchema.parse(unwrapData(json));
|
|
71
79
|
}
|
|
72
80
|
|
|
73
|
-
/** 콜백에서 받은 code를
|
|
81
|
+
/** 콜백에서 받은 code를 액세스 토큰으로 교환 (PKCE verifier 동반) */
|
|
74
82
|
export async function exchangeAuthorizationCode(options: {
|
|
75
|
-
|
|
83
|
+
/** 토큰 엔드포인트 전체 주소 */
|
|
84
|
+
tokenUrl: string;
|
|
76
85
|
clientId: string;
|
|
77
86
|
redirectUri: string;
|
|
78
87
|
code: string;
|
|
79
88
|
codeVerifier: string;
|
|
89
|
+
/** RFC 8707 리소스 지시자 */
|
|
90
|
+
resource?: string;
|
|
80
91
|
fetch?: typeof globalThis.fetch;
|
|
81
92
|
}): Promise<OAuthTokenResponse> {
|
|
82
93
|
return tokenRequest(
|
|
83
|
-
options.
|
|
94
|
+
options.tokenUrl,
|
|
84
95
|
{
|
|
85
96
|
grant_type: "authorization_code",
|
|
86
97
|
code: options.code,
|
|
87
98
|
redirect_uri: options.redirectUri,
|
|
88
99
|
client_id: options.clientId,
|
|
89
100
|
code_verifier: options.codeVerifier,
|
|
101
|
+
...(options.resource ? { resource: options.resource } : {}),
|
|
90
102
|
},
|
|
91
103
|
options.fetch,
|
|
92
104
|
);
|
|
@@ -94,38 +106,21 @@ export async function exchangeAuthorizationCode(options: {
|
|
|
94
106
|
|
|
95
107
|
/** refresh grant (rotation — 응답의 새 refresh_token으로 반드시 교체 저장할 것) */
|
|
96
108
|
export async function refreshUserToken(options: {
|
|
97
|
-
|
|
109
|
+
/** 토큰 엔드포인트 전체 주소 */
|
|
110
|
+
tokenUrl: string;
|
|
98
111
|
clientId: string;
|
|
99
112
|
refreshToken: string;
|
|
113
|
+
/** RFC 8707 리소스 지시자 */
|
|
114
|
+
resource?: string;
|
|
100
115
|
fetch?: typeof globalThis.fetch;
|
|
101
116
|
}): Promise<OAuthTokenResponse> {
|
|
102
117
|
return tokenRequest(
|
|
103
|
-
options.
|
|
118
|
+
options.tokenUrl,
|
|
104
119
|
{
|
|
105
120
|
grant_type: "refresh_token",
|
|
106
121
|
refresh_token: options.refreshToken,
|
|
107
122
|
client_id: options.clientId,
|
|
108
|
-
|
|
109
|
-
options.fetch,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** userToken → storeToken 교환 (RFC 8693 token-exchange grant) */
|
|
114
|
-
export async function exchangeStoreTokenViaOAuth(options: {
|
|
115
|
-
baseUrl: string;
|
|
116
|
-
clientId: string;
|
|
117
|
-
userToken: string;
|
|
118
|
-
storeId: string;
|
|
119
|
-
fetch?: typeof globalThis.fetch;
|
|
120
|
-
}): Promise<OAuthTokenResponse & { storeId?: string; role?: string }> {
|
|
121
|
-
return tokenRequest(
|
|
122
|
-
options.baseUrl,
|
|
123
|
-
{
|
|
124
|
-
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
125
|
-
subject_token: options.userToken,
|
|
126
|
-
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
|
127
|
-
audience: options.storeId,
|
|
128
|
-
client_id: options.clientId,
|
|
123
|
+
...(options.resource ? { resource: options.resource } : {}),
|
|
129
124
|
},
|
|
130
125
|
options.fetch,
|
|
131
126
|
);
|
package/src/schemas/api-log.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* API 로그 — `/v1/api-logs`.
|
|
5
|
-
* 인증에 실패해
|
|
4
|
+
* API 로그 — `/v1/api-logs`. 상점이 해석된 관리 API(`/v1/**`) 요청을 7일간 보관한다.
|
|
5
|
+
* 인증에 실패해 상점을 알 수 없는 요청(401 등)은 기록되지 않는다.
|
|
6
6
|
* Authorization·쿠키 헤더, 요청 본문, 개인정보는 저장하지 않는다.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export const apiLogPrincipalTypeSchema = z
|
|
10
10
|
.enum(["USER", "PAT", "CLIENT"])
|
|
11
|
-
.describe(
|
|
11
|
+
.describe(
|
|
12
|
+
"호출 주체 유형. `USER`=상점 플랫폼 사용자, `PAT`=API 토큰, `CLIENT`=앱(OAuth 클라이언트)",
|
|
13
|
+
);
|
|
12
14
|
export type ApiLogPrincipalType = z.infer<typeof apiLogPrincipalTypeSchema>;
|
|
13
15
|
|
|
14
16
|
export const apiLogPrincipalSchema = z.object({
|
|
@@ -100,7 +102,7 @@ export type ApiLogPage = z.infer<typeof apiLogPageSchema>;
|
|
|
100
102
|
|
|
101
103
|
/**
|
|
102
104
|
* API 로그 요약 — `GET /api-logs/summary`. 연동 트래픽(호출 주체 `PAT`·`CLIENT`)만 집계하며
|
|
103
|
-
*
|
|
105
|
+
* 상점 플랫폼 사용자(`USER`) 요청은 제외한다
|
|
104
106
|
*/
|
|
105
107
|
export const apiLogSummarySchema = z.object({
|
|
106
108
|
windowHours: z.number().int().describe("집계 구간(시간). 최근 24시간"),
|
|
@@ -108,7 +110,7 @@ export const apiLogSummarySchema = z.object({
|
|
|
108
110
|
.number()
|
|
109
111
|
.int()
|
|
110
112
|
.min(0)
|
|
111
|
-
.describe("구간 내 연동 요청 수 (호출 주체 `PAT`·`CLIENT`,
|
|
113
|
+
.describe("구간 내 연동 요청 수 (호출 주체 `PAT`·`CLIENT`, 상점 플랫폼 사용자 제외)"),
|
|
112
114
|
errors: z.number().int().min(0).describe("구간 내 연동 요청 중 4xx·5xx 응답 수"),
|
|
113
115
|
serverErrors: z.number().int().min(0).describe("구간 내 연동 요청 중 5xx 응답 수"),
|
|
114
116
|
});
|
package/src/schemas/audit.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
export const auditActorTypeSchema = z
|
|
11
11
|
.enum(["USER", "AGENT", "APP", "TOKEN", "SYSTEM"])
|
|
12
12
|
.describe(
|
|
13
|
-
"행위자 유형. `USER
|
|
13
|
+
"행위자 유형. `USER`=상점 플랫폼 사용자, `AGENT`=AI 에이전트(MCP·에이전트용 API 토큰), `APP`=앱(OAuth 클라이언트·M2M), " +
|
|
14
14
|
"`TOKEN`=API 토큰, `SYSTEM`=자동화(주기 잡)",
|
|
15
15
|
);
|
|
16
16
|
export type AuditActorType = z.infer<typeof auditActorTypeSchema>;
|
|
@@ -55,7 +55,7 @@ export const auditLogSchema = z.object({
|
|
|
55
55
|
actorRole: z
|
|
56
56
|
.string()
|
|
57
57
|
.nullable()
|
|
58
|
-
.describe("요청 시점의
|
|
58
|
+
.describe("요청 시점의 상점 역할. 역할이 없는 토큰·자동화는 null"),
|
|
59
59
|
target: z.object({
|
|
60
60
|
type: z.string().describe("대상 리소스 유형 (예: `products`, `order-items`, `claim`)"),
|
|
61
61
|
id: z
|
|
@@ -144,7 +144,7 @@ export type AuditLogPage = z.infer<typeof auditLogPageSchema>;
|
|
|
144
144
|
export const agentPolicyDecisionSchema = z
|
|
145
145
|
.enum(["ALLOW", "APPROVAL_REQUIRED", "DENIED"])
|
|
146
146
|
.describe(
|
|
147
|
-
"에이전트 쓰기 판정. `ALLOW`=바로 실행, `APPROVAL_REQUIRED
|
|
147
|
+
"에이전트 쓰기 판정. `ALLOW`=바로 실행, `APPROVAL_REQUIRED`=상점 플랫폼 승인 뒤 실행, `DENIED`=에이전트는 실행할 수 없음",
|
|
148
148
|
);
|
|
149
149
|
export type AgentPolicyDecision = z.infer<typeof agentPolicyDecisionSchema>;
|
|
150
150
|
|
|
@@ -208,7 +208,7 @@ export const approvalRequestSchema = z.object({
|
|
|
208
208
|
decidedBy: z
|
|
209
209
|
.object({ id: z.string(), name: z.string().nullable() })
|
|
210
210
|
.nullable()
|
|
211
|
-
.describe("승인·거절한
|
|
211
|
+
.describe("승인·거절한 상점 플랫폼 사용자"),
|
|
212
212
|
decisionNote: z.string().nullable().describe("승인·거절 메모"),
|
|
213
213
|
executedAt: z.string().nullable().describe("원 요청 실행을 마친 일시"),
|
|
214
214
|
result: z
|
|
@@ -227,7 +227,7 @@ export const approvalRequestSchema = z.object({
|
|
|
227
227
|
approvalUrl: z
|
|
228
228
|
.string()
|
|
229
229
|
.nullable()
|
|
230
|
-
.describe("
|
|
230
|
+
.describe("상점 플랫폼의 승인 화면 주소. 이 배포의 상점 플랫폼 주소를 알 수 없으면 null"),
|
|
231
231
|
});
|
|
232
232
|
export type ApprovalRequest = z.infer<typeof approvalRequestSchema>;
|
|
233
233
|
|
package/src/schemas/auth.ts
CHANGED
|
@@ -4,35 +4,6 @@ export const storeRoleSchema = z.enum(["OWNER", "ADMIN", "STAFF"]);
|
|
|
4
4
|
|
|
5
5
|
export type StoreRole = z.infer<typeof storeRoleSchema>;
|
|
6
6
|
|
|
7
|
-
export const userTokenResponseSchema = z.object({
|
|
8
|
-
access_token: z.string(),
|
|
9
|
-
token_type: z.literal("Bearer"),
|
|
10
|
-
expires_in: z.number().int(),
|
|
11
|
-
refresh_token: z.string().optional(),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export type UserTokenResponse = z.infer<typeof userTokenResponseSchema>;
|
|
15
|
-
|
|
16
|
-
/** client_credentials 응답 (RFC 6749 §4.4.3) — storeToken 직발급, refresh 없음 */
|
|
17
|
-
export const clientCredentialsTokenResponseSchema = z.object({
|
|
18
|
-
access_token: z.string(),
|
|
19
|
-
token_type: z.literal("Bearer"),
|
|
20
|
-
expires_in: z.number().int(),
|
|
21
|
-
scope: z.string(),
|
|
22
|
-
});
|
|
23
|
-
export type ClientCredentialsTokenResponse = z.infer<typeof clientCredentialsTokenResponseSchema>;
|
|
24
|
-
|
|
25
|
-
export const storeTokenResponseSchema = z.object({
|
|
26
|
-
access_token: z.string(),
|
|
27
|
-
token_type: z.literal("Bearer"),
|
|
28
|
-
expires_in: z.number().int(),
|
|
29
|
-
storeId: z.string(),
|
|
30
|
-
role: storeRoleSchema,
|
|
31
|
-
scopes: z.array(z.string()),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export type StoreTokenResponse = z.infer<typeof storeTokenResponseSchema>;
|
|
35
|
-
|
|
36
7
|
export const myStoreSchema = z.object({
|
|
37
8
|
storeId: z.string(),
|
|
38
9
|
storeName: z.string(),
|
|
@@ -52,38 +23,3 @@ export const myAccountSchema = z.object({
|
|
|
52
23
|
});
|
|
53
24
|
|
|
54
25
|
export type MyAccount = z.infer<typeof myAccountSchema>;
|
|
55
|
-
|
|
56
|
-
/** API 토큰(PAT) — 목록/조회 뷰 (평문·해시 미포함) */
|
|
57
|
-
export const apiTokenSchema = z.object({
|
|
58
|
-
tokenId: z.string(),
|
|
59
|
-
name: z.string(),
|
|
60
|
-
prefix: z.string(),
|
|
61
|
-
scopes: z.array(z.string()),
|
|
62
|
-
createdBy: z.string().describe("발급자 userId"),
|
|
63
|
-
createdByName: z.string().nullable().describe("발급자 이름 (표시용). 계정이 없으면 null"),
|
|
64
|
-
createdByEmail: z.string().nullable().describe("발급자 이메일 (표시용). 계정이 없으면 null"),
|
|
65
|
-
createdAt: z.string(),
|
|
66
|
-
expiresAt: z.string().nullable(),
|
|
67
|
-
lastUsedAt: z.string().nullable(),
|
|
68
|
-
agent: z
|
|
69
|
-
.boolean()
|
|
70
|
-
.default(false)
|
|
71
|
-
.describe(
|
|
72
|
-
"AI 에이전트(MCP)용 토큰인가. true면 이 토큰의 쓰기는 행위자 `AGENT`로 기록되고 에이전트 쓰기 정책(승인 필요·거부)을 따른다",
|
|
73
|
-
),
|
|
74
|
-
});
|
|
75
|
-
export type ApiToken = z.infer<typeof apiTokenSchema>;
|
|
76
|
-
|
|
77
|
-
/** 생성 응답 — 평문 토큰은 이 응답에서 단 한 번만 노출된다 */
|
|
78
|
-
export const apiTokenCreatedSchema = apiTokenSchema.extend({ token: z.string() });
|
|
79
|
-
export type ApiTokenCreated = z.infer<typeof apiTokenCreatedSchema>;
|
|
80
|
-
|
|
81
|
-
export const apiTokenCreateRequestSchema = z.object({
|
|
82
|
-
name: z.string().min(1).max(50),
|
|
83
|
-
scopes: z.array(z.string()).min(1),
|
|
84
|
-
/** 일 단위 만료. null = 무기한, 생략 = 90일 */
|
|
85
|
-
expiresInDays: z.number().int().nullable().optional(),
|
|
86
|
-
/** AI 에이전트(MCP)용 토큰. 생략 = false. 발급 뒤에는 바꿀 수 없다 */
|
|
87
|
-
agent: z.boolean().optional(),
|
|
88
|
-
});
|
|
89
|
-
export type ApiTokenCreateRequest = z.infer<typeof apiTokenCreateRequestSchema>;
|
package/src/schemas/claim.ts
CHANGED
|
@@ -31,10 +31,10 @@ export const claimReasonSchema = z.enum([
|
|
|
31
31
|
export type ClaimReason = z.infer<typeof claimReasonSchema>;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* 반품지 (이슈 #30) — 반품·교환 수거가 안내하는 주소다.
|
|
34
|
+
* 반품지 (이슈 #30) — 반품·교환 수거가 안내하는 주소다. 상점 주소록의 기본 반품지이거나,
|
|
35
35
|
* 상품이 반품지를 따로 지정했으면 그 주소다.
|
|
36
36
|
*
|
|
37
|
-
* 취소 클레임은 보낼 물건이 없어 null이고, 반품지를 등록하지 않은
|
|
37
|
+
* 취소 클레임은 보낼 물건이 없어 null이고, 반품지를 등록하지 않은 상점도 null이다.
|
|
38
38
|
* 구매자가 직접 보내는 수거(`BUYER_SEND`)에 이 주소가 필요하다.
|
|
39
39
|
*/
|
|
40
40
|
export const claimReturnAddressSchema = z.object({
|
|
@@ -102,8 +102,8 @@ export const claimSchema = z.object({
|
|
|
102
102
|
returnAddress: claimReturnAddressSchema
|
|
103
103
|
.nullable()
|
|
104
104
|
.describe(
|
|
105
|
-
"수거 안내에 쓸 반품지(이슈 #30). 상품이 지정한 반품지, 없으면
|
|
106
|
-
"취소 클레임과 반품지를 등록하지 않은
|
|
105
|
+
"수거 안내에 쓸 반품지(이슈 #30). 상품이 지정한 반품지, 없으면 상점 기본 반품지다. " +
|
|
106
|
+
"취소 클레임과 반품지를 등록하지 않은 상점은 null",
|
|
107
107
|
),
|
|
108
108
|
});
|
|
109
109
|
|
package/src/schemas/coupon.ts
CHANGED
|
@@ -45,7 +45,7 @@ const limitField = (description: string) =>
|
|
|
45
45
|
const descriptions = {
|
|
46
46
|
name: "구매자에게 보이는 쿠폰 이름. 1~60자",
|
|
47
47
|
description: "구매자에게 보이는 설명. 300자까지",
|
|
48
|
-
code: "쿠폰 코드 — 발급 방식이 `CODE`일 때 필수. 영문·숫자·`-`·`_` 6~30자, 대소문자 구분 없음(대문자로 저장).
|
|
48
|
+
code: "쿠폰 코드 — 발급 방식이 `CODE`일 때 필수. 영문·숫자·`-`·`_` 6~30자, 대소문자 구분 없음(대문자로 저장). 상점 안에서 유일하다",
|
|
49
49
|
startsAt: "사용 시작 시각(ISO 8601, 시간대 포함). 생략하면 바로 쓸 수 있다",
|
|
50
50
|
endsAt:
|
|
51
51
|
"사용 종료 시각(ISO 8601, 시간대 포함). 이 시각부터 못 쓴다. 생략하면 제한이 없다. " +
|
|
@@ -247,7 +247,7 @@ export const issueCouponResultSchema = z.object({
|
|
|
247
247
|
code: z
|
|
248
248
|
.string()
|
|
249
249
|
.describe(
|
|
250
|
-
"실패 사유. `CUSTOMER_NOT_FOUND`(이
|
|
250
|
+
"실패 사유. `CUSTOMER_NOT_FOUND`(이 상점 구매자가 아님)·`COUPON_LIMIT_REACHED`(1인 발급 한도)·" +
|
|
251
251
|
"`COUPON_EXHAUSTED`(총 발급 한도)",
|
|
252
252
|
),
|
|
253
253
|
message: z.string(),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 구매자 위임 REST(`POST /v1/customers/delegations`, #71) — 계정 토큰(또는 관리 API 토큰) + `X-Store-Id`로 부른다(`customer:rw`).
|
|
5
|
+
* 자체 회원 시스템을 가진 셀러 서버가 로그인한 사용자를 sayren 구매자 토큰(30분, refresh 없음)으로 바꾼다. 옛 token-exchange
|
|
6
|
+
* (`member-uid`·`member-ref`)를 대신한다. 에이전트 요청은 거부된다(`x-agent-policy` DENIED).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const uidPattern = /^[A-Za-z0-9._:@|-]{1,128}$/;
|
|
10
|
+
|
|
11
|
+
export const customerDelegationRequestSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
uid: z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(uidPattern)
|
|
16
|
+
.optional()
|
|
17
|
+
.describe(
|
|
18
|
+
"셀러 자체 인증의 사용자 id(1~128자, 영문·숫자·`._:@|-`). 처음 보는 id면 구매자를 만든다",
|
|
19
|
+
),
|
|
20
|
+
email: z
|
|
21
|
+
.email()
|
|
22
|
+
.max(254)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("uid가 없으면 이메일로 지목한다. uid와 함께 주면 처음 만들 때의 이메일이다"),
|
|
25
|
+
name: z.string().max(50).optional().describe("처음 만들 때의 이름"),
|
|
26
|
+
authenticatedAt: z.iso
|
|
27
|
+
.datetime({ offset: true })
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(
|
|
30
|
+
"사용자가 셀러 서비스에서 실제로 로그인한 시각(uid 위임만). 주면 구매자 토큰의 인증 시각이 된다",
|
|
31
|
+
),
|
|
32
|
+
})
|
|
33
|
+
.refine((value) => Boolean(value.uid || value.email), {
|
|
34
|
+
message: "uid 또는 email이 필요합니다",
|
|
35
|
+
path: ["uid"],
|
|
36
|
+
})
|
|
37
|
+
.describe("구매자 위임 요청");
|
|
38
|
+
export type CustomerDelegationRequest = z.infer<typeof customerDelegationRequestSchema>;
|
|
39
|
+
|
|
40
|
+
export const customerDelegationSchema = z
|
|
41
|
+
.object({
|
|
42
|
+
accessToken: z.string().describe("구매자 액세스 토큰(스토어프론트 API Bearer)"),
|
|
43
|
+
tokenType: z.literal("Bearer"),
|
|
44
|
+
expiresIn: z.number().int().describe("수명(초)"),
|
|
45
|
+
memberId: z.string(),
|
|
46
|
+
})
|
|
47
|
+
.describe("구매자 위임 결과");
|
|
48
|
+
export type CustomerDelegation = z.infer<typeof customerDelegationSchema>;
|
package/src/schemas/customer.ts
CHANGED
|
@@ -88,7 +88,7 @@ export const customerConsentSchema = z.object({
|
|
|
88
88
|
kind: consentKindSchema,
|
|
89
89
|
/** 동의한 약관 버전. 마케팅은 버전이 없어 null */
|
|
90
90
|
version: z.string().nullable(),
|
|
91
|
-
/** 동의 시점의 약관 문서 주소.
|
|
91
|
+
/** 동의 시점의 약관 문서 주소. 상점이 주소를 정하지 않았거나 마케팅이면 null */
|
|
92
92
|
documentUrl: z.string().nullable(),
|
|
93
93
|
action: consentActionSchema,
|
|
94
94
|
source: consentSourceSchema,
|
|
@@ -102,9 +102,9 @@ export const customerConsentStateSchema = z.object({
|
|
|
102
102
|
agreed: z.boolean(),
|
|
103
103
|
/** 동의한 약관 버전. 마케팅이거나 이력이 없으면 null */
|
|
104
104
|
agreedVersion: z.string().nullable(),
|
|
105
|
-
/**
|
|
105
|
+
/** 상점의 현재 약관 버전. 마케팅은 null */
|
|
106
106
|
currentVersion: z.string().nullable(),
|
|
107
|
-
/**
|
|
107
|
+
/** 상점이 정한 현재 약관 문서 주소. 정하지 않았거나 마케팅이면 null */
|
|
108
108
|
currentDocumentUrl: z.string().nullable(),
|
|
109
109
|
updatedAt: z.string().nullable(),
|
|
110
110
|
/** 약관이 개정돼 다시 받아야 하는가. 마케팅은 항상 false */
|
|
@@ -107,11 +107,11 @@ const KNOWN_FULFILLMENT_TYPES: readonly string[] = fulfillmentTypeSchema.options
|
|
|
107
107
|
|
|
108
108
|
const ADDRESS_ID_DESCRIPTIONS = {
|
|
109
109
|
shipping:
|
|
110
|
-
"출고지 주소 id(`GET /v1/seller-addresses?type=SHIPPING`). 등록에서 생략하면 그 시점
|
|
111
|
-
"`null`이면 비워 둔다(
|
|
110
|
+
"출고지 주소 id(`GET /v1/seller-addresses?type=SHIPPING`). 등록에서 생략하면 그 시점 상점 기본 출고지를 저장한다. " +
|
|
111
|
+
"`null`이면 비워 둔다(상점 기본 출고지를 따른다). 이 상점의 `SHIPPING` 주소가 아니면 거부한다",
|
|
112
112
|
return:
|
|
113
|
-
"반품지 주소 id(`GET /v1/seller-addresses?type=RETURN`). 등록에서 생략하면 그 시점
|
|
114
|
-
"`null`이면 비워 둔다(
|
|
113
|
+
"반품지 주소 id(`GET /v1/seller-addresses?type=RETURN`). 등록에서 생략하면 그 시점 상점 기본 반품지를 저장한다. " +
|
|
114
|
+
"`null`이면 비워 둔다(상점 기본 반품지를 따른다). 이 상점의 `RETURN` 주소가 아니면 거부한다",
|
|
115
115
|
} as const;
|
|
116
116
|
|
|
117
117
|
/**
|
|
@@ -161,7 +161,7 @@ const shippingCreateSchema = z
|
|
|
161
161
|
.min(0)
|
|
162
162
|
.optional()
|
|
163
163
|
.describe(
|
|
164
|
-
"배송비. 원 단위. 생략하면
|
|
164
|
+
"배송비. 원 단위. 생략하면 상점 기본 배송비(`GET /v1/store`의 `shippingPolicy.baseDeliveryFee`)를 저장한다. FREE면 0이거나 생략한다",
|
|
165
165
|
),
|
|
166
166
|
conditionalFreeAmount: z
|
|
167
167
|
.number()
|
|
@@ -176,7 +176,7 @@ const shippingCreateSchema = z
|
|
|
176
176
|
.superRefine(refineShippingConsistency);
|
|
177
177
|
|
|
178
178
|
/**
|
|
179
|
-
* 상품 이행 — 등록(POST). 배송 상품의 `shipping`을 생략하면 기본값(유료
|
|
179
|
+
* 상품 이행 — 등록(POST). 배송 상품의 `shipping`을 생략하면 기본값(유료 배송·상점 기본 배송비·기본 출고지·반품지)이다.
|
|
180
180
|
*/
|
|
181
181
|
export const productFulfillmentCreateSchema = z.discriminatedUnion("type", [
|
|
182
182
|
z.strictObject({ type: z.literal("SHIPPING"), shipping: shippingCreateSchema.optional() }),
|
|
@@ -235,11 +235,11 @@ export const productFulfillmentSchema = z.discriminatedUnion("type", [
|
|
|
235
235
|
shippingAddressId: z
|
|
236
236
|
.string()
|
|
237
237
|
.nullable()
|
|
238
|
-
.describe("출고지 주소 id. null이면
|
|
238
|
+
.describe("출고지 주소 id. null이면 상점 기본 출고지를 따른다"),
|
|
239
239
|
returnAddressId: z
|
|
240
240
|
.string()
|
|
241
241
|
.nullable()
|
|
242
|
-
.describe("반품지 주소 id. null이면
|
|
242
|
+
.describe("반품지 주소 id. null이면 상점 기본 반품지를 따른다"),
|
|
243
243
|
}),
|
|
244
244
|
}),
|
|
245
245
|
z.object({ type: z.literal("MANUAL") }),
|
|
@@ -306,9 +306,7 @@ export const orderItemFulfillmentSnapshotSchema = z.discriminatedUnion("type", [
|
|
|
306
306
|
returnAddressId: z
|
|
307
307
|
.string()
|
|
308
308
|
.nullable()
|
|
309
|
-
.describe(
|
|
310
|
-
"주문 시점 반품지 주소 id. null이거나 지워진 주소면 스토어 기본 반품지로 안내한다",
|
|
311
|
-
),
|
|
309
|
+
.describe("주문 시점 반품지 주소 id. null이거나 지워진 주소면 상점 기본 반품지로 안내한다"),
|
|
312
310
|
}),
|
|
313
311
|
}),
|
|
314
312
|
z.object({ type: z.literal("MANUAL") }),
|
package/src/schemas/order.ts
CHANGED
|
@@ -328,7 +328,7 @@ export const dispatchRequestSchema = z
|
|
|
328
328
|
* 발송 시각. ISO 8601이고 시간대(`Z` 또는 `+09:00`)를 포함해야 한다. 생략하면 현재 시각이다.
|
|
329
329
|
*
|
|
330
330
|
* 전에는 `z.string()`이라 아무 문자열이나 그대로 저장됐다. 이 값은 배송 정보 jsonb에 들어가
|
|
331
|
-
* 자동 배송완료·추적 동기화 잡의 대상 선별 조건이 된다 — 잘못된 한 행이
|
|
331
|
+
* 자동 배송완료·추적 동기화 잡의 대상 선별 조건이 된다 — 잘못된 한 행이 상점 구분 없이 도는
|
|
332
332
|
* 잡 전체를 멈출 수 있어 입력에서 좁혔다. 검증을 통과한 값도 저장 전에 UTC ISO로 정규화한다.
|
|
333
333
|
*/
|
|
334
334
|
dispatchedAt: z.iso.datetime({ offset: true }).optional(),
|