@sayren/store-sdk 0.22.0 → 0.28.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.d.mts +905 -620
- package/dist/index.mjs +352 -243
- package/dist/server.d.mts +19 -28
- package/dist/server.mjs +29 -42
- package/package.json +1 -1
- package/src/client.ts +59 -141
- package/src/index.ts +3 -2
- package/src/member-delegation.ts +51 -77
- package/src/oauth.ts +28 -33
- package/src/schemas/auth.ts +0 -64
- package/src/schemas/customer-delegation.ts +48 -0
- package/src/schemas/product.ts +7 -0
- package/src/schemas/scope.ts +1 -2
- package/src/schemas/store.ts +1 -1
- package/src/schemas/storefront-hosting.ts +82 -6
- package/src/schemas/subscription.ts +222 -0
- package/src/server.ts +1 -2
- package/src/session.ts +84 -26
- package/src/schemas/api-client.ts +0 -121
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/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>;
|
|
@@ -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/product.ts
CHANGED
|
@@ -35,6 +35,13 @@ export const productSummarySchema = z.object({
|
|
|
35
35
|
"상품 이행의 유형. 유형별 속성은 단건 조회(`GET /v1/products/{productId}`)의 `fulfillment`에 있다",
|
|
36
36
|
),
|
|
37
37
|
updatedAt: z.string(),
|
|
38
|
+
sample: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.default(false)
|
|
42
|
+
.describe(
|
|
43
|
+
"스토어를 만들 때 넣은 샘플 상품인가(이슈 #70). 콘솔이 「샘플」 배지를 단다. 스토어프론트에는 싣지 않는다. 서버는 항상 싣는다",
|
|
44
|
+
),
|
|
38
45
|
});
|
|
39
46
|
|
|
40
47
|
export type ProductSummary = z.infer<typeof productSummarySchema>;
|
package/src/schemas/scope.ts
CHANGED
|
@@ -49,8 +49,7 @@ export function isStoreScope(scope: string): boolean {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* 역할별 자동 부여 스코프 (auth-spec §7).
|
|
52
|
-
*
|
|
53
|
-
* 발급 시 지정한 스코프만 갖는다(발급자 보유 스코프 이내).
|
|
52
|
+
* 계정 토큰으로 스토어를 호출할 때의 스코프는 그 스토어에서의 역할로 이 표에서 결정된다.
|
|
54
53
|
*/
|
|
55
54
|
export const STORE_ROLE_SCOPES = {
|
|
56
55
|
OWNER: [
|
package/src/schemas/store.ts
CHANGED
|
@@ -270,7 +270,7 @@ export type StoreMember = z.infer<typeof storeMemberSchema>;
|
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
272
|
* 초대 메일 발송 결과 — `SENDING`은 발송 요청 중(결과를 아직 적지 못함), `NOT_CONFIGURED`는 메일 발송이 설정되지 않아
|
|
273
|
-
* 보내지 않은 것이다. 발송 실패는 초대를 무효로 만들지 않는다 — 재발송으로 다시 보낸다
|
|
273
|
+
* 보내지 않은 것이다. 스토어의 월 메일 한도(요금제)를 다 써서 보내지 않았으면 `FAILED` + `deliveryErrorCode: MAIL_QUOTA_EXCEEDED`다. 발송 실패는 초대를 무효로 만들지 않는다 — 재발송으로 다시 보낸다
|
|
274
274
|
*/
|
|
275
275
|
export const invitationDeliveryStatusSchema = z.enum([
|
|
276
276
|
"SENDING",
|
|
@@ -4,11 +4,12 @@ import { RESERVED_STORE_CODES } from "./store";
|
|
|
4
4
|
/**
|
|
5
5
|
* 스토어프론트 호스팅 (이슈 #53, M1: 템플릿 발행) — 관리 API `/v1/storefront/**`의 계약이다.
|
|
6
6
|
*
|
|
7
|
-
* 셀러는 템플릿을 골라 사이트를 만들고 `{slug}.{호스팅 도메인}` 주소를 받는다.
|
|
7
|
+
* 셀러는 템플릿을 골라 사이트를 만들고 `{slug}.{호스팅 도메인}` 주소를 받는다. 스토어를 만들어도 사이트는 생기지 않고
|
|
8
|
+
* `POST /v1/storefront/site`가 유일한 생성 경로다. 템플릿은 만들 때만 고르고 바꾸지 않는다. 발행은 버전 단위이고,
|
|
8
9
|
* 되돌리기는 이전 버전을 다시 발행하는 것이다(재빌드 없음). 상태의 원천은 api DB이고 Cloudflare 쪽 반영
|
|
9
10
|
* (라우팅 표·사이트 Worker)은 커밋 뒤 비동기다 — 반영 상태는 `provisioning`으로 본다.
|
|
10
11
|
*
|
|
11
|
-
* 스코프는 조회 `storefront:r`, 쓰기 `storefront:rw`(역할 OWNER·ADMIN)다. 에이전트(MCP)의
|
|
12
|
+
* 스코프는 조회 `storefront:r`, 쓰기 `storefront:rw`(역할 OWNER·ADMIN)다. 에이전트(MCP)의 발행·되돌리기·
|
|
12
13
|
* 공개 전환·슬러그 변경·호스팅 끄기는 승인(`APPROVAL_REQUIRED`)이다 — 구매자가 보는 화면을 바꾸는 쓰기다.
|
|
13
14
|
*
|
|
14
15
|
* 오류 코드:
|
|
@@ -23,6 +24,8 @@ import { RESERVED_STORE_CODES } from "./store";
|
|
|
23
24
|
* - 409 `STORE_SUSPENDED` — 정지된 스토어에는 사이트를 만들 수 없다
|
|
24
25
|
* - 409 `STOREFRONT_VERSION_NOT_READY` — 빌드가 끝나지 않았거나 실패한 버전은 발행할 수 없다
|
|
25
26
|
* - 409 `STOREFRONT_NO_ROLLBACK_TARGET` — 되돌릴 이전 발행 버전이 없다
|
|
27
|
+
* - 409 `STOREFRONT_NOT_READY` — 공개 전환: 판매 중인 상품(샘플 제외)이 없다(이슈 #70). 상품을 등록한 뒤 공개한다
|
|
28
|
+
* - 409 `TEMPLATE_CHANGE_NOT_SUPPORTED` — 템플릿 변경(`POST /v1/storefront/site/template`, 폐기)은 항상 이 오류다
|
|
26
29
|
* - 400 `STOREFRONT_SLUG_CONFIRM_MISMATCH` — 호스팅 끄기 확인 입력이 현재 슬러그와 다르다
|
|
27
30
|
* - 503 `STOREFRONT_PREVIEW_NOT_CONFIGURED` — 미리보기 서명 키가 설정되지 않았다
|
|
28
31
|
*
|
|
@@ -89,6 +92,16 @@ export const RESERVED_STOREFRONT_SLUGS: readonly string[] = [
|
|
|
89
92
|
*/
|
|
90
93
|
export const STOREFRONT_EDIT_PREVIEW_SUFFIX = "-preview";
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 공식 쇼케이스 슬러그 접두사(이슈 #72) — `demo-{템플릿 id}`는 템플릿 미리보기용 쇼케이스 사이트 주소다. 쇼케이스로 지정된
|
|
97
|
+
* 스토어만 쓸 수 있다(서버가 스토어 용도로 판정한다). 공식 미리보기를 흉내 내는 주소를 막는다
|
|
98
|
+
*/
|
|
99
|
+
export const STOREFRONT_SHOWCASE_SLUG_PREFIX = "demo-";
|
|
100
|
+
|
|
101
|
+
export function isShowcaseStorefrontSlug(slug: string): boolean {
|
|
102
|
+
return slug.toLowerCase().startsWith(STOREFRONT_SHOWCASE_SLUG_PREFIX);
|
|
103
|
+
}
|
|
104
|
+
|
|
92
105
|
export function isReservedStorefrontSlug(slug: string): boolean {
|
|
93
106
|
const lower = slug.toLowerCase();
|
|
94
107
|
return (
|
|
@@ -132,8 +145,25 @@ export const storefrontTemplateSchema = z.object({
|
|
|
132
145
|
industry: z.string().describe("업종 (예: `general`·`fashion`·`food`). 목록 필터에 쓴다"),
|
|
133
146
|
description: z.string().describe("템플릿 설명"),
|
|
134
147
|
thumbnailUrl: z.string().nullable().describe("썸네일 이미지 주소. 없으면 null"),
|
|
135
|
-
version: z.string().describe("현재 템플릿 버전. 새
|
|
148
|
+
version: z.string().describe("현재 템플릿 버전. 새 사이트는 이 버전으로 만든다"),
|
|
136
149
|
isDefault: z.boolean().describe("기본 템플릿인가 (스토어 생성 시 자동 생성에 쓴다)"),
|
|
150
|
+
tags: z
|
|
151
|
+
.array(z.string())
|
|
152
|
+
.default([])
|
|
153
|
+
.describe(
|
|
154
|
+
"템플릿 특징 태그 (예: `후기 강조`). 목록 카드에 보인다. 옛 서버 응답은 빈 배열로 읽는다",
|
|
155
|
+
),
|
|
156
|
+
supportsCheckout: z
|
|
157
|
+
.boolean()
|
|
158
|
+
.default(true)
|
|
159
|
+
.describe(
|
|
160
|
+
"주문을 받는 템플릿인가. false는 가격·문의 중심의 카탈로그형이다. 옛 서버 응답은 true로 읽는다",
|
|
161
|
+
),
|
|
162
|
+
previewUrl: z
|
|
163
|
+
.string()
|
|
164
|
+
.nullable()
|
|
165
|
+
.default(null)
|
|
166
|
+
.describe("공식 쇼케이스 사이트 주소. 연결된 쇼케이스가 공개 중일 때만 싣고 그 밖에는 null"),
|
|
137
167
|
});
|
|
138
168
|
export type StorefrontTemplate = z.infer<typeof storefrontTemplateSchema>;
|
|
139
169
|
|
|
@@ -242,7 +272,7 @@ export const storefrontEditPreviewSchema = z.object({
|
|
|
242
272
|
export type StorefrontEditPreview = z.infer<typeof storefrontEditPreviewSchema>;
|
|
243
273
|
|
|
244
274
|
/**
|
|
245
|
-
* 사이트 정지 — 운영 정책에 따라 플랫폼 운영자가 사이트를 정지한다. 정지 중에는 방문자에게 정지 안내가 보이고
|
|
275
|
+
* 사이트 정지 — 운영 정책에 따라 플랫폼 운영자가 사이트를 정지한다. 정지 중에는 방문자에게 정지 안내가 보이고 발행·되돌리기·
|
|
246
276
|
* 공개 전환·슬러그 변경·끄기·편집이 409 `STOREFRONT_SITE_NOT_ACTIVE`다. 셀러가 풀 수 없다(고객센터로 문의한다).
|
|
247
277
|
*/
|
|
248
278
|
export const storefrontSuspensionSchema = z.object({
|
|
@@ -301,6 +331,50 @@ export const storefrontSlugAvailabilitySchema = z.object({
|
|
|
301
331
|
});
|
|
302
332
|
export type StorefrontSlugAvailability = z.infer<typeof storefrontSlugAvailabilitySchema>;
|
|
303
333
|
|
|
334
|
+
/**
|
|
335
|
+
* 배포 진행 — 사이트의 지금 발행 상태가 공개 주소에 반영되기까지의 단계와 기록이다(콘솔의 스토어프론트 만들기 3단계가 폴링한다).
|
|
336
|
+
* 만들기(`POST /v1/storefront/site`)는 곧바로 201로 답하고 반영은 그 뒤에 비동기로 하므로, 끝났는지는 이 조회로 본다.
|
|
337
|
+
* 템플릿 사이트는 미리 빌드한 템플릿 산출물을 올리므로 빌드 단계가 없다. 기록은 사이트 상태에서 만든 요약이다.
|
|
338
|
+
*/
|
|
339
|
+
export const storefrontDeploymentStepSchema = z.object({
|
|
340
|
+
key: z
|
|
341
|
+
.enum(["SITE", "UPLOAD", "PUBLISH"])
|
|
342
|
+
.describe("`SITE` 사이트·첫 버전 기록 · `UPLOAD` 버전 올리기 · `PUBLISH` 주소 연결·발행"),
|
|
343
|
+
status: z.enum(["PENDING", "RUNNING", "DONE", "FAILED"]),
|
|
344
|
+
finishedAt: z.string().nullable().describe("끝난 시각 (ISO 8601). 모르거나 끝나지 않았으면 null"),
|
|
345
|
+
});
|
|
346
|
+
export type StorefrontDeploymentStep = z.infer<typeof storefrontDeploymentStepSchema>;
|
|
347
|
+
|
|
348
|
+
export const storefrontDeploymentLogSchema = z.object({
|
|
349
|
+
at: z.string().nullable().describe("기록 시각 (ISO 8601). 모르면 null"),
|
|
350
|
+
level: z.enum(["INFO", "WARN", "ERROR"]),
|
|
351
|
+
code: z
|
|
352
|
+
.string()
|
|
353
|
+
.describe(
|
|
354
|
+
"기록 종류 (`SITE_CREATED`·`ADDRESS_RESERVED`·`WAITING`·`SYNCING`·`VERSION_UPLOADED`·`SYNC_FAILED`·`RETRY_SCHEDULED`·`PUBLISHED`)",
|
|
355
|
+
),
|
|
356
|
+
message: z.string().describe("사람이 읽는 문장"),
|
|
357
|
+
});
|
|
358
|
+
export type StorefrontDeploymentLog = z.infer<typeof storefrontDeploymentLogSchema>;
|
|
359
|
+
|
|
360
|
+
export const storefrontDeploymentSchema = z.object({
|
|
361
|
+
siteId: z.string(),
|
|
362
|
+
status: z
|
|
363
|
+
.enum(["IN_PROGRESS", "SUCCEEDED", "FAILED"])
|
|
364
|
+
.describe(
|
|
365
|
+
"`IN_PROGRESS` 반영 중 · `SUCCEEDED` 공개 주소에 반영됨 · `FAILED` 여러 번 실패했거나 다음 자동 시도가 멀다(`POST …/deployment/retry`로 곧바로 다시 시도한다)",
|
|
366
|
+
),
|
|
367
|
+
url: z.string().describe("사이트 기본 주소 (`https://{slug}.{호스팅 도메인}`)"),
|
|
368
|
+
startedAt: z.string().describe("사이트를 만든 시각 (ISO 8601)"),
|
|
369
|
+
finishedAt: z.string().nullable().describe("반영을 마친 시각 (ISO 8601)"),
|
|
370
|
+
steps: z.array(storefrontDeploymentStepSchema),
|
|
371
|
+
log: z.array(storefrontDeploymentLogSchema).describe("오래된 것부터"),
|
|
372
|
+
errorCode: z.string().nullable().describe("마지막 반영 실패 코드"),
|
|
373
|
+
attempts: z.number().int().describe("연속 실패 횟수"),
|
|
374
|
+
nextAttemptAt: z.string().nullable().describe("다음 자동 시도 시각 (ISO 8601)"),
|
|
375
|
+
});
|
|
376
|
+
export type StorefrontDeployment = z.infer<typeof storefrontDeploymentSchema>;
|
|
377
|
+
|
|
304
378
|
// ─── 요청 ─────────────────────────────────────────────────
|
|
305
379
|
|
|
306
380
|
export const createStorefrontSiteRequestSchema = z.object({
|
|
@@ -309,6 +383,7 @@ export const createStorefrontSiteRequestSchema = z.object({
|
|
|
309
383
|
});
|
|
310
384
|
export type CreateStorefrontSiteRequest = z.infer<typeof createStorefrontSiteRequestSchema>;
|
|
311
385
|
|
|
386
|
+
/** @deprecated 템플릿 변경은 지원하지 않는다(항상 409 `TEMPLATE_CHANGE_NOT_SUPPORTED`). 호환을 위해 스키마만 남겼다 */
|
|
312
387
|
export const changeStorefrontTemplateRequestSchema = z.object({
|
|
313
388
|
templateId: z.string().min(1).max(64).describe("바꿀 템플릿 id"),
|
|
314
389
|
publish: z
|
|
@@ -317,6 +392,7 @@ export const changeStorefrontTemplateRequestSchema = z.object({
|
|
|
317
392
|
.describe("새 버전을 곧바로 발행하는가 (기본 true). false면 버전만 만든다"),
|
|
318
393
|
message: z.string().max(200).optional().describe("버전 메모"),
|
|
319
394
|
});
|
|
395
|
+
/** @deprecated 템플릿 변경은 지원하지 않는다 */
|
|
320
396
|
export type ChangeStorefrontTemplateRequest = z.infer<typeof changeStorefrontTemplateRequestSchema>;
|
|
321
397
|
|
|
322
398
|
/**
|
|
@@ -1002,7 +1078,7 @@ export const storefrontRuleResultSchema = z.object({
|
|
|
1002
1078
|
export type StorefrontRuleResult = z.infer<typeof storefrontRuleResultSchema>;
|
|
1003
1079
|
|
|
1004
1080
|
/**
|
|
1005
|
-
* 규칙 검증 보고서 — `@sayren/mcp`의 `verify_storefront`와 같은 모양이다(원천 `
|
|
1081
|
+
* 규칙 검증 보고서 — `@sayren/mcp`의 `verify_storefront`와 같은 모양이다(원천 `packages/mcp/src/verify.ts`). 위반마다
|
|
1006
1082
|
* 고칠 자리(`file`·`line`)와 방법(`fix`·`example`)을 싣는다. `POST /v1/storefront/site/verify`(편집 환경이 잠들어 있어도
|
|
1007
1083
|
* 된다)와 빌드의 `verify`가 이 모양이다.
|
|
1008
1084
|
*/
|
|
@@ -1074,7 +1150,7 @@ export const STOREFRONT_BUILD_STAGES = [
|
|
|
1074
1150
|
* 의존성 검사 결과 한 줄(#61) — 거절(`DEPENDENCY_REJECTED`의 `details.rejected`)·경고(빌드 `dependencies`, 소스 교체 `dependencyWarnings`)가
|
|
1075
1151
|
* 같은 모양이다. 대상 패키지와 사유. 셀러가 고칠 수 있게 버전·권고 id·거절 사유를 싣는다.
|
|
1076
1152
|
* `reason`: `DENYLISTED`(거절 목록) · `ADVISORY`(보안 권고) · `MALWARE`(악성 코드 권고, 패키지 전체) · `INSTALL_SCRIPT`(루트
|
|
1077
|
-
* package.json의 설치
|
|
1153
|
+
* package.json의 설치 훅·의존성의 설치 스크립트, 경고) · `INSTALL_CONFIG`(설치 방식 필드, 경고) · `UNSUPPORTED_SOURCE`(레지스트리 밖 설치 경로) · `BUNDLED_DEPENDENCIES`(묶여
|
|
1078
1154
|
* 오는 의존성, 경고) · `INVALID_MANIFEST`(package.json·잠금 파일 형식), 늘어날 수 있다.
|
|
1079
1155
|
*/
|
|
1080
1156
|
export const storefrontDependencyFindingSchema = z.object({
|