@sayren/store-sdk 0.21.0 → 0.22.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.
@@ -42,6 +42,16 @@ import { RESERVED_STORE_CODES } from "./store";
42
42
  * - 409 `STOREFRONT_BUILD_IN_PROGRESS` — 이 사이트에 끝나지 않은 빌드가 있다(사이트당 하나)
43
43
  * - 404 `STOREFRONT_BUILD_NOT_FOUND`
44
44
  * - 502 `STOREFRONT_SANDBOX_UNAVAILABLE` — 편집 Sandbox 호출이 실패했다. 잠시 뒤 다시 한다
45
+ *
46
+ * 소스 내려받기·올리기(#60, `/v1/storefront/site/source/**`, `sayren` CLI):
47
+ * - 409 `SOURCE_CHANGED` — 작업 트리가 내려받은 뒤(`expectedBaseRef`) 바뀌었다(콘솔·AI 편집). `error.details.currentRef`
48
+ * - 409 `EDIT_SESSION_OPEN` — 편집 환경이 열려 있다. `endEditSession: true`로 끄고 진행할 수 있다
49
+ * - 409 `SOURCE_BLOBS_MISSING` — 올리지 않은 파일 내용이 있다(`error.details.hashes`). `POST source/blobs`로 올리고 다시 한다
50
+ * - 409 `DEPENDENCY_REJECTED` — 쓸 수 없는 npm 의존성이다(`error.details.rejected: [{ name, version, reason, detail }]`)
51
+ * - 413 `SOURCE_TOO_MANY_NEW_FILES` — 한 번에 새 내용이 800개를 넘는다(`error.details.max`). 나눠서 바꾼다
52
+ * - 400 `DEPENDENCY_FILES_MISMATCH` — `packageJson`·`lockfile` 내용이 목록의 해시와 다르거나 빠졌다
53
+ * - 400 `INVALID_SOURCE_REF` · `BLOB_HASH_MISMATCH` · `INVALID_FILE_PATH`(`.env*`·작업 트리 밖 경로)
54
+ * - 404 `STOREFRONT_SOURCE_NOT_FOUND` — 그 스냅샷이 없다
45
55
  */
46
56
 
47
57
  // ─── 슬러그 ───────────────────────────────────────────────
@@ -69,6 +79,8 @@ export const RESERVED_STOREFRONT_SLUGS: readonly string[] = [
69
79
  "dispatch",
70
80
  "control",
71
81
  "outbound",
82
+ // 내 도메인(M3)의 CNAME 대상·fallback origin이다(`sites.{호스팅 도메인}`)
83
+ "sites",
72
84
  ];
73
85
 
74
86
  /**
@@ -160,6 +172,13 @@ export const storefrontVersionSchema = z.object({
160
172
  createdByType: z
161
173
  .string()
162
174
  .describe("만든 행위자 유형 (`USER`·`AGENT`·`APP`·`TOKEN`·`SYSTEM`). 늘어날 수 있다"),
175
+ createdVia: z
176
+ .string()
177
+ .nullable()
178
+ .optional()
179
+ .describe(
180
+ "만든 경로 (`CONSOLE` 콘솔 · `AI` AI 에이전트(MCP) · `CLI` `sayren` CLI · `API` 관리 API · `SYSTEM` 자동). 늘어날 수 있다",
181
+ ),
163
182
  createdAt: z.string().describe("만든 시각 (ISO 8601)"),
164
183
  });
165
184
  export type StorefrontVersion = z.infer<typeof storefrontVersionSchema>;
@@ -185,7 +204,9 @@ export const storefrontDomainSchema = z.object({
185
204
  kind: z.enum(["PLATFORM", "CUSTOM"]).describe("`PLATFORM` 슬러그 주소 · `CUSTOM` 내 도메인"),
186
205
  status: z
187
206
  .enum(["ACTIVE", "HELD"])
188
- .describe("`ACTIVE` 사용 중 · `HELD` 슬러그를 바꿔 보류 중(새 주소로 301)"),
207
+ .describe(
208
+ "`ACTIVE` 사용 중 · `HELD` 슬러그를 바꿔 보류 중(새 주소로 301). 연결 중인 내 도메인은 싣지 않는다(`GET /v1/storefront/site/domains`)",
209
+ ),
189
210
  isPrimary: z.boolean().describe("대표 주소인가 (`Store.storefrontUrl`)"),
190
211
  holdUntil: z.string().nullable().describe("보류가 끝나는 시각 (ISO 8601). 보류가 아니면 null"),
191
212
  });
@@ -359,6 +380,264 @@ export const storefrontPreviewLinkSchema = z.object({
359
380
  });
360
381
  export type StorefrontPreviewLink = z.infer<typeof storefrontPreviewLinkSchema>;
361
382
 
383
+ // ─── 내 도메인 (M3) ───────────────────────────────────────
384
+ //
385
+ // 셀러가 가진 도메인(예: `shop.brand.com`)을 사이트 주소로 연결한다. 흐름은 추가(`PENDING_DNS`) → 셀러가 DNS에 TXT(소유 확인)·
386
+ // CNAME(연결)을 넣음 → 서버가 TXT를 확인하면 도메인을 이 스토어에 잡고 인증서 발급을 시작한다(`PENDING_VALIDATION`) → 인증서가
387
+ // 발급되면 `ACTIVE`다. 확인은 서버가 주기적으로 하고 `verify`는 곧바로 한 번 더 본다. 연결된 도메인은 대표 주소로 지정할 수 있고,
388
+ // 대표가 아닌 주소(기본 주소 포함)는 대표 주소로 301한다.
389
+ //
390
+ // - TXT 소유 확인은 도메인마다 새 값이다. CNAME만으로는 연결하지 않는다 — 연결을 끊은 뒤 CNAME이 남은 도메인을 다른 스토어가
391
+ // 가져가지 못하게 한다.
392
+ // - 소유 확인 전(`PENDING_DNS`)에는 도메인을 잡지 않는다. 같은 도메인을 여러 스토어가 추가해 둘 수 있고, 먼저 TXT 확인을 통과한
393
+ // 스토어가 잡는다. 이미 다른 스토어가 잡은 도메인은 409 `STOREFRONT_DOMAIN_TAKEN`이다.
394
+ // - 해제하면 곧바로 연결을 지운다. 연결된 뒤 DNS가 7일 넘게 이 서비스를 가리키지 않으면 서버가 연결을 해제한다.
395
+ // - 결제 복귀·방문 분석·소셜 로그인 허용 목록은 연결된(`ACTIVE`) 도메인을 자동으로 더한다.
396
+ //
397
+ // 오류 코드:
398
+ // - 403 `STOREFRONT_CUSTOM_DOMAINS_DISABLED` — 플랫폼 스위치가 꺼져 도메인을 추가할 수 없다
399
+ // - 400 `STOREFRONT_DOMAIN_INVALID` — 형식이 틀렸거나 연결할 수 없는 도메인이다(`error.details.reason`)
400
+ // - 404 `STOREFRONT_DOMAIN_NOT_FOUND`
401
+ // - 409 `STOREFRONT_DOMAIN_EXISTS` — 이 스토어에 이미 추가한 도메인이다
402
+ // - 409 `STOREFRONT_DOMAIN_TAKEN` — 다른 스토어가 연결했거나 연결 중인 도메인이다
403
+ // - 409 `STOREFRONT_DOMAIN_LIMIT` — 스토어당 도메인 수 상한(`error.details.limit`)
404
+ // - 409 `STOREFRONT_DOMAIN_NOT_ACTIVE` — 연결되지 않았거나 경고(`failureCode`)가 있는 도메인은 대표 주소로 지정할 수 없다
405
+ // - 429 `TOO_MANY_REQUESTS` — 재확인은 스토어당 분당 20번까지다(`error.details.retryAfterSeconds`)
406
+ // - 400 `STOREFRONT_DOMAIN_NOT_REMOVABLE` — 기본 주소는 해제할 수 없다
407
+
408
+ /**
409
+ * 연결할 수 없는 도메인 — 플랫폼이 쓰는 도메인과 그 하위(기본 주소는 슬러그로 바꾼다). 호스팅 도메인은 서버 설정이라
410
+ * `storefrontCustomHostnameProblem`의 인자로 따로 받는다.
411
+ */
412
+ export const STOREFRONT_PLATFORM_DOMAINS: readonly string[] = [
413
+ "sayren.co",
414
+ "sayren.app",
415
+ "sayren.shop",
416
+ "avarlabs.com",
417
+ ];
418
+
419
+ /** 공개 인터넷에서 쓸 수 없는 최상위 도메인(RFC 2606·6761·6762 등) */
420
+ const NON_PUBLIC_TLDS: readonly string[] = [
421
+ "example",
422
+ "localhost",
423
+ "local",
424
+ "test",
425
+ "invalid",
426
+ "onion",
427
+ "internal",
428
+ "arpa",
429
+ "home",
430
+ "lan",
431
+ "corp",
432
+ "intranet",
433
+ "alt",
434
+ ];
435
+
436
+ /**
437
+ * 공용 접미사 — 누구나 그 아래 이름을 받는 도메인이라 접미사 자체는 연결할 수 없다. Public Suffix List 전체가 아니라 자주 틀리는
438
+ * 것만 둔다(여기 없는 접미사도 TXT 소유 확인을 통과할 수 없어 연결되지 않는다 — 이 목록은 추가 단계에서 빨리 알려 주는 안내다).
439
+ */
440
+ export const STOREFRONT_PUBLIC_SUFFIXES: readonly string[] = [
441
+ "co.kr",
442
+ "or.kr",
443
+ "ne.kr",
444
+ "go.kr",
445
+ "ac.kr",
446
+ "re.kr",
447
+ "pe.kr",
448
+ "mil.kr",
449
+ "hs.kr",
450
+ "ms.kr",
451
+ "es.kr",
452
+ "sc.kr",
453
+ "kg.kr",
454
+ "seoul.kr",
455
+ "busan.kr",
456
+ "incheon.kr",
457
+ "daegu.kr",
458
+ "gwangju.kr",
459
+ "daejeon.kr",
460
+ "ulsan.kr",
461
+ "gyeonggi.kr",
462
+ "gangwon.kr",
463
+ "chungbuk.kr",
464
+ "chungnam.kr",
465
+ "jeonbuk.kr",
466
+ "jeonnam.kr",
467
+ "gyeongbuk.kr",
468
+ "gyeongnam.kr",
469
+ "jeju.kr",
470
+ "co.jp",
471
+ "ne.jp",
472
+ "or.jp",
473
+ "com.cn",
474
+ "net.cn",
475
+ "com.tw",
476
+ "com.hk",
477
+ "co.uk",
478
+ "org.uk",
479
+ "com.au",
480
+ "net.au",
481
+ "com.sg",
482
+ "co.nz",
483
+ "com.br",
484
+ "github.io",
485
+ "gitlab.io",
486
+ "pages.dev",
487
+ "workers.dev",
488
+ "vercel.app",
489
+ "netlify.app",
490
+ "herokuapp.com",
491
+ "web.app",
492
+ "firebaseapp.com",
493
+ "blogspot.com",
494
+ "cloudfront.net",
495
+ "azurewebsites.net",
496
+ "myshopify.com",
497
+ "wixsite.com",
498
+ "cafe24.com",
499
+ "imweb.me",
500
+ ];
501
+
502
+ /** 스토어당 연결할 수 있는 도메인 수 기본값 — 요금제(#44) 전의 상한이다. 실제 값은 `GET …/domains`의 `limit` */
503
+ export const STOREFRONT_CUSTOM_DOMAIN_LIMIT_DEFAULT = 2;
504
+ /** DNS를 넣을 때까지 기다리는 기간(일) — 지나면 `FAILED`(`DNS_NOT_CONFIGURED`) */
505
+ export const STOREFRONT_DOMAIN_DNS_WAIT_DAYS = 7;
506
+ /** 연결된 도메인의 DNS가 이 서비스를 가리키지 않아도 연결을 유지하는 기간(일) — 지나면 서버가 해제한다 */
507
+ export const STOREFRONT_DOMAIN_DNS_GRACE_DAYS = 7;
508
+ /** 소유 확인 TXT 레코드의 이름 앞부분 — `_sayren-verify.{도메인}` */
509
+ export const STOREFRONT_DOMAIN_TXT_LABEL = "_sayren-verify";
510
+
511
+ const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
512
+ const PUNYCODE_TLD = /^xn--[a-z0-9-]{1,59}$/;
513
+ const ALPHA_TLD = /^[a-z]{2,63}$/;
514
+
515
+ /**
516
+ * 도메인 정규화 — 앞뒤 공백·끝 점을 떼고 소문자로 바꾸며 국제화 도메인은 punycode(`xn--`)로 바꾼다. 스킴·경로·포트가 붙은 입력
517
+ * (`https://www.a.com/`)은 호스트만 남기지 않고 그대로 두어 형식 검사에서 떨어진다(서버는 사용자가 적은 도메인만 받는다).
518
+ * 변환할 수 없으면 입력을 소문자로만 바꿔 돌려준다(형식 검사가 거부한다).
519
+ */
520
+ export function normalizeStorefrontHostname(input: string): string {
521
+ const trimmed = input.trim().replace(/\.+$/, "");
522
+ if (!trimmed || /[\s/:?#@\\[\]%]/.test(trimmed)) return trimmed.toLowerCase();
523
+ if (/^[\x21-\x7e]+$/.test(trimmed)) return trimmed.toLowerCase();
524
+ try {
525
+ // WHATWG URL이 IDNA(UTS #46)로 punycode를 만든다. 브라우저·Node·Workers가 같은 결과다
526
+ return new URL(`http://${trimmed}`).hostname.toLowerCase();
527
+ } catch {
528
+ return trimmed.toLowerCase();
529
+ }
530
+ }
531
+
532
+ export type StorefrontHostnameProblem =
533
+ | "FORMAT"
534
+ | "IP_ADDRESS"
535
+ | "SINGLE_LABEL"
536
+ | "NON_PUBLIC_TLD"
537
+ | "PLATFORM_DOMAIN"
538
+ | "PUBLIC_SUFFIX"
539
+ | "WILDCARD";
540
+
541
+ /**
542
+ * 연결할 수 있는 도메인인가(정규화한 값 기준). 문제가 없으면 null이다. 규칙: 253자 이하, 라벨 2개 이상, 라벨은 소문자·숫자·하이픈
543
+ * 63자 이하(하이픈으로 시작·끝 불가), 최상위 도메인은 영문자이거나 punycode, IP 주소·와일드카드·공개 인터넷 밖 최상위 도메인·
544
+ * 플랫폼 도메인과 그 하위·공용 접미사 자체는 안 된다. 제어·디스패치 Worker가 형식 규칙의 복제본을 들고 있다(api
545
+ * `test/storefront-hosting-parity.spec.ts`가 대조한다).
546
+ */
547
+ export function storefrontCustomHostnameProblem(
548
+ hostname: string,
549
+ hostingDomain = "sayren.co",
550
+ ): StorefrontHostnameProblem | null {
551
+ if (hostname.startsWith("*.")) return "WILDCARD";
552
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname) || /^\[?[0-9a-f:.]*:[0-9a-f:.]*\]?$/.test(hostname))
553
+ return "IP_ADDRESS";
554
+ if (hostname.length === 0 || hostname.length > 253) return "FORMAT";
555
+ const labels = hostname.split(".");
556
+ if (labels.length < 2) return "SINGLE_LABEL";
557
+ if (!labels.every((label) => LABEL.test(label))) return "FORMAT";
558
+ const tld = labels[labels.length - 1] ?? "";
559
+ if (!ALPHA_TLD.test(tld) && !PUNYCODE_TLD.test(tld)) return "FORMAT";
560
+ if (NON_PUBLIC_TLDS.includes(tld)) return "NON_PUBLIC_TLD";
561
+ const platform = new Set([...STOREFRONT_PLATFORM_DOMAINS, hostingDomain.toLowerCase()]);
562
+ for (const domain of platform) {
563
+ if (hostname === domain || hostname.endsWith(`.${domain}`)) return "PLATFORM_DOMAIN";
564
+ }
565
+ if (STOREFRONT_PUBLIC_SUFFIXES.includes(hostname)) return "PUBLIC_SUFFIX";
566
+ return null;
567
+ }
568
+
569
+ /** 소유 확인 TXT 레코드의 전체 이름 */
570
+ export function storefrontDomainTxtNameOf(hostname: string): string {
571
+ return `${STOREFRONT_DOMAIN_TXT_LABEL}.${hostname}`;
572
+ }
573
+
574
+ export const storefrontCustomDomainStatusSchema = z
575
+ .enum(["PENDING_DNS", "PENDING_VALIDATION", "ACTIVE", "FAILED"])
576
+ .describe(
577
+ "`PENDING_DNS` DNS 설정 대기(소유 확인 전) · `PENDING_VALIDATION` 소유 확인됨, 인증서 발급 중 · `ACTIVE` 연결됨 · `FAILED` 연결 실패(`failureCode`)",
578
+ );
579
+ export type StorefrontCustomDomainStatus = z.infer<typeof storefrontCustomDomainStatusSchema>;
580
+
581
+ export const storefrontDnsRecordSchema = z.object({
582
+ type: z.enum(["CNAME", "TXT"]).describe("레코드 유형"),
583
+ name: z.string().describe("레코드 이름 (전체 이름, 예: `_sayren-verify.shop.brand.com`)"),
584
+ value: z.string().describe("레코드 값"),
585
+ purpose: z.enum(["ROUTING", "OWNERSHIP"]).describe("`ROUTING` 연결 · `OWNERSHIP` 소유 확인"),
586
+ });
587
+ export type StorefrontDnsRecord = z.infer<typeof storefrontDnsRecordSchema>;
588
+
589
+ export const storefrontSiteDomainSchema = z.object({
590
+ domainId: z.string(),
591
+ hostname: z.string().describe("호스트 이름 (punycode, 소문자)"),
592
+ kind: z
593
+ .enum(["PLATFORM", "CUSTOM"])
594
+ .describe("`PLATFORM` 기본 주소(슬러그) · `CUSTOM` 내 도메인"),
595
+ status: storefrontCustomDomainStatusSchema.describe(
596
+ "기본 주소는 늘 `ACTIVE`다. 내 도메인은 연결 단계",
597
+ ),
598
+ isPrimary: z
599
+ .boolean()
600
+ .describe("대표 주소인가 (`Store.storefrontUrl`). 대표가 아닌 주소는 대표로 301한다"),
601
+ dnsRecords: z.array(storefrontDnsRecordSchema).describe("DNS에 넣을 레코드. 기본 주소는 빈 배열"),
602
+ certificateStatus: z
603
+ .enum(["PENDING", "ACTIVE", "FAILED"])
604
+ .nullable()
605
+ .describe("인증서 상태. 소유 확인 전·기본 주소는 null"),
606
+ failureCode: z
607
+ .string()
608
+ .nullable()
609
+ .describe(
610
+ "실패·경고 코드. `DNS_NOT_CONFIGURED` 기한 안에 TXT를 찾지 못함 · `VALIDATION_TIMEOUT` 인증서 발급 기한 지남 · `CERTIFICATE_FAILED` 인증서 발급 실패 · `DNS_NOT_POINTING` 연결된 뒤 DNS가 이 서비스를 가리키지 않음(기한이 지나면 해제) · `DOMAIN_TAKEN` 다른 스토어가 먼저 소유를 확인함 · `HOSTNAME_BLOCKED` 연결이 막힘 · `HOSTNAME_CONFLICT` 연결 준비 중 충돌(운영 확인). 늘어날 수 있다",
611
+ ),
612
+ verifiedAt: z.string().nullable().describe("소유를 확인한 시각 (ISO 8601)"),
613
+ activatedAt: z.string().nullable().describe("연결된 시각 (ISO 8601)"),
614
+ checkedAt: z.string().nullable().describe("마지막으로 확인한 시각 (ISO 8601)"),
615
+ createdAt: z.string(),
616
+ });
617
+ export type StorefrontSiteDomain = z.infer<typeof storefrontSiteDomainSchema>;
618
+
619
+ /** `GET /v1/storefront/site/domains` — 사이트의 주소 목록(기본 주소 + 내 도메인)과 연결 설정 */
620
+ export const storefrontSiteDomainsSchema = z.object({
621
+ enabled: z.boolean().describe("내 도메인을 추가할 수 있는가 (플랫폼 스위치)"),
622
+ limit: z
623
+ .number()
624
+ .int()
625
+ .min(0)
626
+ .describe("스토어당 내 도메인 수 상한 (해제한 도메인은 세지 않는다)"),
627
+ target: z.string().describe("CNAME 대상 (예: `sites.sayren.co`)"),
628
+ domains: z.array(storefrontSiteDomainSchema).describe("대표 주소 먼저, 그다음 추가한 순서"),
629
+ });
630
+ export type StorefrontSiteDomains = z.infer<typeof storefrontSiteDomainsSchema>;
631
+
632
+ export const addStorefrontDomainRequestSchema = z.object({
633
+ hostname: z
634
+ .string()
635
+ .min(1)
636
+ .max(300)
637
+ .describe("연결할 도메인 (예: `shop.brand.com`). 서버가 소문자·punycode로 정규화한다"),
638
+ });
639
+ export type AddStorefrontDomainRequest = z.infer<typeof addStorefrontDomainRequestSchema>;
640
+
362
641
  // ─── 편집 (M2b) ───────────────────────────────────────────
363
642
  //
364
643
  // 셀러(콘솔)와 에이전트(MCP)가 사이트의 작업 트리를 고치고 버전을 만든다. 작업 트리는 사이트당 하나이고 편집 Sandbox
@@ -776,9 +1055,11 @@ export const STOREFRONT_BUILD_TERMINAL_STATUSES: readonly StorefrontBuildStatus[
776
1055
 
777
1056
  /**
778
1057
  * 빌드 단계 — 이 순서로 돈다. 첫 실패에서 멈추지만 규칙 검증(`VERIFY`)은 실패해도 타입 검사까지 이어서 해 한 번에 알려 준다.
779
- * `UPLOAD`는 사이트 버전 Worker 업로드다(성공하면 발행할 수 있다). 값은 늘어날 수 있다.
1058
+ * `UPLOAD`는 사이트 버전 Worker 업로드다(성공하면 발행할 수 있다). `DEPENDENCIES`는 빌드 시작 전 의존성 검사(거절 목록·보안 권고)다.
1059
+ * 값은 늘어날 수 있다.
780
1060
  */
781
1061
  export const STOREFRONT_BUILD_STAGES = [
1062
+ "DEPENDENCIES",
782
1063
  "VERIFY",
783
1064
  "RESTORE",
784
1065
  "INSTALL",
@@ -789,6 +1070,40 @@ export const STOREFRONT_BUILD_STAGES = [
789
1070
  "UPLOAD",
790
1071
  ] as const;
791
1072
 
1073
+ /**
1074
+ * 의존성 검사 결과 한 줄(#61) — 거절(`DEPENDENCY_REJECTED`의 `details.rejected`)·경고(빌드 `dependencies`, 소스 교체 `dependencyWarnings`)가
1075
+ * 같은 모양이다. 대상 패키지와 사유. 셀러가 고칠 수 있게 버전·권고 id·거절 사유를 싣는다.
1076
+ * `reason`: `DENYLISTED`(거절 목록) · `ADVISORY`(보안 권고) · `MALWARE`(악성 코드 권고, 패키지 전체) · `INSTALL_SCRIPT`(루트
1077
+ * package.json의 설치 훅은 거절, 의존성의 설치 스크립트는 경고) · `UNSUPPORTED_SOURCE`(레지스트리 밖 설치 경로) · `BUNDLED_DEPENDENCIES`(묶여
1078
+ * 오는 의존성, 경고) · `INVALID_MANIFEST`(package.json·잠금 파일 형식), 늘어날 수 있다.
1079
+ */
1080
+ export const storefrontDependencyFindingSchema = z.object({
1081
+ name: z.string().describe("패키지 이름. 파일 전체의 문제면 `package.json`·`pnpm-lock.yaml`"),
1082
+ version: z.string().nullable().describe("해당 버전. 버전과 무관하면 null"),
1083
+ reason: z.string().describe("사유 코드"),
1084
+ detail: z.string().describe("셀러용 설명"),
1085
+ advisory: z
1086
+ .object({
1087
+ id: z.string(),
1088
+ url: z.string().nullable(),
1089
+ severity: z.string(),
1090
+ title: z.string(),
1091
+ })
1092
+ .nullable()
1093
+ .optional()
1094
+ .describe("보안 권고(사유가 `ADVISORY`·`MALWARE`일 때)"),
1095
+ });
1096
+ export type StorefrontDependencyFinding = z.infer<typeof storefrontDependencyFindingSchema>;
1097
+
1098
+ export const storefrontBuildDependenciesSchema = z.object({
1099
+ checkedPackages: z.number().int().min(0).describe("검사한 설치 트리의 패키지(이름·버전) 수"),
1100
+ rejected: z.array(storefrontDependencyFindingSchema).describe("빌드를 막은 의존성"),
1101
+ warnings: z
1102
+ .array(storefrontDependencyFindingSchema)
1103
+ .describe("빌드는 막지 않는 경고(중간 이하 심각도 권고)"),
1104
+ });
1105
+ export type StorefrontBuildDependencies = z.infer<typeof storefrontBuildDependenciesSchema>;
1106
+
792
1107
  export const storefrontBuildSchema = z.object({
793
1108
  buildId: z.string(),
794
1109
  versionId: z.string(),
@@ -798,13 +1113,13 @@ export const storefrontBuildSchema = z.object({
798
1113
  .string()
799
1114
  .nullable()
800
1115
  .describe(
801
- "실패한 단계 (`VERIFY`·`RESTORE`·`INSTALL`·`TYPECHECK`·`BUILD`·`BUNDLE`·`PACK`·`UPLOAD`, 늘어날 수 있다). 실패가 아니면 null",
1116
+ "실패한 단계 (`DEPENDENCIES`·`VERIFY`·`RESTORE`·`INSTALL`·`TYPECHECK`·`BUILD`·`BUNDLE`·`PACK`·`UPLOAD`, 늘어날 수 있다). 실패가 아니면 null",
802
1117
  ),
803
1118
  errorCode: z
804
1119
  .string()
805
1120
  .nullable()
806
1121
  .describe(
807
- "실패 코드 (`RULES_VIOLATED`·`INSTALL_FAILED`·`TYPECHECK_FAILED`·`BUILD_FAILED`·`BUNDLE_FAILED`·`ARTIFACT_INVALID`·`UPLOAD_FAILED`·`SOURCE_NOT_FOUND`·`CAPACITY_EXHAUSTED`·`INTERNAL`, 늘어날 수 있다). 실패가 아니면 null",
1122
+ "실패 코드 (`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
1123
  ),
809
1124
  logTail: z.string().nullable().describe("실패 단계 출력 꼬리 (최대 8KB). 없으면 null"),
810
1125
  stages: z
@@ -819,6 +1134,13 @@ export const storefrontBuildSchema = z.object({
819
1134
  verify: storefrontVerifyReportSchema
820
1135
  .nullable()
821
1136
  .describe("규칙 검증 보고서(위반 상세는 50건까지). 그 단계까지 가지 못했으면 null"),
1137
+ // 옛 api 응답에는 없다 — 새 SDK가 배포 전 api와 함께 쓰여도 깨지지 않게 선택 필드다
1138
+ dependencies: storefrontBuildDependenciesSchema
1139
+ .nullable()
1140
+ .optional()
1141
+ .describe(
1142
+ "의존성 검사 결과(거절 목록·보안 권고). 빌드 시작 전에 돈다. 거절이 있으면 `failureStage: DEPENDENCIES`, `errorCode: DEPENDENCY_REJECTED`다. 아직 검사 전이면 null(옛 서버는 싣지 않는다)",
1143
+ ),
822
1144
  queuedAt: z.string(),
823
1145
  startedAt: z.string().nullable(),
824
1146
  finishedAt: z.string().nullable(),
@@ -836,6 +1158,13 @@ export const storefrontBuildListQuerySchema = z.object({
836
1158
  */
837
1159
  export const createStorefrontVersionRequestSchema = z.object({
838
1160
  message: z.string().max(200).optional().describe("버전 메모 (에이전트는 바꾼 내용 요약)"),
1161
+ expectedSourceRef: z
1162
+ .string()
1163
+ .max(200)
1164
+ .optional()
1165
+ .describe(
1166
+ "고정할 작업 트리 (`PUT /v1/storefront/site/source`의 `sourceRef`). 주면 지금 작업 트리가 이 값일 때만 버전을 만든다(다르면 409 `SOURCE_CHANGED`)",
1167
+ ),
839
1168
  });
840
1169
  export type CreateStorefrontVersionRequest = z.infer<typeof createStorefrontVersionRequestSchema>;
841
1170
 
@@ -854,3 +1183,142 @@ export type StorefrontVersionCreated = z.infer<typeof storefrontVersionCreatedSc
854
1183
  export const createStorefrontEditPreviewLinkRequestSchema =
855
1184
  createStorefrontPreviewLinkRequestSchema;
856
1185
  export type CreateStorefrontEditPreviewLinkRequest = CreateStorefrontPreviewLinkRequest;
1186
+
1187
+ // ─── 소스 내려받기·올리기 (#60, `sayren` CLI) ─────────────────────────────
1188
+ //
1189
+ // 셀러가 사이트 작업 트리를 로컬로 받아(`npx sayren pull`) 고치고 되돌려 놓는다(`npx sayren deploy`). 콘솔·AI 편집과 같은
1190
+ // 작업 트리다. 올리는 것은 소스뿐이고 빌드는 서버가 한다(버전 만들기). 흐름:
1191
+ // 1. `GET source` — 지금 작업 트리를 고정하고(`sourceRef`) 압축 파일 크기를 알린다
1192
+ // 2. `GET source/archive?sourceRef=&part=` — 압축 파일(tar.gz)을 4MiB씩 받는다
1193
+ // 3. `PUT source` — 바꾼 뒤 트리 전체 목록(경로·해시·크기)과 `package.json`·`pnpm-lock.yaml` 내용을 보낸다(의존성 판정). 서버에 없는 내용은 409 `SOURCE_BLOBS_MISSING`이
1194
+ // 알려 주고, `POST source/blobs`로 올린 뒤 다시 보낸다. `expectedBaseRef`(내려받은 `sourceRef`)가 지금과 다르면 409 `SOURCE_CHANGED`
1195
+
1196
+ /** 압축 파일 조각 하나의 최대 크기(바이트) */
1197
+ export const STOREFRONT_SOURCE_PART_BYTES = 4 * 1024 * 1024;
1198
+ /** 내용 올리기 한 번의 최대 파일 수 */
1199
+ export const STOREFRONT_SOURCE_BLOB_BATCH_MAX = 200;
1200
+ /** 작업 트리 파일 수 상한 */
1201
+ export const STOREFRONT_WORKSPACE_MAX_FILES = 2000;
1202
+
1203
+ /** 작업 트리 스냅샷 키 — `sources/{siteId}/{sha256}.tgz` */
1204
+ export const storefrontSourceRefSchema = z
1205
+ .string()
1206
+ .regex(/^sources\/sfs_[a-z0-9]{8,40}\/[0-9a-f]{64}\.tgz$/, "작업 트리 스냅샷 키 형식이 아닙니다")
1207
+ .describe("작업 트리 스냅샷 (`sources/{siteId}/{sha256}.tgz`, 내용 주소)");
1208
+
1209
+ /** `GET /v1/storefront/site/source` — 지금 작업 트리(편집 환경이 떠 있으면 지금 내용을 고정한다) */
1210
+ export const storefrontSourceSchema = z.object({
1211
+ siteId: z.string(),
1212
+ storeId: z.string(),
1213
+ storeCode: z.string().describe("스토어 코드 — 로컬 개발 서버의 `SAYREN_STORE_CODE`"),
1214
+ sourceRef: storefrontSourceRefSchema,
1215
+ treeSha256: z.string().describe("트리 지문 (경로 오름차순 `{path}\\t{sha256}\\n`의 SHA-256)"),
1216
+ files: z.number().int().min(0).describe("파일 수"),
1217
+ bytes: z.number().int().min(0).describe("파일 크기 합"),
1218
+ archive: z.object({
1219
+ format: z.literal("tar+gzip"),
1220
+ bytes: z.number().int().min(0).describe("압축 파일 크기"),
1221
+ sha256: z.string().describe("압축 파일의 SHA-256 (`sourceRef`의 해시와 같다)"),
1222
+ parts: z.number().int().min(0).describe("`GET source/archive`로 받을 조각 수"),
1223
+ partBytes: z.number().int().describe("조각 하나의 최대 크기"),
1224
+ }),
1225
+ editSessionOpen: z
1226
+ .boolean()
1227
+ .describe("편집 환경이 열려 있는가 (올리기 전에 끄거나 `endEditSession`)"),
1228
+ });
1229
+ export type StorefrontSource = z.infer<typeof storefrontSourceSchema>;
1230
+
1231
+ export const storefrontSourceArchiveQuerySchema = z.object({
1232
+ sourceRef: storefrontSourceRefSchema,
1233
+ part: z.coerce.number().int().min(0).max(100).optional().describe("조각 번호 (0부터, 기본 0)"),
1234
+ });
1235
+
1236
+ /** `GET /v1/storefront/site/source/archive` — 압축 파일 조각 */
1237
+ export const storefrontSourceArchivePartSchema = z.object({
1238
+ sourceRef: storefrontSourceRefSchema,
1239
+ part: z.number().int().min(0),
1240
+ parts: z.number().int().min(0),
1241
+ offset: z.number().int().min(0),
1242
+ bytes: z.number().int().min(0).describe("이 조각의 크기"),
1243
+ totalBytes: z.number().int().min(0),
1244
+ content: z.string().describe("조각 내용 (base64)"),
1245
+ });
1246
+ export type StorefrontSourceArchivePart = z.infer<typeof storefrontSourceArchivePartSchema>;
1247
+
1248
+ /** `POST /v1/storefront/site/source/blobs` — 파일 내용 올리기(해시 키, 다시 올려도 안전하다). 작업 트리는 바뀌지 않는다 */
1249
+ export const uploadStorefrontSourceBlobsRequestSchema = z.object({
1250
+ blobs: z
1251
+ .array(
1252
+ z.object({
1253
+ hash: storefrontFileHashSchema,
1254
+ content: z.string().describe("파일 내용 (base64, 1MiB까지)"),
1255
+ }),
1256
+ )
1257
+ .min(1)
1258
+ .max(STOREFRONT_SOURCE_BLOB_BATCH_MAX),
1259
+ });
1260
+ export type UploadStorefrontSourceBlobsRequest = z.infer<
1261
+ typeof uploadStorefrontSourceBlobsRequestSchema
1262
+ >;
1263
+
1264
+ export const storefrontSourceBlobsResultSchema = z.object({
1265
+ stored: z.number().int().min(0).describe("새로 올린 수"),
1266
+ existing: z.number().int().min(0).describe("이미 있던 수"),
1267
+ });
1268
+ export type StorefrontSourceBlobsResult = z.infer<typeof storefrontSourceBlobsResultSchema>;
1269
+
1270
+ /**
1271
+ * `PUT /v1/storefront/site/source` — 작업 트리 전체를 이 목록으로 바꾼다(없는 경로는 지운다). 전부 아니면 전무다.
1272
+ * `package.json`·`pnpm-lock.yaml`도 바꿀 수 있고 의존성은 허용 목록 안이어야 한다. `.env*`·작업 트리 밖 경로는 받지 않는다.
1273
+ */
1274
+ export const replaceStorefrontSourceRequestSchema = z.object({
1275
+ expectedBaseRef: storefrontSourceRefSchema
1276
+ .optional()
1277
+ .describe(
1278
+ "내려받은 작업 트리. 지금 작업 트리가 이 값일 때만 바꾼다. 생략하면 조건 없이 덮어쓴다",
1279
+ ),
1280
+ files: z
1281
+ .array(
1282
+ z.object({
1283
+ path: storefrontFilePathSchema,
1284
+ hash: storefrontFileHashSchema,
1285
+ size: z.number().int().min(0).max(STOREFRONT_FILE_MAX_BYTES),
1286
+ }),
1287
+ )
1288
+ .min(1)
1289
+ .max(STOREFRONT_WORKSPACE_MAX_FILES)
1290
+ .refine((files) => new Set(files.map((file) => file.path)).size === files.length, {
1291
+ message: "같은 경로를 두 번 실을 수 없습니다",
1292
+ }),
1293
+ endEditSession: z
1294
+ .boolean()
1295
+ .optional()
1296
+ .describe("열린 편집 환경을 끄고 진행한다 (기본 false — 열려 있으면 409 `EDIT_SESSION_OPEN`)"),
1297
+ packageJson: z
1298
+ .string()
1299
+ .max(STOREFRONT_FILE_MAX_BYTES)
1300
+ .optional()
1301
+ .describe(
1302
+ "`files`에 `package.json`이 있으면 그 내용(utf8). 서버가 의존성을 판정한다. 해시가 목록과 다르면 400 `DEPENDENCY_FILES_MISMATCH`",
1303
+ ),
1304
+ lockfile: z
1305
+ .string()
1306
+ .max(STOREFRONT_FILE_MAX_BYTES)
1307
+ .optional()
1308
+ .describe("`files`에 `pnpm-lock.yaml`이 있으면 그 내용(utf8)"),
1309
+ });
1310
+ export type ReplaceStorefrontSourceRequest = z.infer<typeof replaceStorefrontSourceRequestSchema>;
1311
+
1312
+ export const storefrontSourceReplacedSchema = z.object({
1313
+ sourceRef: storefrontSourceRefSchema.describe("새 작업 트리 — 다음 올리기의 `expectedBaseRef`"),
1314
+ previousRef: z.string().describe("바꾸기 전 작업 트리"),
1315
+ treeSha256: z.string(),
1316
+ files: z.number().int().min(0),
1317
+ bytes: z.number().int().min(0),
1318
+ changed: z.boolean().describe("내용이 바뀌었는가 (같은 트리를 다시 올리면 false)"),
1319
+ endedEditSession: z.boolean().describe("열린 편집 환경을 끄고 진행했는가"),
1320
+ dependencyWarnings: z
1321
+ .array(storefrontDependencyFindingSchema)
1322
+ .describe("배포를 막지 않는 의존성 경고 (보안 권고 등)"),
1323
+ });
1324
+ export type StorefrontSourceReplaced = z.infer<typeof storefrontSourceReplacedSchema>;