@sayren/store-sdk 0.21.0 → 0.23.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 +746 -17
- package/dist/index.mjs +421 -15
- package/package.json +1 -1
- package/src/client.ts +88 -2
- package/src/schemas/storefront-hosting.ts +525 -9
- package/src/session.ts +42 -1
|
@@ -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,7 @@ 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 `TEMPLATE_CHANGE_NOT_SUPPORTED` — 템플릿 변경(`POST /v1/storefront/site/template`, 폐기)은 항상 이 오류다
|
|
26
28
|
* - 400 `STOREFRONT_SLUG_CONFIRM_MISMATCH` — 호스팅 끄기 확인 입력이 현재 슬러그와 다르다
|
|
27
29
|
* - 503 `STOREFRONT_PREVIEW_NOT_CONFIGURED` — 미리보기 서명 키가 설정되지 않았다
|
|
28
30
|
*
|
|
@@ -42,6 +44,16 @@ import { RESERVED_STORE_CODES } from "./store";
|
|
|
42
44
|
* - 409 `STOREFRONT_BUILD_IN_PROGRESS` — 이 사이트에 끝나지 않은 빌드가 있다(사이트당 하나)
|
|
43
45
|
* - 404 `STOREFRONT_BUILD_NOT_FOUND`
|
|
44
46
|
* - 502 `STOREFRONT_SANDBOX_UNAVAILABLE` — 편집 Sandbox 호출이 실패했다. 잠시 뒤 다시 한다
|
|
47
|
+
*
|
|
48
|
+
* 소스 내려받기·올리기(#60, `/v1/storefront/site/source/**`, `sayren` CLI):
|
|
49
|
+
* - 409 `SOURCE_CHANGED` — 작업 트리가 내려받은 뒤(`expectedBaseRef`) 바뀌었다(콘솔·AI 편집). `error.details.currentRef`
|
|
50
|
+
* - 409 `EDIT_SESSION_OPEN` — 편집 환경이 열려 있다. `endEditSession: true`로 끄고 진행할 수 있다
|
|
51
|
+
* - 409 `SOURCE_BLOBS_MISSING` — 올리지 않은 파일 내용이 있다(`error.details.hashes`). `POST source/blobs`로 올리고 다시 한다
|
|
52
|
+
* - 409 `DEPENDENCY_REJECTED` — 쓸 수 없는 npm 의존성이다(`error.details.rejected: [{ name, version, reason, detail }]`)
|
|
53
|
+
* - 413 `SOURCE_TOO_MANY_NEW_FILES` — 한 번에 새 내용이 800개를 넘는다(`error.details.max`). 나눠서 바꾼다
|
|
54
|
+
* - 400 `DEPENDENCY_FILES_MISMATCH` — `packageJson`·`lockfile` 내용이 목록의 해시와 다르거나 빠졌다
|
|
55
|
+
* - 400 `INVALID_SOURCE_REF` · `BLOB_HASH_MISMATCH` · `INVALID_FILE_PATH`(`.env*`·작업 트리 밖 경로)
|
|
56
|
+
* - 404 `STOREFRONT_SOURCE_NOT_FOUND` — 그 스냅샷이 없다
|
|
45
57
|
*/
|
|
46
58
|
|
|
47
59
|
// ─── 슬러그 ───────────────────────────────────────────────
|
|
@@ -69,6 +81,8 @@ export const RESERVED_STOREFRONT_SLUGS: readonly string[] = [
|
|
|
69
81
|
"dispatch",
|
|
70
82
|
"control",
|
|
71
83
|
"outbound",
|
|
84
|
+
// 내 도메인(M3)의 CNAME 대상·fallback origin이다(`sites.{호스팅 도메인}`)
|
|
85
|
+
"sites",
|
|
72
86
|
];
|
|
73
87
|
|
|
74
88
|
/**
|
|
@@ -120,7 +134,7 @@ export const storefrontTemplateSchema = z.object({
|
|
|
120
134
|
industry: z.string().describe("업종 (예: `general`·`fashion`·`food`). 목록 필터에 쓴다"),
|
|
121
135
|
description: z.string().describe("템플릿 설명"),
|
|
122
136
|
thumbnailUrl: z.string().nullable().describe("썸네일 이미지 주소. 없으면 null"),
|
|
123
|
-
version: z.string().describe("현재 템플릿 버전. 새
|
|
137
|
+
version: z.string().describe("현재 템플릿 버전. 새 사이트는 이 버전으로 만든다"),
|
|
124
138
|
isDefault: z.boolean().describe("기본 템플릿인가 (스토어 생성 시 자동 생성에 쓴다)"),
|
|
125
139
|
});
|
|
126
140
|
export type StorefrontTemplate = z.infer<typeof storefrontTemplateSchema>;
|
|
@@ -160,6 +174,13 @@ export const storefrontVersionSchema = z.object({
|
|
|
160
174
|
createdByType: z
|
|
161
175
|
.string()
|
|
162
176
|
.describe("만든 행위자 유형 (`USER`·`AGENT`·`APP`·`TOKEN`·`SYSTEM`). 늘어날 수 있다"),
|
|
177
|
+
createdVia: z
|
|
178
|
+
.string()
|
|
179
|
+
.nullable()
|
|
180
|
+
.optional()
|
|
181
|
+
.describe(
|
|
182
|
+
"만든 경로 (`CONSOLE` 콘솔 · `AI` AI 에이전트(MCP) · `CLI` `sayren` CLI · `API` 관리 API · `SYSTEM` 자동). 늘어날 수 있다",
|
|
183
|
+
),
|
|
163
184
|
createdAt: z.string().describe("만든 시각 (ISO 8601)"),
|
|
164
185
|
});
|
|
165
186
|
export type StorefrontVersion = z.infer<typeof storefrontVersionSchema>;
|
|
@@ -185,7 +206,9 @@ export const storefrontDomainSchema = z.object({
|
|
|
185
206
|
kind: z.enum(["PLATFORM", "CUSTOM"]).describe("`PLATFORM` 슬러그 주소 · `CUSTOM` 내 도메인"),
|
|
186
207
|
status: z
|
|
187
208
|
.enum(["ACTIVE", "HELD"])
|
|
188
|
-
.describe(
|
|
209
|
+
.describe(
|
|
210
|
+
"`ACTIVE` 사용 중 · `HELD` 슬러그를 바꿔 보류 중(새 주소로 301). 연결 중인 내 도메인은 싣지 않는다(`GET /v1/storefront/site/domains`)",
|
|
211
|
+
),
|
|
189
212
|
isPrimary: z.boolean().describe("대표 주소인가 (`Store.storefrontUrl`)"),
|
|
190
213
|
holdUntil: z.string().nullable().describe("보류가 끝나는 시각 (ISO 8601). 보류가 아니면 null"),
|
|
191
214
|
});
|
|
@@ -221,7 +244,7 @@ export const storefrontEditPreviewSchema = z.object({
|
|
|
221
244
|
export type StorefrontEditPreview = z.infer<typeof storefrontEditPreviewSchema>;
|
|
222
245
|
|
|
223
246
|
/**
|
|
224
|
-
* 사이트 정지 — 운영 정책에 따라 플랫폼 운영자가 사이트를 정지한다. 정지 중에는 방문자에게 정지 안내가 보이고
|
|
247
|
+
* 사이트 정지 — 운영 정책에 따라 플랫폼 운영자가 사이트를 정지한다. 정지 중에는 방문자에게 정지 안내가 보이고 발행·되돌리기·
|
|
225
248
|
* 공개 전환·슬러그 변경·끄기·편집이 409 `STOREFRONT_SITE_NOT_ACTIVE`다. 셀러가 풀 수 없다(고객센터로 문의한다).
|
|
226
249
|
*/
|
|
227
250
|
export const storefrontSuspensionSchema = z.object({
|
|
@@ -280,6 +303,50 @@ export const storefrontSlugAvailabilitySchema = z.object({
|
|
|
280
303
|
});
|
|
281
304
|
export type StorefrontSlugAvailability = z.infer<typeof storefrontSlugAvailabilitySchema>;
|
|
282
305
|
|
|
306
|
+
/**
|
|
307
|
+
* 배포 진행 — 사이트의 지금 발행 상태가 공개 주소에 반영되기까지의 단계와 기록이다(콘솔의 스토어프론트 만들기 3단계가 폴링한다).
|
|
308
|
+
* 만들기(`POST /v1/storefront/site`)는 곧바로 201로 답하고 반영은 그 뒤에 비동기로 하므로, 끝났는지는 이 조회로 본다.
|
|
309
|
+
* 템플릿 사이트는 미리 빌드한 템플릿 산출물을 올리므로 빌드 단계가 없다. 기록은 사이트 상태에서 만든 요약이다.
|
|
310
|
+
*/
|
|
311
|
+
export const storefrontDeploymentStepSchema = z.object({
|
|
312
|
+
key: z
|
|
313
|
+
.enum(["SITE", "UPLOAD", "PUBLISH"])
|
|
314
|
+
.describe("`SITE` 사이트·첫 버전 기록 · `UPLOAD` 버전 올리기 · `PUBLISH` 주소 연결·발행"),
|
|
315
|
+
status: z.enum(["PENDING", "RUNNING", "DONE", "FAILED"]),
|
|
316
|
+
finishedAt: z.string().nullable().describe("끝난 시각 (ISO 8601). 모르거나 끝나지 않았으면 null"),
|
|
317
|
+
});
|
|
318
|
+
export type StorefrontDeploymentStep = z.infer<typeof storefrontDeploymentStepSchema>;
|
|
319
|
+
|
|
320
|
+
export const storefrontDeploymentLogSchema = z.object({
|
|
321
|
+
at: z.string().nullable().describe("기록 시각 (ISO 8601). 모르면 null"),
|
|
322
|
+
level: z.enum(["INFO", "WARN", "ERROR"]),
|
|
323
|
+
code: z
|
|
324
|
+
.string()
|
|
325
|
+
.describe(
|
|
326
|
+
"기록 종류 (`SITE_CREATED`·`ADDRESS_RESERVED`·`WAITING`·`SYNCING`·`VERSION_UPLOADED`·`SYNC_FAILED`·`RETRY_SCHEDULED`·`PUBLISHED`)",
|
|
327
|
+
),
|
|
328
|
+
message: z.string().describe("사람이 읽는 문장"),
|
|
329
|
+
});
|
|
330
|
+
export type StorefrontDeploymentLog = z.infer<typeof storefrontDeploymentLogSchema>;
|
|
331
|
+
|
|
332
|
+
export const storefrontDeploymentSchema = z.object({
|
|
333
|
+
siteId: z.string(),
|
|
334
|
+
status: z
|
|
335
|
+
.enum(["IN_PROGRESS", "SUCCEEDED", "FAILED"])
|
|
336
|
+
.describe(
|
|
337
|
+
"`IN_PROGRESS` 반영 중 · `SUCCEEDED` 공개 주소에 반영됨 · `FAILED` 여러 번 실패했거나 다음 자동 시도가 멀다(`POST …/deployment/retry`로 곧바로 다시 시도한다)",
|
|
338
|
+
),
|
|
339
|
+
url: z.string().describe("사이트 기본 주소 (`https://{slug}.{호스팅 도메인}`)"),
|
|
340
|
+
startedAt: z.string().describe("사이트를 만든 시각 (ISO 8601)"),
|
|
341
|
+
finishedAt: z.string().nullable().describe("반영을 마친 시각 (ISO 8601)"),
|
|
342
|
+
steps: z.array(storefrontDeploymentStepSchema),
|
|
343
|
+
log: z.array(storefrontDeploymentLogSchema).describe("오래된 것부터"),
|
|
344
|
+
errorCode: z.string().nullable().describe("마지막 반영 실패 코드"),
|
|
345
|
+
attempts: z.number().int().describe("연속 실패 횟수"),
|
|
346
|
+
nextAttemptAt: z.string().nullable().describe("다음 자동 시도 시각 (ISO 8601)"),
|
|
347
|
+
});
|
|
348
|
+
export type StorefrontDeployment = z.infer<typeof storefrontDeploymentSchema>;
|
|
349
|
+
|
|
283
350
|
// ─── 요청 ─────────────────────────────────────────────────
|
|
284
351
|
|
|
285
352
|
export const createStorefrontSiteRequestSchema = z.object({
|
|
@@ -288,6 +355,7 @@ export const createStorefrontSiteRequestSchema = z.object({
|
|
|
288
355
|
});
|
|
289
356
|
export type CreateStorefrontSiteRequest = z.infer<typeof createStorefrontSiteRequestSchema>;
|
|
290
357
|
|
|
358
|
+
/** @deprecated 템플릿 변경은 지원하지 않는다(항상 409 `TEMPLATE_CHANGE_NOT_SUPPORTED`). 호환을 위해 스키마만 남겼다 */
|
|
291
359
|
export const changeStorefrontTemplateRequestSchema = z.object({
|
|
292
360
|
templateId: z.string().min(1).max(64).describe("바꿀 템플릿 id"),
|
|
293
361
|
publish: z
|
|
@@ -296,6 +364,7 @@ export const changeStorefrontTemplateRequestSchema = z.object({
|
|
|
296
364
|
.describe("새 버전을 곧바로 발행하는가 (기본 true). false면 버전만 만든다"),
|
|
297
365
|
message: z.string().max(200).optional().describe("버전 메모"),
|
|
298
366
|
});
|
|
367
|
+
/** @deprecated 템플릿 변경은 지원하지 않는다 */
|
|
299
368
|
export type ChangeStorefrontTemplateRequest = z.infer<typeof changeStorefrontTemplateRequestSchema>;
|
|
300
369
|
|
|
301
370
|
/**
|
|
@@ -359,6 +428,264 @@ export const storefrontPreviewLinkSchema = z.object({
|
|
|
359
428
|
});
|
|
360
429
|
export type StorefrontPreviewLink = z.infer<typeof storefrontPreviewLinkSchema>;
|
|
361
430
|
|
|
431
|
+
// ─── 내 도메인 (M3) ───────────────────────────────────────
|
|
432
|
+
//
|
|
433
|
+
// 셀러가 가진 도메인(예: `shop.brand.com`)을 사이트 주소로 연결한다. 흐름은 추가(`PENDING_DNS`) → 셀러가 DNS에 TXT(소유 확인)·
|
|
434
|
+
// CNAME(연결)을 넣음 → 서버가 TXT를 확인하면 도메인을 이 스토어에 잡고 인증서 발급을 시작한다(`PENDING_VALIDATION`) → 인증서가
|
|
435
|
+
// 발급되면 `ACTIVE`다. 확인은 서버가 주기적으로 하고 `verify`는 곧바로 한 번 더 본다. 연결된 도메인은 대표 주소로 지정할 수 있고,
|
|
436
|
+
// 대표가 아닌 주소(기본 주소 포함)는 대표 주소로 301한다.
|
|
437
|
+
//
|
|
438
|
+
// - TXT 소유 확인은 도메인마다 새 값이다. CNAME만으로는 연결하지 않는다 — 연결을 끊은 뒤 CNAME이 남은 도메인을 다른 스토어가
|
|
439
|
+
// 가져가지 못하게 한다.
|
|
440
|
+
// - 소유 확인 전(`PENDING_DNS`)에는 도메인을 잡지 않는다. 같은 도메인을 여러 스토어가 추가해 둘 수 있고, 먼저 TXT 확인을 통과한
|
|
441
|
+
// 스토어가 잡는다. 이미 다른 스토어가 잡은 도메인은 409 `STOREFRONT_DOMAIN_TAKEN`이다.
|
|
442
|
+
// - 해제하면 곧바로 연결을 지운다. 연결된 뒤 DNS가 7일 넘게 이 서비스를 가리키지 않으면 서버가 연결을 해제한다.
|
|
443
|
+
// - 결제 복귀·방문 분석·소셜 로그인 허용 목록은 연결된(`ACTIVE`) 도메인을 자동으로 더한다.
|
|
444
|
+
//
|
|
445
|
+
// 오류 코드:
|
|
446
|
+
// - 403 `STOREFRONT_CUSTOM_DOMAINS_DISABLED` — 플랫폼 스위치가 꺼져 도메인을 추가할 수 없다
|
|
447
|
+
// - 400 `STOREFRONT_DOMAIN_INVALID` — 형식이 틀렸거나 연결할 수 없는 도메인이다(`error.details.reason`)
|
|
448
|
+
// - 404 `STOREFRONT_DOMAIN_NOT_FOUND`
|
|
449
|
+
// - 409 `STOREFRONT_DOMAIN_EXISTS` — 이 스토어에 이미 추가한 도메인이다
|
|
450
|
+
// - 409 `STOREFRONT_DOMAIN_TAKEN` — 다른 스토어가 연결했거나 연결 중인 도메인이다
|
|
451
|
+
// - 409 `STOREFRONT_DOMAIN_LIMIT` — 스토어당 도메인 수 상한(`error.details.limit`)
|
|
452
|
+
// - 409 `STOREFRONT_DOMAIN_NOT_ACTIVE` — 연결되지 않았거나 경고(`failureCode`)가 있는 도메인은 대표 주소로 지정할 수 없다
|
|
453
|
+
// - 429 `TOO_MANY_REQUESTS` — 재확인은 스토어당 분당 20번까지다(`error.details.retryAfterSeconds`)
|
|
454
|
+
// - 400 `STOREFRONT_DOMAIN_NOT_REMOVABLE` — 기본 주소는 해제할 수 없다
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 연결할 수 없는 도메인 — 플랫폼이 쓰는 도메인과 그 하위(기본 주소는 슬러그로 바꾼다). 호스팅 도메인은 서버 설정이라
|
|
458
|
+
* `storefrontCustomHostnameProblem`의 인자로 따로 받는다.
|
|
459
|
+
*/
|
|
460
|
+
export const STOREFRONT_PLATFORM_DOMAINS: readonly string[] = [
|
|
461
|
+
"sayren.co",
|
|
462
|
+
"sayren.app",
|
|
463
|
+
"sayren.shop",
|
|
464
|
+
"avarlabs.com",
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
/** 공개 인터넷에서 쓸 수 없는 최상위 도메인(RFC 2606·6761·6762 등) */
|
|
468
|
+
const NON_PUBLIC_TLDS: readonly string[] = [
|
|
469
|
+
"example",
|
|
470
|
+
"localhost",
|
|
471
|
+
"local",
|
|
472
|
+
"test",
|
|
473
|
+
"invalid",
|
|
474
|
+
"onion",
|
|
475
|
+
"internal",
|
|
476
|
+
"arpa",
|
|
477
|
+
"home",
|
|
478
|
+
"lan",
|
|
479
|
+
"corp",
|
|
480
|
+
"intranet",
|
|
481
|
+
"alt",
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 공용 접미사 — 누구나 그 아래 이름을 받는 도메인이라 접미사 자체는 연결할 수 없다. Public Suffix List 전체가 아니라 자주 틀리는
|
|
486
|
+
* 것만 둔다(여기 없는 접미사도 TXT 소유 확인을 통과할 수 없어 연결되지 않는다 — 이 목록은 추가 단계에서 빨리 알려 주는 안내다).
|
|
487
|
+
*/
|
|
488
|
+
export const STOREFRONT_PUBLIC_SUFFIXES: readonly string[] = [
|
|
489
|
+
"co.kr",
|
|
490
|
+
"or.kr",
|
|
491
|
+
"ne.kr",
|
|
492
|
+
"go.kr",
|
|
493
|
+
"ac.kr",
|
|
494
|
+
"re.kr",
|
|
495
|
+
"pe.kr",
|
|
496
|
+
"mil.kr",
|
|
497
|
+
"hs.kr",
|
|
498
|
+
"ms.kr",
|
|
499
|
+
"es.kr",
|
|
500
|
+
"sc.kr",
|
|
501
|
+
"kg.kr",
|
|
502
|
+
"seoul.kr",
|
|
503
|
+
"busan.kr",
|
|
504
|
+
"incheon.kr",
|
|
505
|
+
"daegu.kr",
|
|
506
|
+
"gwangju.kr",
|
|
507
|
+
"daejeon.kr",
|
|
508
|
+
"ulsan.kr",
|
|
509
|
+
"gyeonggi.kr",
|
|
510
|
+
"gangwon.kr",
|
|
511
|
+
"chungbuk.kr",
|
|
512
|
+
"chungnam.kr",
|
|
513
|
+
"jeonbuk.kr",
|
|
514
|
+
"jeonnam.kr",
|
|
515
|
+
"gyeongbuk.kr",
|
|
516
|
+
"gyeongnam.kr",
|
|
517
|
+
"jeju.kr",
|
|
518
|
+
"co.jp",
|
|
519
|
+
"ne.jp",
|
|
520
|
+
"or.jp",
|
|
521
|
+
"com.cn",
|
|
522
|
+
"net.cn",
|
|
523
|
+
"com.tw",
|
|
524
|
+
"com.hk",
|
|
525
|
+
"co.uk",
|
|
526
|
+
"org.uk",
|
|
527
|
+
"com.au",
|
|
528
|
+
"net.au",
|
|
529
|
+
"com.sg",
|
|
530
|
+
"co.nz",
|
|
531
|
+
"com.br",
|
|
532
|
+
"github.io",
|
|
533
|
+
"gitlab.io",
|
|
534
|
+
"pages.dev",
|
|
535
|
+
"workers.dev",
|
|
536
|
+
"vercel.app",
|
|
537
|
+
"netlify.app",
|
|
538
|
+
"herokuapp.com",
|
|
539
|
+
"web.app",
|
|
540
|
+
"firebaseapp.com",
|
|
541
|
+
"blogspot.com",
|
|
542
|
+
"cloudfront.net",
|
|
543
|
+
"azurewebsites.net",
|
|
544
|
+
"myshopify.com",
|
|
545
|
+
"wixsite.com",
|
|
546
|
+
"cafe24.com",
|
|
547
|
+
"imweb.me",
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
/** 스토어당 연결할 수 있는 도메인 수 기본값 — 요금제(#44) 전의 상한이다. 실제 값은 `GET …/domains`의 `limit` */
|
|
551
|
+
export const STOREFRONT_CUSTOM_DOMAIN_LIMIT_DEFAULT = 2;
|
|
552
|
+
/** DNS를 넣을 때까지 기다리는 기간(일) — 지나면 `FAILED`(`DNS_NOT_CONFIGURED`) */
|
|
553
|
+
export const STOREFRONT_DOMAIN_DNS_WAIT_DAYS = 7;
|
|
554
|
+
/** 연결된 도메인의 DNS가 이 서비스를 가리키지 않아도 연결을 유지하는 기간(일) — 지나면 서버가 해제한다 */
|
|
555
|
+
export const STOREFRONT_DOMAIN_DNS_GRACE_DAYS = 7;
|
|
556
|
+
/** 소유 확인 TXT 레코드의 이름 앞부분 — `_sayren-verify.{도메인}` */
|
|
557
|
+
export const STOREFRONT_DOMAIN_TXT_LABEL = "_sayren-verify";
|
|
558
|
+
|
|
559
|
+
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
560
|
+
const PUNYCODE_TLD = /^xn--[a-z0-9-]{1,59}$/;
|
|
561
|
+
const ALPHA_TLD = /^[a-z]{2,63}$/;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 도메인 정규화 — 앞뒤 공백·끝 점을 떼고 소문자로 바꾸며 국제화 도메인은 punycode(`xn--`)로 바꾼다. 스킴·경로·포트가 붙은 입력
|
|
565
|
+
* (`https://www.a.com/`)은 호스트만 남기지 않고 그대로 두어 형식 검사에서 떨어진다(서버는 사용자가 적은 도메인만 받는다).
|
|
566
|
+
* 변환할 수 없으면 입력을 소문자로만 바꿔 돌려준다(형식 검사가 거부한다).
|
|
567
|
+
*/
|
|
568
|
+
export function normalizeStorefrontHostname(input: string): string {
|
|
569
|
+
const trimmed = input.trim().replace(/\.+$/, "");
|
|
570
|
+
if (!trimmed || /[\s/:?#@\\[\]%]/.test(trimmed)) return trimmed.toLowerCase();
|
|
571
|
+
if (/^[\x21-\x7e]+$/.test(trimmed)) return trimmed.toLowerCase();
|
|
572
|
+
try {
|
|
573
|
+
// WHATWG URL이 IDNA(UTS #46)로 punycode를 만든다. 브라우저·Node·Workers가 같은 결과다
|
|
574
|
+
return new URL(`http://${trimmed}`).hostname.toLowerCase();
|
|
575
|
+
} catch {
|
|
576
|
+
return trimmed.toLowerCase();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export type StorefrontHostnameProblem =
|
|
581
|
+
| "FORMAT"
|
|
582
|
+
| "IP_ADDRESS"
|
|
583
|
+
| "SINGLE_LABEL"
|
|
584
|
+
| "NON_PUBLIC_TLD"
|
|
585
|
+
| "PLATFORM_DOMAIN"
|
|
586
|
+
| "PUBLIC_SUFFIX"
|
|
587
|
+
| "WILDCARD";
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* 연결할 수 있는 도메인인가(정규화한 값 기준). 문제가 없으면 null이다. 규칙: 253자 이하, 라벨 2개 이상, 라벨은 소문자·숫자·하이픈
|
|
591
|
+
* 63자 이하(하이픈으로 시작·끝 불가), 최상위 도메인은 영문자이거나 punycode, IP 주소·와일드카드·공개 인터넷 밖 최상위 도메인·
|
|
592
|
+
* 플랫폼 도메인과 그 하위·공용 접미사 자체는 안 된다. 제어·디스패치 Worker가 형식 규칙의 복제본을 들고 있다(api
|
|
593
|
+
* `test/storefront-hosting-parity.spec.ts`가 대조한다).
|
|
594
|
+
*/
|
|
595
|
+
export function storefrontCustomHostnameProblem(
|
|
596
|
+
hostname: string,
|
|
597
|
+
hostingDomain = "sayren.co",
|
|
598
|
+
): StorefrontHostnameProblem | null {
|
|
599
|
+
if (hostname.startsWith("*.")) return "WILDCARD";
|
|
600
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) || /^\[?[0-9a-f:.]*:[0-9a-f:.]*\]?$/.test(hostname))
|
|
601
|
+
return "IP_ADDRESS";
|
|
602
|
+
if (hostname.length === 0 || hostname.length > 253) return "FORMAT";
|
|
603
|
+
const labels = hostname.split(".");
|
|
604
|
+
if (labels.length < 2) return "SINGLE_LABEL";
|
|
605
|
+
if (!labels.every((label) => LABEL.test(label))) return "FORMAT";
|
|
606
|
+
const tld = labels[labels.length - 1] ?? "";
|
|
607
|
+
if (!ALPHA_TLD.test(tld) && !PUNYCODE_TLD.test(tld)) return "FORMAT";
|
|
608
|
+
if (NON_PUBLIC_TLDS.includes(tld)) return "NON_PUBLIC_TLD";
|
|
609
|
+
const platform = new Set([...STOREFRONT_PLATFORM_DOMAINS, hostingDomain.toLowerCase()]);
|
|
610
|
+
for (const domain of platform) {
|
|
611
|
+
if (hostname === domain || hostname.endsWith(`.${domain}`)) return "PLATFORM_DOMAIN";
|
|
612
|
+
}
|
|
613
|
+
if (STOREFRONT_PUBLIC_SUFFIXES.includes(hostname)) return "PUBLIC_SUFFIX";
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** 소유 확인 TXT 레코드의 전체 이름 */
|
|
618
|
+
export function storefrontDomainTxtNameOf(hostname: string): string {
|
|
619
|
+
return `${STOREFRONT_DOMAIN_TXT_LABEL}.${hostname}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export const storefrontCustomDomainStatusSchema = z
|
|
623
|
+
.enum(["PENDING_DNS", "PENDING_VALIDATION", "ACTIVE", "FAILED"])
|
|
624
|
+
.describe(
|
|
625
|
+
"`PENDING_DNS` DNS 설정 대기(소유 확인 전) · `PENDING_VALIDATION` 소유 확인됨, 인증서 발급 중 · `ACTIVE` 연결됨 · `FAILED` 연결 실패(`failureCode`)",
|
|
626
|
+
);
|
|
627
|
+
export type StorefrontCustomDomainStatus = z.infer<typeof storefrontCustomDomainStatusSchema>;
|
|
628
|
+
|
|
629
|
+
export const storefrontDnsRecordSchema = z.object({
|
|
630
|
+
type: z.enum(["CNAME", "TXT"]).describe("레코드 유형"),
|
|
631
|
+
name: z.string().describe("레코드 이름 (전체 이름, 예: `_sayren-verify.shop.brand.com`)"),
|
|
632
|
+
value: z.string().describe("레코드 값"),
|
|
633
|
+
purpose: z.enum(["ROUTING", "OWNERSHIP"]).describe("`ROUTING` 연결 · `OWNERSHIP` 소유 확인"),
|
|
634
|
+
});
|
|
635
|
+
export type StorefrontDnsRecord = z.infer<typeof storefrontDnsRecordSchema>;
|
|
636
|
+
|
|
637
|
+
export const storefrontSiteDomainSchema = z.object({
|
|
638
|
+
domainId: z.string(),
|
|
639
|
+
hostname: z.string().describe("호스트 이름 (punycode, 소문자)"),
|
|
640
|
+
kind: z
|
|
641
|
+
.enum(["PLATFORM", "CUSTOM"])
|
|
642
|
+
.describe("`PLATFORM` 기본 주소(슬러그) · `CUSTOM` 내 도메인"),
|
|
643
|
+
status: storefrontCustomDomainStatusSchema.describe(
|
|
644
|
+
"기본 주소는 늘 `ACTIVE`다. 내 도메인은 연결 단계",
|
|
645
|
+
),
|
|
646
|
+
isPrimary: z
|
|
647
|
+
.boolean()
|
|
648
|
+
.describe("대표 주소인가 (`Store.storefrontUrl`). 대표가 아닌 주소는 대표로 301한다"),
|
|
649
|
+
dnsRecords: z.array(storefrontDnsRecordSchema).describe("DNS에 넣을 레코드. 기본 주소는 빈 배열"),
|
|
650
|
+
certificateStatus: z
|
|
651
|
+
.enum(["PENDING", "ACTIVE", "FAILED"])
|
|
652
|
+
.nullable()
|
|
653
|
+
.describe("인증서 상태. 소유 확인 전·기본 주소는 null"),
|
|
654
|
+
failureCode: z
|
|
655
|
+
.string()
|
|
656
|
+
.nullable()
|
|
657
|
+
.describe(
|
|
658
|
+
"실패·경고 코드. `DNS_NOT_CONFIGURED` 기한 안에 TXT를 찾지 못함 · `VALIDATION_TIMEOUT` 인증서 발급 기한 지남 · `CERTIFICATE_FAILED` 인증서 발급 실패 · `DNS_NOT_POINTING` 연결된 뒤 DNS가 이 서비스를 가리키지 않음(기한이 지나면 해제) · `DOMAIN_TAKEN` 다른 스토어가 먼저 소유를 확인함 · `HOSTNAME_BLOCKED` 연결이 막힘 · `HOSTNAME_CONFLICT` 연결 준비 중 충돌(운영 확인). 늘어날 수 있다",
|
|
659
|
+
),
|
|
660
|
+
verifiedAt: z.string().nullable().describe("소유를 확인한 시각 (ISO 8601)"),
|
|
661
|
+
activatedAt: z.string().nullable().describe("연결된 시각 (ISO 8601)"),
|
|
662
|
+
checkedAt: z.string().nullable().describe("마지막으로 확인한 시각 (ISO 8601)"),
|
|
663
|
+
createdAt: z.string(),
|
|
664
|
+
});
|
|
665
|
+
export type StorefrontSiteDomain = z.infer<typeof storefrontSiteDomainSchema>;
|
|
666
|
+
|
|
667
|
+
/** `GET /v1/storefront/site/domains` — 사이트의 주소 목록(기본 주소 + 내 도메인)과 연결 설정 */
|
|
668
|
+
export const storefrontSiteDomainsSchema = z.object({
|
|
669
|
+
enabled: z.boolean().describe("내 도메인을 추가할 수 있는가 (플랫폼 스위치)"),
|
|
670
|
+
limit: z
|
|
671
|
+
.number()
|
|
672
|
+
.int()
|
|
673
|
+
.min(0)
|
|
674
|
+
.describe("스토어당 내 도메인 수 상한 (해제한 도메인은 세지 않는다)"),
|
|
675
|
+
target: z.string().describe("CNAME 대상 (예: `sites.sayren.co`)"),
|
|
676
|
+
domains: z.array(storefrontSiteDomainSchema).describe("대표 주소 먼저, 그다음 추가한 순서"),
|
|
677
|
+
});
|
|
678
|
+
export type StorefrontSiteDomains = z.infer<typeof storefrontSiteDomainsSchema>;
|
|
679
|
+
|
|
680
|
+
export const addStorefrontDomainRequestSchema = z.object({
|
|
681
|
+
hostname: z
|
|
682
|
+
.string()
|
|
683
|
+
.min(1)
|
|
684
|
+
.max(300)
|
|
685
|
+
.describe("연결할 도메인 (예: `shop.brand.com`). 서버가 소문자·punycode로 정규화한다"),
|
|
686
|
+
});
|
|
687
|
+
export type AddStorefrontDomainRequest = z.infer<typeof addStorefrontDomainRequestSchema>;
|
|
688
|
+
|
|
362
689
|
// ─── 편집 (M2b) ───────────────────────────────────────────
|
|
363
690
|
//
|
|
364
691
|
// 셀러(콘솔)와 에이전트(MCP)가 사이트의 작업 트리를 고치고 버전을 만든다. 작업 트리는 사이트당 하나이고 편집 Sandbox
|
|
@@ -723,7 +1050,7 @@ export const storefrontRuleResultSchema = z.object({
|
|
|
723
1050
|
export type StorefrontRuleResult = z.infer<typeof storefrontRuleResultSchema>;
|
|
724
1051
|
|
|
725
1052
|
/**
|
|
726
|
-
* 규칙 검증 보고서 — `@sayren/mcp`의 `verify_storefront`와 같은 모양이다(원천 `
|
|
1053
|
+
* 규칙 검증 보고서 — `@sayren/mcp`의 `verify_storefront`와 같은 모양이다(원천 `packages/mcp/src/verify.ts`). 위반마다
|
|
727
1054
|
* 고칠 자리(`file`·`line`)와 방법(`fix`·`example`)을 싣는다. `POST /v1/storefront/site/verify`(편집 환경이 잠들어 있어도
|
|
728
1055
|
* 된다)와 빌드의 `verify`가 이 모양이다.
|
|
729
1056
|
*/
|
|
@@ -776,9 +1103,11 @@ export const STOREFRONT_BUILD_TERMINAL_STATUSES: readonly StorefrontBuildStatus[
|
|
|
776
1103
|
|
|
777
1104
|
/**
|
|
778
1105
|
* 빌드 단계 — 이 순서로 돈다. 첫 실패에서 멈추지만 규칙 검증(`VERIFY`)은 실패해도 타입 검사까지 이어서 해 한 번에 알려 준다.
|
|
779
|
-
* `UPLOAD`는 사이트 버전 Worker 업로드다(성공하면 발행할 수 있다).
|
|
1106
|
+
* `UPLOAD`는 사이트 버전 Worker 업로드다(성공하면 발행할 수 있다). `DEPENDENCIES`는 빌드 시작 전 의존성 검사(거절 목록·보안 권고)다.
|
|
1107
|
+
* 값은 늘어날 수 있다.
|
|
780
1108
|
*/
|
|
781
1109
|
export const STOREFRONT_BUILD_STAGES = [
|
|
1110
|
+
"DEPENDENCIES",
|
|
782
1111
|
"VERIFY",
|
|
783
1112
|
"RESTORE",
|
|
784
1113
|
"INSTALL",
|
|
@@ -789,6 +1118,40 @@ export const STOREFRONT_BUILD_STAGES = [
|
|
|
789
1118
|
"UPLOAD",
|
|
790
1119
|
] as const;
|
|
791
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* 의존성 검사 결과 한 줄(#61) — 거절(`DEPENDENCY_REJECTED`의 `details.rejected`)·경고(빌드 `dependencies`, 소스 교체 `dependencyWarnings`)가
|
|
1123
|
+
* 같은 모양이다. 대상 패키지와 사유. 셀러가 고칠 수 있게 버전·권고 id·거절 사유를 싣는다.
|
|
1124
|
+
* `reason`: `DENYLISTED`(거절 목록) · `ADVISORY`(보안 권고) · `MALWARE`(악성 코드 권고, 패키지 전체) · `INSTALL_SCRIPT`(루트
|
|
1125
|
+
* package.json의 설치 훅·의존성의 설치 스크립트, 경고) · `INSTALL_CONFIG`(설치 방식 필드, 경고) · `UNSUPPORTED_SOURCE`(레지스트리 밖 설치 경로) · `BUNDLED_DEPENDENCIES`(묶여
|
|
1126
|
+
* 오는 의존성, 경고) · `INVALID_MANIFEST`(package.json·잠금 파일 형식), 늘어날 수 있다.
|
|
1127
|
+
*/
|
|
1128
|
+
export const storefrontDependencyFindingSchema = z.object({
|
|
1129
|
+
name: z.string().describe("패키지 이름. 파일 전체의 문제면 `package.json`·`pnpm-lock.yaml`"),
|
|
1130
|
+
version: z.string().nullable().describe("해당 버전. 버전과 무관하면 null"),
|
|
1131
|
+
reason: z.string().describe("사유 코드"),
|
|
1132
|
+
detail: z.string().describe("셀러용 설명"),
|
|
1133
|
+
advisory: z
|
|
1134
|
+
.object({
|
|
1135
|
+
id: z.string(),
|
|
1136
|
+
url: z.string().nullable(),
|
|
1137
|
+
severity: z.string(),
|
|
1138
|
+
title: z.string(),
|
|
1139
|
+
})
|
|
1140
|
+
.nullable()
|
|
1141
|
+
.optional()
|
|
1142
|
+
.describe("보안 권고(사유가 `ADVISORY`·`MALWARE`일 때)"),
|
|
1143
|
+
});
|
|
1144
|
+
export type StorefrontDependencyFinding = z.infer<typeof storefrontDependencyFindingSchema>;
|
|
1145
|
+
|
|
1146
|
+
export const storefrontBuildDependenciesSchema = z.object({
|
|
1147
|
+
checkedPackages: z.number().int().min(0).describe("검사한 설치 트리의 패키지(이름·버전) 수"),
|
|
1148
|
+
rejected: z.array(storefrontDependencyFindingSchema).describe("빌드를 막은 의존성"),
|
|
1149
|
+
warnings: z
|
|
1150
|
+
.array(storefrontDependencyFindingSchema)
|
|
1151
|
+
.describe("빌드는 막지 않는 경고(중간 이하 심각도 권고)"),
|
|
1152
|
+
});
|
|
1153
|
+
export type StorefrontBuildDependencies = z.infer<typeof storefrontBuildDependenciesSchema>;
|
|
1154
|
+
|
|
792
1155
|
export const storefrontBuildSchema = z.object({
|
|
793
1156
|
buildId: z.string(),
|
|
794
1157
|
versionId: z.string(),
|
|
@@ -798,13 +1161,13 @@ export const storefrontBuildSchema = z.object({
|
|
|
798
1161
|
.string()
|
|
799
1162
|
.nullable()
|
|
800
1163
|
.describe(
|
|
801
|
-
"실패한 단계 (`VERIFY`·`RESTORE`·`INSTALL`·`TYPECHECK`·`BUILD`·`BUNDLE`·`PACK`·`UPLOAD`, 늘어날 수 있다). 실패가 아니면 null",
|
|
1164
|
+
"실패한 단계 (`DEPENDENCIES`·`VERIFY`·`RESTORE`·`INSTALL`·`TYPECHECK`·`BUILD`·`BUNDLE`·`PACK`·`UPLOAD`, 늘어날 수 있다). 실패가 아니면 null",
|
|
802
1165
|
),
|
|
803
1166
|
errorCode: z
|
|
804
1167
|
.string()
|
|
805
1168
|
.nullable()
|
|
806
1169
|
.describe(
|
|
807
|
-
"실패 코드 (`RULES_VIOLATED`·`INSTALL_FAILED`·`TYPECHECK_FAILED`·`BUILD_FAILED`·`BUNDLE_FAILED`·`ARTIFACT_INVALID`·`UPLOAD_FAILED`·`SOURCE_NOT_FOUND`·`CAPACITY_EXHAUSTED`·`INTERNAL`, 늘어날 수 있다). 실패가 아니면 null",
|
|
1170
|
+
"실패 코드 (`DEPENDENCY_REJECTED`·`DEPENDENCY_CHECK_UNAVAILABLE`·`DEPENDENCIES_CHANGED`·`RULES_VIOLATED`·`INSTALL_FAILED`·`INSTALL_TOO_LARGE`·`TYPECHECK_FAILED`·`BUILD_FAILED`·`BUILD_TIMEOUT`·`BUNDLE_FAILED`·`ARTIFACT_TOO_LARGE`·`ARTIFACT_INVALID`·`UPLOAD_FAILED`·`SOURCE_NOT_FOUND`·`CAPACITY_EXHAUSTED`·`INTERNAL`, 늘어날 수 있다). 실패가 아니면 null",
|
|
808
1171
|
),
|
|
809
1172
|
logTail: z.string().nullable().describe("실패 단계 출력 꼬리 (최대 8KB). 없으면 null"),
|
|
810
1173
|
stages: z
|
|
@@ -819,6 +1182,13 @@ export const storefrontBuildSchema = z.object({
|
|
|
819
1182
|
verify: storefrontVerifyReportSchema
|
|
820
1183
|
.nullable()
|
|
821
1184
|
.describe("규칙 검증 보고서(위반 상세는 50건까지). 그 단계까지 가지 못했으면 null"),
|
|
1185
|
+
// 옛 api 응답에는 없다 — 새 SDK가 배포 전 api와 함께 쓰여도 깨지지 않게 선택 필드다
|
|
1186
|
+
dependencies: storefrontBuildDependenciesSchema
|
|
1187
|
+
.nullable()
|
|
1188
|
+
.optional()
|
|
1189
|
+
.describe(
|
|
1190
|
+
"의존성 검사 결과(거절 목록·보안 권고). 빌드 시작 전에 돈다. 거절이 있으면 `failureStage: DEPENDENCIES`, `errorCode: DEPENDENCY_REJECTED`다. 아직 검사 전이면 null(옛 서버는 싣지 않는다)",
|
|
1191
|
+
),
|
|
822
1192
|
queuedAt: z.string(),
|
|
823
1193
|
startedAt: z.string().nullable(),
|
|
824
1194
|
finishedAt: z.string().nullable(),
|
|
@@ -836,6 +1206,13 @@ export const storefrontBuildListQuerySchema = z.object({
|
|
|
836
1206
|
*/
|
|
837
1207
|
export const createStorefrontVersionRequestSchema = z.object({
|
|
838
1208
|
message: z.string().max(200).optional().describe("버전 메모 (에이전트는 바꾼 내용 요약)"),
|
|
1209
|
+
expectedSourceRef: z
|
|
1210
|
+
.string()
|
|
1211
|
+
.max(200)
|
|
1212
|
+
.optional()
|
|
1213
|
+
.describe(
|
|
1214
|
+
"고정할 작업 트리 (`PUT /v1/storefront/site/source`의 `sourceRef`). 주면 지금 작업 트리가 이 값일 때만 버전을 만든다(다르면 409 `SOURCE_CHANGED`)",
|
|
1215
|
+
),
|
|
839
1216
|
});
|
|
840
1217
|
export type CreateStorefrontVersionRequest = z.infer<typeof createStorefrontVersionRequestSchema>;
|
|
841
1218
|
|
|
@@ -854,3 +1231,142 @@ export type StorefrontVersionCreated = z.infer<typeof storefrontVersionCreatedSc
|
|
|
854
1231
|
export const createStorefrontEditPreviewLinkRequestSchema =
|
|
855
1232
|
createStorefrontPreviewLinkRequestSchema;
|
|
856
1233
|
export type CreateStorefrontEditPreviewLinkRequest = CreateStorefrontPreviewLinkRequest;
|
|
1234
|
+
|
|
1235
|
+
// ─── 소스 내려받기·올리기 (#60, `sayren` CLI) ─────────────────────────────
|
|
1236
|
+
//
|
|
1237
|
+
// 셀러가 사이트 작업 트리를 로컬로 받아(`npx sayren pull`) 고치고 되돌려 놓는다(`npx sayren deploy`). 콘솔·AI 편집과 같은
|
|
1238
|
+
// 작업 트리다. 올리는 것은 소스뿐이고 빌드는 서버가 한다(버전 만들기). 흐름:
|
|
1239
|
+
// 1. `GET source` — 지금 작업 트리를 고정하고(`sourceRef`) 압축 파일 크기를 알린다
|
|
1240
|
+
// 2. `GET source/archive?sourceRef=&part=` — 압축 파일(tar.gz)을 4MiB씩 받는다
|
|
1241
|
+
// 3. `PUT source` — 바꾼 뒤 트리 전체 목록(경로·해시·크기)과 `package.json`·`pnpm-lock.yaml` 내용을 보낸다(의존성 판정). 서버에 없는 내용은 409 `SOURCE_BLOBS_MISSING`이
|
|
1242
|
+
// 알려 주고, `POST source/blobs`로 올린 뒤 다시 보낸다. `expectedBaseRef`(내려받은 `sourceRef`)가 지금과 다르면 409 `SOURCE_CHANGED`
|
|
1243
|
+
|
|
1244
|
+
/** 압축 파일 조각 하나의 최대 크기(바이트) */
|
|
1245
|
+
export const STOREFRONT_SOURCE_PART_BYTES = 4 * 1024 * 1024;
|
|
1246
|
+
/** 내용 올리기 한 번의 최대 파일 수 */
|
|
1247
|
+
export const STOREFRONT_SOURCE_BLOB_BATCH_MAX = 200;
|
|
1248
|
+
/** 작업 트리 파일 수 상한 */
|
|
1249
|
+
export const STOREFRONT_WORKSPACE_MAX_FILES = 2000;
|
|
1250
|
+
|
|
1251
|
+
/** 작업 트리 스냅샷 키 — `sources/{siteId}/{sha256}.tgz` */
|
|
1252
|
+
export const storefrontSourceRefSchema = z
|
|
1253
|
+
.string()
|
|
1254
|
+
.regex(/^sources\/sfs_[a-z0-9]{8,40}\/[0-9a-f]{64}\.tgz$/, "작업 트리 스냅샷 키 형식이 아닙니다")
|
|
1255
|
+
.describe("작업 트리 스냅샷 (`sources/{siteId}/{sha256}.tgz`, 내용 주소)");
|
|
1256
|
+
|
|
1257
|
+
/** `GET /v1/storefront/site/source` — 지금 작업 트리(편집 환경이 떠 있으면 지금 내용을 고정한다) */
|
|
1258
|
+
export const storefrontSourceSchema = z.object({
|
|
1259
|
+
siteId: z.string(),
|
|
1260
|
+
storeId: z.string(),
|
|
1261
|
+
storeCode: z.string().describe("스토어 코드 — 로컬 개발 서버의 `SAYREN_STORE_CODE`"),
|
|
1262
|
+
sourceRef: storefrontSourceRefSchema,
|
|
1263
|
+
treeSha256: z.string().describe("트리 지문 (경로 오름차순 `{path}\\t{sha256}\\n`의 SHA-256)"),
|
|
1264
|
+
files: z.number().int().min(0).describe("파일 수"),
|
|
1265
|
+
bytes: z.number().int().min(0).describe("파일 크기 합"),
|
|
1266
|
+
archive: z.object({
|
|
1267
|
+
format: z.literal("tar+gzip"),
|
|
1268
|
+
bytes: z.number().int().min(0).describe("압축 파일 크기"),
|
|
1269
|
+
sha256: z.string().describe("압축 파일의 SHA-256 (`sourceRef`의 해시와 같다)"),
|
|
1270
|
+
parts: z.number().int().min(0).describe("`GET source/archive`로 받을 조각 수"),
|
|
1271
|
+
partBytes: z.number().int().describe("조각 하나의 최대 크기"),
|
|
1272
|
+
}),
|
|
1273
|
+
editSessionOpen: z
|
|
1274
|
+
.boolean()
|
|
1275
|
+
.describe("편집 환경이 열려 있는가 (올리기 전에 끄거나 `endEditSession`)"),
|
|
1276
|
+
});
|
|
1277
|
+
export type StorefrontSource = z.infer<typeof storefrontSourceSchema>;
|
|
1278
|
+
|
|
1279
|
+
export const storefrontSourceArchiveQuerySchema = z.object({
|
|
1280
|
+
sourceRef: storefrontSourceRefSchema,
|
|
1281
|
+
part: z.coerce.number().int().min(0).max(100).optional().describe("조각 번호 (0부터, 기본 0)"),
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
/** `GET /v1/storefront/site/source/archive` — 압축 파일 조각 */
|
|
1285
|
+
export const storefrontSourceArchivePartSchema = z.object({
|
|
1286
|
+
sourceRef: storefrontSourceRefSchema,
|
|
1287
|
+
part: z.number().int().min(0),
|
|
1288
|
+
parts: z.number().int().min(0),
|
|
1289
|
+
offset: z.number().int().min(0),
|
|
1290
|
+
bytes: z.number().int().min(0).describe("이 조각의 크기"),
|
|
1291
|
+
totalBytes: z.number().int().min(0),
|
|
1292
|
+
content: z.string().describe("조각 내용 (base64)"),
|
|
1293
|
+
});
|
|
1294
|
+
export type StorefrontSourceArchivePart = z.infer<typeof storefrontSourceArchivePartSchema>;
|
|
1295
|
+
|
|
1296
|
+
/** `POST /v1/storefront/site/source/blobs` — 파일 내용 올리기(해시 키, 다시 올려도 안전하다). 작업 트리는 바뀌지 않는다 */
|
|
1297
|
+
export const uploadStorefrontSourceBlobsRequestSchema = z.object({
|
|
1298
|
+
blobs: z
|
|
1299
|
+
.array(
|
|
1300
|
+
z.object({
|
|
1301
|
+
hash: storefrontFileHashSchema,
|
|
1302
|
+
content: z.string().describe("파일 내용 (base64, 1MiB까지)"),
|
|
1303
|
+
}),
|
|
1304
|
+
)
|
|
1305
|
+
.min(1)
|
|
1306
|
+
.max(STOREFRONT_SOURCE_BLOB_BATCH_MAX),
|
|
1307
|
+
});
|
|
1308
|
+
export type UploadStorefrontSourceBlobsRequest = z.infer<
|
|
1309
|
+
typeof uploadStorefrontSourceBlobsRequestSchema
|
|
1310
|
+
>;
|
|
1311
|
+
|
|
1312
|
+
export const storefrontSourceBlobsResultSchema = z.object({
|
|
1313
|
+
stored: z.number().int().min(0).describe("새로 올린 수"),
|
|
1314
|
+
existing: z.number().int().min(0).describe("이미 있던 수"),
|
|
1315
|
+
});
|
|
1316
|
+
export type StorefrontSourceBlobsResult = z.infer<typeof storefrontSourceBlobsResultSchema>;
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* `PUT /v1/storefront/site/source` — 작업 트리 전체를 이 목록으로 바꾼다(없는 경로는 지운다). 전부 아니면 전무다.
|
|
1320
|
+
* `package.json`·`pnpm-lock.yaml`도 바꿀 수 있고 의존성은 허용 목록 안이어야 한다. `.env*`·작업 트리 밖 경로는 받지 않는다.
|
|
1321
|
+
*/
|
|
1322
|
+
export const replaceStorefrontSourceRequestSchema = z.object({
|
|
1323
|
+
expectedBaseRef: storefrontSourceRefSchema
|
|
1324
|
+
.optional()
|
|
1325
|
+
.describe(
|
|
1326
|
+
"내려받은 작업 트리. 지금 작업 트리가 이 값일 때만 바꾼다. 생략하면 조건 없이 덮어쓴다",
|
|
1327
|
+
),
|
|
1328
|
+
files: z
|
|
1329
|
+
.array(
|
|
1330
|
+
z.object({
|
|
1331
|
+
path: storefrontFilePathSchema,
|
|
1332
|
+
hash: storefrontFileHashSchema,
|
|
1333
|
+
size: z.number().int().min(0).max(STOREFRONT_FILE_MAX_BYTES),
|
|
1334
|
+
}),
|
|
1335
|
+
)
|
|
1336
|
+
.min(1)
|
|
1337
|
+
.max(STOREFRONT_WORKSPACE_MAX_FILES)
|
|
1338
|
+
.refine((files) => new Set(files.map((file) => file.path)).size === files.length, {
|
|
1339
|
+
message: "같은 경로를 두 번 실을 수 없습니다",
|
|
1340
|
+
}),
|
|
1341
|
+
endEditSession: z
|
|
1342
|
+
.boolean()
|
|
1343
|
+
.optional()
|
|
1344
|
+
.describe("열린 편집 환경을 끄고 진행한다 (기본 false — 열려 있으면 409 `EDIT_SESSION_OPEN`)"),
|
|
1345
|
+
packageJson: z
|
|
1346
|
+
.string()
|
|
1347
|
+
.max(STOREFRONT_FILE_MAX_BYTES)
|
|
1348
|
+
.optional()
|
|
1349
|
+
.describe(
|
|
1350
|
+
"`files`에 `package.json`이 있으면 그 내용(utf8). 서버가 의존성을 판정한다. 해시가 목록과 다르면 400 `DEPENDENCY_FILES_MISMATCH`",
|
|
1351
|
+
),
|
|
1352
|
+
lockfile: z
|
|
1353
|
+
.string()
|
|
1354
|
+
.max(STOREFRONT_FILE_MAX_BYTES)
|
|
1355
|
+
.optional()
|
|
1356
|
+
.describe("`files`에 `pnpm-lock.yaml`이 있으면 그 내용(utf8)"),
|
|
1357
|
+
});
|
|
1358
|
+
export type ReplaceStorefrontSourceRequest = z.infer<typeof replaceStorefrontSourceRequestSchema>;
|
|
1359
|
+
|
|
1360
|
+
export const storefrontSourceReplacedSchema = z.object({
|
|
1361
|
+
sourceRef: storefrontSourceRefSchema.describe("새 작업 트리 — 다음 올리기의 `expectedBaseRef`"),
|
|
1362
|
+
previousRef: z.string().describe("바꾸기 전 작업 트리"),
|
|
1363
|
+
treeSha256: z.string(),
|
|
1364
|
+
files: z.number().int().min(0),
|
|
1365
|
+
bytes: z.number().int().min(0),
|
|
1366
|
+
changed: z.boolean().describe("내용이 바뀌었는가 (같은 트리를 다시 올리면 false)"),
|
|
1367
|
+
endedEditSession: z.boolean().describe("열린 편집 환경을 끄고 진행했는가"),
|
|
1368
|
+
dependencyWarnings: z
|
|
1369
|
+
.array(storefrontDependencyFindingSchema)
|
|
1370
|
+
.describe("배포를 막지 않는 의존성 경고 (보안 권고 등)"),
|
|
1371
|
+
});
|
|
1372
|
+
export type StorefrontSourceReplaced = z.infer<typeof storefrontSourceReplacedSchema>;
|