@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.
@@ -4,7 +4,7 @@ import { RESERVED_STORE_CODES } from "./store";
4
4
  /**
5
5
  * 스토어프론트 호스팅 (이슈 #53, M1: 템플릿 발행) — 관리 API `/v1/storefront/**`의 계약이다.
6
6
  *
7
- * 셀러는 템플릿을 골라 사이트를 만들고 `{slug}.{호스팅 도메인}` 주소를 받는다. 스토어를 만들어도 사이트는 생기지 않고
7
+ * 셀러는 템플릿을 골라 사이트를 만들고 `{slug}.{호스팅 도메인}` 주소를 받는다. 상점을 만들어도 사이트는 생기지 않고
8
8
  * `POST /v1/storefront/site`가 유일한 생성 경로다. 템플릿은 만들 때만 고르고 바꾸지 않는다. 발행은 버전 단위이고,
9
9
  * 되돌리기는 이전 버전을 다시 발행하는 것이다(재빌드 없음). 상태의 원천은 api DB이고 Cloudflare 쪽 반영
10
10
  * (라우팅 표·사이트 Worker)은 커밋 뒤 비동기다 — 반영 상태는 `provisioning`으로 본다.
@@ -15,15 +15,16 @@ import { RESERVED_STORE_CODES } from "./store";
15
15
  * 오류 코드:
16
16
  * - 403 `STOREFRONT_HOSTING_DISABLED` — 플랫폼 스위치가 꺼져 새 사이트를 만들 수 없다
17
17
  * - 404 `STOREFRONT_SITE_NOT_FOUND` · `STOREFRONT_TEMPLATE_NOT_FOUND` · `STOREFRONT_VERSION_NOT_FOUND`
18
- * - 409 `STOREFRONT_SITE_EXISTS` — 이미 사이트가 있다(스토어당 1개)
18
+ * - 409 `STOREFRONT_SITE_EXISTS` — 이미 사이트가 있다(상점당 1개)
19
19
  * - 409 `STOREFRONT_SLUG_TAKEN` — 다른 사이트가 쓰거나 보류 중인 슬러그다
20
- * - 409 `STOREFRONT_SLUG_CHANGE_LIMIT` — 슬러그는 스토어당 30일에 3번까지 바꾼다
21
- * - 409 `STOREFRONT_SITE_CREATE_LIMIT` — 사이트는 스토어당 30일에 3번까지 만든다
20
+ * - 409 `STOREFRONT_SLUG_CHANGE_LIMIT` — 슬러그는 상점당 30일에 3번까지 바꾼다
21
+ * - 409 `STOREFRONT_SITE_CREATE_LIMIT` — 사이트는 상점당 30일에 3번까지 만든다
22
22
  * - 409 `STOREFRONT_SITE_CHANGED` — 읽은 상태(`expected*`)와 현재 상태가 다르다. 다시 읽고 시도한다
23
23
  * - 409 `STOREFRONT_SITE_NOT_ACTIVE` — 정지(`SUSPENDED`)된 사이트는 바꾸거나 끌 수 없다
24
- * - 409 `STORE_SUSPENDED` — 정지된 스토어에는 사이트를 만들 수 없다
24
+ * - 409 `STORE_SUSPENDED` — 정지된 상점에는 사이트를 만들 수 없다
25
25
  * - 409 `STOREFRONT_VERSION_NOT_READY` — 빌드가 끝나지 않았거나 실패한 버전은 발행할 수 없다
26
26
  * - 409 `STOREFRONT_NO_ROLLBACK_TARGET` — 되돌릴 이전 발행 버전이 없다
27
+ * - 409 `STOREFRONT_NOT_READY` — 공개 전환: 판매 중인 상품(샘플 제외)이 없다(이슈 #70). 상품을 등록한 뒤 공개한다
27
28
  * - 409 `TEMPLATE_CHANGE_NOT_SUPPORTED` — 템플릿 변경(`POST /v1/storefront/site/template`, 폐기)은 항상 이 오류다
28
29
  * - 400 `STOREFRONT_SLUG_CONFIRM_MISMATCH` — 호스팅 끄기 확인 입력이 현재 슬러그와 다르다
29
30
  * - 503 `STOREFRONT_PREVIEW_NOT_CONFIGURED` — 미리보기 서명 키가 설정되지 않았다
@@ -46,7 +47,7 @@ import { RESERVED_STORE_CODES } from "./store";
46
47
  * - 502 `STOREFRONT_SANDBOX_UNAVAILABLE` — 편집 Sandbox 호출이 실패했다. 잠시 뒤 다시 한다
47
48
  *
48
49
  * 소스 내려받기·올리기(#60, `/v1/storefront/site/source/**`, `sayren` CLI):
49
- * - 409 `SOURCE_CHANGED` — 작업 트리가 내려받은 뒤(`expectedBaseRef`) 바뀌었다(콘솔·AI 편집). `error.details.currentRef`
50
+ * - 409 `SOURCE_CHANGED` — 작업 트리가 내려받은 뒤(`expectedBaseRef`) 바뀌었다(상점 플랫폼·AI 편집). `error.details.currentRef`
50
51
  * - 409 `EDIT_SESSION_OPEN` — 편집 환경이 열려 있다. `endEditSession: true`로 끄고 진행할 수 있다
51
52
  * - 409 `SOURCE_BLOBS_MISSING` — 올리지 않은 파일 내용이 있다(`error.details.hashes`). `POST source/blobs`로 올리고 다시 한다
52
53
  * - 409 `DEPENDENCY_REJECTED` — 쓸 수 없는 npm 의존성이다(`error.details.rejected: [{ name, version, reason, detail }]`)
@@ -59,12 +60,12 @@ import { RESERVED_STORE_CODES } from "./store";
59
60
  // ─── 슬러그 ───────────────────────────────────────────────
60
61
 
61
62
  /**
62
- * 슬러그 형식 — 스토어 코드와 같은 DNS 라벨 규칙(소문자·숫자·하이픈 3~30자, 하이픈으로 시작·끝 불가)에
63
+ * 슬러그 형식 — 상점 코드와 같은 DNS 라벨 규칙(소문자·숫자·하이픈 3~30자, 하이픈으로 시작·끝 불가)에
63
64
  * 연속 하이픈 금지를 더한다. `--`는 버전 미리보기 주소(`v3--{slug}`)와 punycode(`xn--`)가 쓰는 구분자다.
64
65
  */
65
66
  export const STOREFRONT_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{1,28}[a-z0-9])$/;
66
67
 
67
- /** 호스팅 도메인에서 플랫폼이 쓰거나 쓸 이름 — 스토어 코드 예약어에 호스팅 전용 이름을 더한다 */
68
+ /** 호스팅 도메인에서 플랫폼이 쓰거나 쓸 이름 — 상점 코드 예약어에 호스팅 전용 이름을 더한다 */
68
69
  export const RESERVED_STOREFRONT_SLUGS: readonly string[] = [
69
70
  ...RESERVED_STORE_CODES,
70
71
  "mcp",
@@ -91,6 +92,16 @@ export const RESERVED_STOREFRONT_SLUGS: readonly string[] = [
91
92
  */
92
93
  export const STOREFRONT_EDIT_PREVIEW_SUFFIX = "-preview";
93
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
+
94
105
  export function isReservedStorefrontSlug(slug: string): boolean {
95
106
  const lower = slug.toLowerCase();
96
107
  return (
@@ -121,9 +132,9 @@ export const storefrontSlugSchema = z
121
132
  export const STOREFRONT_SLUG_HOLD_DAYS = 90;
122
133
  /** 슬러그 변경 한도 — 이 기간(일) 안에 */
123
134
  export const STOREFRONT_SLUG_CHANGE_WINDOW_DAYS = 30;
124
- /** 슬러그 변경 한도 — 횟수(스토어 단위. 끄고 다시 만들어도 이어서 센다) */
135
+ /** 슬러그 변경 한도 — 횟수(상점 단위. 끄고 다시 만들어도 이어서 센다) */
125
136
  export const STOREFRONT_SLUG_CHANGE_LIMIT = 3;
126
- /** 사이트 만들기 한도 — 같은 창(30일) 안의 횟수(스토어 단위). 끄고 만들기를 되풀이해 이름을 묶어 두지 못하게 한다 */
137
+ /** 사이트 만들기 한도 — 같은 창(30일) 안의 횟수(상점 단위). 끄고 만들기를 되풀이해 이름을 묶어 두지 못하게 한다 */
127
138
  export const STOREFRONT_SITE_CREATE_LIMIT = 3;
128
139
 
129
140
  // ─── 템플릿 ───────────────────────────────────────────────
@@ -135,7 +146,24 @@ export const storefrontTemplateSchema = z.object({
135
146
  description: z.string().describe("템플릿 설명"),
136
147
  thumbnailUrl: z.string().nullable().describe("썸네일 이미지 주소. 없으면 null"),
137
148
  version: z.string().describe("현재 템플릿 버전. 새 사이트는 이 버전으로 만든다"),
138
- isDefault: z.boolean().describe("기본 템플릿인가 (스토어 생성 시 자동 생성에 쓴다)"),
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"),
139
167
  });
140
168
  export type StorefrontTemplate = z.infer<typeof storefrontTemplateSchema>;
141
169
 
@@ -179,7 +207,7 @@ export const storefrontVersionSchema = z.object({
179
207
  .nullable()
180
208
  .optional()
181
209
  .describe(
182
- "만든 경로 (`CONSOLE` 콘솔 · `AI` AI 에이전트(MCP) · `CLI` `sayren` CLI · `API` 관리 API · `SYSTEM` 자동). 늘어날 수 있다",
210
+ "만든 경로 (`CONSOLE` 상점 플랫폼 · `AI` AI 에이전트(MCP) · `CLI` `sayren` CLI · `API` 관리 API · `SYSTEM` 자동). 늘어날 수 있다",
183
211
  ),
184
212
  createdAt: z.string().describe("만든 시각 (ISO 8601)"),
185
213
  });
@@ -189,7 +217,7 @@ export type StorefrontVersion = z.infer<typeof storefrontVersionSchema>;
189
217
 
190
218
  export const storefrontSiteStatusSchema = z
191
219
  .enum(["ACTIVE", "SUSPENDED", "CLOSED"])
192
- .describe("`ACTIVE` 운영 중 · `SUSPENDED` 운영자 정지 · `CLOSED` 호스팅 끔·스토어 폐쇄");
220
+ .describe("`ACTIVE` 운영 중 · `SUSPENDED` 운영자 정지 · `CLOSED` 호스팅 끔·상점 폐쇄");
193
221
  export type StorefrontSiteStatus = z.infer<typeof storefrontSiteStatusSchema>;
194
222
 
195
223
  export const storefrontVisibilitySchema = z
@@ -231,7 +259,7 @@ export type StorefrontProvisioning = z.infer<typeof storefrontProvisioningSchema
231
259
 
232
260
  /**
233
261
  * 편집 미리보기 주소(M2b) — 사이트마다 `{사이트 id 정규화}-preview.{호스팅 도메인}` 호스트 하나다. 호스팅 도메인의 와일드카드
234
- * 인증서가 덮으므로 따로 준비할 것이 없다. 이 주소는 편집 미리보기 링크(콘솔 전용)로 받은 쿠키가 있어야 열린다.
262
+ * 인증서가 덮으므로 따로 준비할 것이 없다. 이 주소는 편집 미리보기 링크(상점 플랫폼 전용)로 받은 쿠키가 있어야 열린다.
235
263
  */
236
264
  export const storefrontEditPreviewSchema = z.object({
237
265
  hostname: z
@@ -304,7 +332,7 @@ export const storefrontSlugAvailabilitySchema = z.object({
304
332
  export type StorefrontSlugAvailability = z.infer<typeof storefrontSlugAvailabilitySchema>;
305
333
 
306
334
  /**
307
- * 배포 진행 — 사이트의 지금 발행 상태가 공개 주소에 반영되기까지의 단계와 기록이다(콘솔의 스토어프론트 만들기 3단계가 폴링한다).
335
+ * 배포 진행 — 사이트의 지금 발행 상태가 공개 주소에 반영되기까지의 단계와 기록이다(상점 플랫폼의 스토어프론트 만들기 3단계가 폴링한다).
308
336
  * 만들기(`POST /v1/storefront/site`)는 곧바로 201로 답하고 반영은 그 뒤에 비동기로 하므로, 끝났는지는 이 조회로 본다.
309
337
  * 템플릿 사이트는 미리 빌드한 템플릿 산출물을 올리므로 빌드 단계가 없다. 기록은 사이트 상태에서 만든 요약이다.
310
338
  */
@@ -351,7 +379,7 @@ export type StorefrontDeployment = z.infer<typeof storefrontDeploymentSchema>;
351
379
 
352
380
  export const createStorefrontSiteRequestSchema = z.object({
353
381
  templateId: z.string().min(1).max(64).describe("템플릿 id"),
354
- slug: storefrontSlugSchema.optional().describe("주소 슬러그. 생략하면 스토어 코드"),
382
+ slug: storefrontSlugSchema.optional().describe("주소 슬러그. 생략하면 상점 코드"),
355
383
  });
356
384
  export type CreateStorefrontSiteRequest = z.infer<typeof createStorefrontSiteRequestSchema>;
357
385
 
@@ -411,7 +439,7 @@ export const storefrontSlugAvailabilityQuerySchema = z.object({
411
439
  });
412
440
 
413
441
  /**
414
- * 오픈 준비 중 미리보기 링크 — 콘솔에서 로그인한 OWNER·ADMIN만 받는다(PAT·에이전트 불가). 링크는 5분 안에 한 번 열면
442
+ * 오픈 준비 중 미리보기 링크 — 상점 플랫폼에서 로그인한 OWNER·ADMIN만 받는다(PAT·에이전트 불가). 링크는 5분 안에 한 번 열면
415
443
  * 그 브라우저에 12시간짜리 미리보기 쿠키가 생기고 오픈 준비 중인 사이트의 실제 화면을 본다. 링크를 남에게 넘기면 그 사람도
416
444
  * 볼 수 있으므로 공유하지 않는다.
417
445
  */
@@ -431,14 +459,14 @@ export type StorefrontPreviewLink = z.infer<typeof storefrontPreviewLinkSchema>;
431
459
  // ─── 내 도메인 (M3) ───────────────────────────────────────
432
460
  //
433
461
  // 셀러가 가진 도메인(예: `shop.brand.com`)을 사이트 주소로 연결한다. 흐름은 추가(`PENDING_DNS`) → 셀러가 DNS에 TXT(소유 확인)·
434
- // CNAME(연결)을 넣음 → 서버가 TXT를 확인하면 도메인을 이 스토어에 잡고 인증서 발급을 시작한다(`PENDING_VALIDATION`) → 인증서가
462
+ // CNAME(연결)을 넣음 → 서버가 TXT를 확인하면 도메인을 이 상점에 잡고 인증서 발급을 시작한다(`PENDING_VALIDATION`) → 인증서가
435
463
  // 발급되면 `ACTIVE`다. 확인은 서버가 주기적으로 하고 `verify`는 곧바로 한 번 더 본다. 연결된 도메인은 대표 주소로 지정할 수 있고,
436
464
  // 대표가 아닌 주소(기본 주소 포함)는 대표 주소로 301한다.
437
465
  //
438
- // - TXT 소유 확인은 도메인마다 새 값이다. CNAME만으로는 연결하지 않는다 — 연결을 끊은 뒤 CNAME이 남은 도메인을 다른 스토어가
466
+ // - TXT 소유 확인은 도메인마다 새 값이다. CNAME만으로는 연결하지 않는다 — 연결을 끊은 뒤 CNAME이 남은 도메인을 다른 상점이
439
467
  // 가져가지 못하게 한다.
440
- // - 소유 확인 전(`PENDING_DNS`)에는 도메인을 잡지 않는다. 같은 도메인을 여러 스토어가 추가해 둘 수 있고, 먼저 TXT 확인을 통과한
441
- // 스토어가 잡는다. 이미 다른 스토어가 잡은 도메인은 409 `STOREFRONT_DOMAIN_TAKEN`이다.
468
+ // - 소유 확인 전(`PENDING_DNS`)에는 도메인을 잡지 않는다. 같은 도메인을 여러 상점이 추가해 둘 수 있고, 먼저 TXT 확인을 통과한
469
+ // 상점이 잡는다. 이미 다른 상점이 잡은 도메인은 409 `STOREFRONT_DOMAIN_TAKEN`이다.
442
470
  // - 해제하면 곧바로 연결을 지운다. 연결된 뒤 DNS가 7일 넘게 이 서비스를 가리키지 않으면 서버가 연결을 해제한다.
443
471
  // - 결제 복귀·방문 분석·소셜 로그인 허용 목록은 연결된(`ACTIVE`) 도메인을 자동으로 더한다.
444
472
  //
@@ -446,11 +474,11 @@ export type StorefrontPreviewLink = z.infer<typeof storefrontPreviewLinkSchema>;
446
474
  // - 403 `STOREFRONT_CUSTOM_DOMAINS_DISABLED` — 플랫폼 스위치가 꺼져 도메인을 추가할 수 없다
447
475
  // - 400 `STOREFRONT_DOMAIN_INVALID` — 형식이 틀렸거나 연결할 수 없는 도메인이다(`error.details.reason`)
448
476
  // - 404 `STOREFRONT_DOMAIN_NOT_FOUND`
449
- // - 409 `STOREFRONT_DOMAIN_EXISTS` — 이 스토어에 이미 추가한 도메인이다
450
- // - 409 `STOREFRONT_DOMAIN_TAKEN` — 다른 스토어가 연결했거나 연결 중인 도메인이다
451
- // - 409 `STOREFRONT_DOMAIN_LIMIT` — 스토어당 도메인 수 상한(`error.details.limit`)
477
+ // - 409 `STOREFRONT_DOMAIN_EXISTS` — 이 상점에 이미 추가한 도메인이다
478
+ // - 409 `STOREFRONT_DOMAIN_TAKEN` — 다른 상점이 연결했거나 연결 중인 도메인이다
479
+ // - 409 `STOREFRONT_DOMAIN_LIMIT` — 상점당 도메인 수 상한(`error.details.limit`)
452
480
  // - 409 `STOREFRONT_DOMAIN_NOT_ACTIVE` — 연결되지 않았거나 경고(`failureCode`)가 있는 도메인은 대표 주소로 지정할 수 없다
453
- // - 429 `TOO_MANY_REQUESTS` — 재확인은 스토어당 분당 20번까지다(`error.details.retryAfterSeconds`)
481
+ // - 429 `TOO_MANY_REQUESTS` — 재확인은 상점당 분당 20번까지다(`error.details.retryAfterSeconds`)
454
482
  // - 400 `STOREFRONT_DOMAIN_NOT_REMOVABLE` — 기본 주소는 해제할 수 없다
455
483
 
456
484
  /**
@@ -547,7 +575,7 @@ export const STOREFRONT_PUBLIC_SUFFIXES: readonly string[] = [
547
575
  "imweb.me",
548
576
  ];
549
577
 
550
- /** 스토어당 연결할 수 있는 도메인 수 기본값 — 요금제(#44) 전의 상한이다. 실제 값은 `GET …/domains`의 `limit` */
578
+ /** 상점당 연결할 수 있는 도메인 수 기본값 — 요금제(#44) 전의 상한이다. 실제 값은 `GET …/domains`의 `limit` */
551
579
  export const STOREFRONT_CUSTOM_DOMAIN_LIMIT_DEFAULT = 2;
552
580
  /** DNS를 넣을 때까지 기다리는 기간(일) — 지나면 `FAILED`(`DNS_NOT_CONFIGURED`) */
553
581
  export const STOREFRONT_DOMAIN_DNS_WAIT_DAYS = 7;
@@ -655,7 +683,7 @@ export const storefrontSiteDomainSchema = z.object({
655
683
  .string()
656
684
  .nullable()
657
685
  .describe(
658
- "실패·경고 코드. `DNS_NOT_CONFIGURED` 기한 안에 TXT를 찾지 못함 · `VALIDATION_TIMEOUT` 인증서 발급 기한 지남 · `CERTIFICATE_FAILED` 인증서 발급 실패 · `DNS_NOT_POINTING` 연결된 뒤 DNS가 이 서비스를 가리키지 않음(기한이 지나면 해제) · `DOMAIN_TAKEN` 다른 스토어가 먼저 소유를 확인함 · `HOSTNAME_BLOCKED` 연결이 막힘 · `HOSTNAME_CONFLICT` 연결 준비 중 충돌(운영 확인). 늘어날 수 있다",
686
+ "실패·경고 코드. `DNS_NOT_CONFIGURED` 기한 안에 TXT를 찾지 못함 · `VALIDATION_TIMEOUT` 인증서 발급 기한 지남 · `CERTIFICATE_FAILED` 인증서 발급 실패 · `DNS_NOT_POINTING` 연결된 뒤 DNS가 이 서비스를 가리키지 않음(기한이 지나면 해제) · `DOMAIN_TAKEN` 다른 상점이 먼저 소유를 확인함 · `HOSTNAME_BLOCKED` 연결이 막힘 · `HOSTNAME_CONFLICT` 연결 준비 중 충돌(운영 확인). 늘어날 수 있다",
659
687
  ),
660
688
  verifiedAt: z.string().nullable().describe("소유를 확인한 시각 (ISO 8601)"),
661
689
  activatedAt: z.string().nullable().describe("연결된 시각 (ISO 8601)"),
@@ -667,11 +695,7 @@ export type StorefrontSiteDomain = z.infer<typeof storefrontSiteDomainSchema>;
667
695
  /** `GET /v1/storefront/site/domains` — 사이트의 주소 목록(기본 주소 + 내 도메인)과 연결 설정 */
668
696
  export const storefrontSiteDomainsSchema = z.object({
669
697
  enabled: z.boolean().describe("내 도메인을 추가할 수 있는가 (플랫폼 스위치)"),
670
- limit: z
671
- .number()
672
- .int()
673
- .min(0)
674
- .describe("스토어당 내 도메인 수 상한 (해제한 도메인은 세지 않는다)"),
698
+ limit: z.number().int().min(0).describe("상점당 내 도메인 수 상한 (해제한 도메인은 세지 않는다)"),
675
699
  target: z.string().describe("CNAME 대상 (예: `sites.sayren.co`)"),
676
700
  domains: z.array(storefrontSiteDomainSchema).describe("대표 주소 먼저, 그다음 추가한 순서"),
677
701
  });
@@ -688,11 +712,11 @@ export type AddStorefrontDomainRequest = z.infer<typeof addStorefrontDomainReque
688
712
 
689
713
  // ─── 편집 (M2b) ───────────────────────────────────────────
690
714
  //
691
- // 셀러(콘솔)와 에이전트(MCP)가 사이트의 작업 트리를 고치고 버전을 만든다. 작업 트리는 사이트당 하나이고 편집 Sandbox
715
+ // 셀러(상점 플랫폼)와 에이전트(MCP)가 사이트의 작업 트리를 고치고 버전을 만든다. 작업 트리는 사이트당 하나이고 편집 Sandbox
692
716
  // (격리된 컨테이너)가 dev 서버로 띄운다. 편집 환경은 플랫폼 전체 동시 상한이 있고 15분 동안 활동이 없으면 잠든다(작업
693
717
  // 트리는 보존된다). 버전 만들기는 그 시점의 작업 트리를 고정해 일회용 빌드 환경에서 규칙 검증 → 타입 검사 → 빌드를
694
718
  // 하고, 통과하면 발행할 수 있는 `READY` 버전이 된다. 에이전트의 파일 쓰기·검사·버전 만들기는 승인 없이 되고, 발행·
695
- // 되돌리기·공개 전환은 기존대로 승인이다. 편집 미리보기 링크는 콘솔 OWNER·ADMIN만 받는다(에이전트에게 주지 않는다).
719
+ // 되돌리기·공개 전환은 기존대로 승인이다. 편집 미리보기 링크는 상점 플랫폼 OWNER·ADMIN만 받는다(에이전트에게 주지 않는다).
696
720
 
697
721
  /** 플랫폼 전체 동시 편집 환경 상한 */
698
722
  export const STOREFRONT_EDIT_CAPACITY = 20;
@@ -785,7 +809,7 @@ export const storefrontEditSessionStatusSchema = z
785
809
  .enum(["STARTING", "RUNNING"])
786
810
  .describe("`STARTING` 띄우는 중 · `RUNNING` 사용 중");
787
811
 
788
- /** 편집 환경을 띄우는 단계 — 콘솔 진행 표시용이다. 값은 늘어날 수 있다 */
812
+ /** 편집 환경을 띄우는 단계 — 상점 플랫폼 진행 표시용이다. 값은 늘어날 수 있다 */
789
813
  export const STOREFRONT_EDIT_START_STAGES = ["CONTAINER", "RESTORE", "DEV_SERVER"] as const;
790
814
 
791
815
  export const storefrontEditSessionSchema = z.object({
@@ -1224,7 +1248,7 @@ export const storefrontVersionCreatedSchema = z.object({
1224
1248
  export type StorefrontVersionCreated = z.infer<typeof storefrontVersionCreatedSchema>;
1225
1249
 
1226
1250
  /**
1227
- * `POST /v1/storefront/site/editing/preview-link` — 편집 미리보기(dev 서버) 링크. 콘솔에서 로그인한 OWNER·ADMIN만
1251
+ * `POST /v1/storefront/site/editing/preview-link` — 편집 미리보기(dev 서버) 링크. 상점 플랫폼에서 로그인한 OWNER·ADMIN만
1228
1252
  * 받는다(PAT·에이전트 불가). 5분 안에 열면 그 브라우저에 미리보기 호스트 전용 쿠키(12시간)가 생긴다. 편집 환경이
1229
1253
  * 없으면 409 `EDIT_SESSION_REQUIRED`다(편집 환경이 `STARTING`이면 링크는 주고, 미리보기 화면이 기동 대기를 보여 준다).
1230
1254
  */
@@ -1234,7 +1258,7 @@ export type CreateStorefrontEditPreviewLinkRequest = CreateStorefrontPreviewLink
1234
1258
 
1235
1259
  // ─── 소스 내려받기·올리기 (#60, `sayren` CLI) ─────────────────────────────
1236
1260
  //
1237
- // 셀러가 사이트 작업 트리를 로컬로 받아(`npx sayren pull`) 고치고 되돌려 놓는다(`npx sayren deploy`). 콘솔·AI 편집과 같은
1261
+ // 셀러가 사이트 작업 트리를 로컬로 받아(`npx sayren pull`) 고치고 되돌려 놓는다(`npx sayren deploy`). 상점 플랫폼·AI 편집과 같은
1238
1262
  // 작업 트리다. 올리는 것은 소스뿐이고 빌드는 서버가 한다(버전 만들기). 흐름:
1239
1263
  // 1. `GET source` — 지금 작업 트리를 고정하고(`sourceRef`) 압축 파일 크기를 알린다
1240
1264
  // 2. `GET source/archive?sourceRef=&part=` — 압축 파일(tar.gz)을 4MiB씩 받는다
@@ -1258,7 +1282,7 @@ export const storefrontSourceRefSchema = z
1258
1282
  export const storefrontSourceSchema = z.object({
1259
1283
  siteId: z.string(),
1260
1284
  storeId: z.string(),
1261
- storeCode: z.string().describe("스토어 코드 — 로컬 개발 서버의 `SAYREN_STORE_CODE`"),
1285
+ storeCode: z.string().describe("상점 코드 — 로컬 개발 서버의 `SAYREN_STORE_CODE`"),
1262
1286
  sourceRef: storefrontSourceRefSchema,
1263
1287
  treeSha256: z.string().describe("트리 지문 (경로 오름차순 `{path}\\t{sha256}\\n`의 SHA-256)"),
1264
1288
  files: z.number().int().min(0).describe("파일 수"),
@@ -0,0 +1,222 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * 요금제·구독 — `GET /v1/store/subscription`(스코프 `store:r`). 구독 단위는 상점이다.
5
+ * 결제 수단·청구 금액은 싣지 않는다. 요금제 변경은 상점 플랫폼에서만 한다.
6
+ */
7
+
8
+ /** 요금제 코드 — 이름(`plan.name`)이 바뀌어도 코드는 바뀌지 않는다. 값은 추가만 한다 */
9
+ export const planCodeSchema = z
10
+ .enum(["BETA", "FREE", "BASIC", "PRO", "BUSINESS", "ENTERPRISE", "PREMIUM"])
11
+ .describe(
12
+ "요금제 코드. `FREE`=Free, `PRO`=Pro, `PREMIUM`=프리미엄, `ENTERPRISE`=엔터프라이즈, `BETA`=베타(운영 오픈 기간). `BASIC`·`BUSINESS`는 2026-09-30 이전 요금제(더 쓰지 않는다)",
13
+ );
14
+ export type PlanCode = z.infer<typeof planCodeSchema>;
15
+
16
+ /**
17
+ * 응답의 요금제 코드 — 아는 코드(`PlanCode`)거나 이 SDK가 모르는 새 코드(문자열)다. 요금제 코드는 추가될 수 있어 응답 파싱을 깨지 않게
18
+ * 넓게 받는다(옛 SDK는 새 코드를 모르면 파싱에 실패했다, #44 리뷰 M3). 요청에는 `planCodeSchema`를 쓴다
19
+ */
20
+ export const responsePlanCodeSchema = z
21
+ .union([planCodeSchema, z.string()])
22
+ .describe(planCodeSchema.description ?? "요금제 코드");
23
+ export type ResponsePlanCode = PlanCode | (string & {});
24
+
25
+ export const subscriptionStatusSchema = z
26
+ .enum(["ACTIVE", "PAST_DUE", "CANCELED", "INCOMPLETE"])
27
+ .describe(
28
+ "구독 상태. `ACTIVE`=이용 중, `PAST_DUE`=결제 실패 후 유예 중, `CANCELED`=해지됨, `INCOMPLETE`=첫 결제 대기",
29
+ );
30
+ export type SubscriptionStatus = z.infer<typeof subscriptionStatusSchema>;
31
+
32
+ const limit = (description: string) =>
33
+ z.number().int().nullable().describe(`${description}. null이면 한도 없음`);
34
+
35
+ export const subscriptionLimitsSchema = z.object({
36
+ liveOrdersMonthly: limit("더 쓰지 않는다 — 라이브 주문 수로 요금제를 나누지 않아 항상 null"),
37
+ seatsIncluded: limit("구성원 석 — 구성원은 모든 요금제에서 무료·무제한이라 null"),
38
+ aiActionsMonthly: limit("MCP(에이전트) 쓰기(AI 작업) 월 한도"),
39
+ aiReadsMonthly: limit("MCP(에이전트) 읽기 월 한도"),
40
+ customDomains: limit("내 도메인 수"),
41
+ webhooks: limit("웹훅 구독 수(0이면 쓸 수 없다)"),
42
+ socialProviders: limit("더 쓰지 않는다 — 소셜 로그인 공급자 수로 요금제를 나누지 않아 항상 null"),
43
+ emailsMonthly: limit("더 쓰지 않는다 — 메일 한도가 없어 항상 null"),
44
+ storefrontRequestsMonthly: limit("스토어프론트 월 트래픽(요청 수, 넘어도 막지 않고 안내만)"),
45
+ apiLogRetentionDays: limit("API 로그 보관 일수(0이면 API 로그 없음)"),
46
+ auditLogRetentionDays: limit("감사 로그 보관 일수"),
47
+ storefrontPreviewHoursMonthly: limit("스토어프론트 편집 미리보기 월 시간"),
48
+ storefrontBuildMinutesMonthly: limit("스토어프론트 빌드 월 분"),
49
+ adminApiRequestsMonthly: limit("관리 API 요청 월 포함량(넘으면 속도만 낮춘다, 주문 처리 제외)"),
50
+ });
51
+ export type SubscriptionLimits = z.infer<typeof subscriptionLimitsSchema>;
52
+
53
+ export const subscriptionUsageSchema = z.object({
54
+ liveOrders: z.number().int().describe("이번 기간 라이브 주문 수(테스트 결제 주문 제외)"),
55
+ aiActions: z.number().int().describe("이번 기간 MCP(에이전트) 쓰기(AI 작업) 수"),
56
+ aiReads: z.number().int().describe("이번 기간 MCP(에이전트) 읽기 수"),
57
+ storefrontRequests: z.number().int().describe("이번 기간 스토어프론트 요청 수"),
58
+ adminApiRequests: z
59
+ .number()
60
+ .int()
61
+ .describe("이번 기간 관리 API 요청 수(API 토큰·앱·에이전트 호출, 상점 플랫폼 제외)"),
62
+ emails: z.number().int().describe("이번 기간 초대·양도 알림 메일 수(한도 없음, 표시만)"),
63
+ storefrontPreviewMinutes: z.number().int().describe("이번 기간 편집 미리보기 분(끝난 세션)"),
64
+ storefrontBuildMinutes: z.number().int().describe("이번 기간 빌드 분(끝난 빌드)"),
65
+ seats: z.number().int().describe("현재 석 수 — 구성원(OWNER 포함) + 대기 중 초대"),
66
+ customDomains: z.number().int().describe("현재 내 도메인 수(해제한 도메인 제외)"),
67
+ webhooks: z.number().int().describe("현재 웹훅 구독 수"),
68
+ });
69
+ export type SubscriptionUsage = z.infer<typeof subscriptionUsageSchema>;
70
+
71
+ export const storeSubscriptionSchema = z.object({
72
+ plan: z.object({
73
+ code: responsePlanCodeSchema,
74
+ name: z.string().describe("요금제 표시 이름(예: 프로)"),
75
+ }),
76
+ status: subscriptionStatusSchema,
77
+ interval: z
78
+ .enum(["MONTH"])
79
+ .nullable()
80
+ .describe("결제 주기. `MONTH`=월간. 요금을 내지 않는 요금제(베타·무료)는 null"),
81
+ cancelAtPeriodEnd: z.boolean().describe("기간 끝에 해지 예정인가"),
82
+ period: z
83
+ .object({
84
+ start: z.string().describe("사용량 기간 시작(ISO 8601)"),
85
+ end: z.string().describe("사용량 기간 끝(ISO 8601, 이 시각에 사용량이 초기화된다)"),
86
+ })
87
+ .describe("사용량을 세는 기간. 요금을 내지 않는 요금제는 한국 시간 달력 월이다"),
88
+ limits: subscriptionLimitsSchema,
89
+ features: z.object({
90
+ adminApi: z.boolean().describe("관리 API(개인 액세스 토큰·외부 앱 토큰 호출)를 쓸 수 있다"),
91
+ analytics: z.boolean().describe("애널리틱스 조회를 쓸 수 있다"),
92
+ storeClone: z.boolean().describe("더 쓰지 않는다 — 상점 복제 기능을 없애 항상 false"),
93
+ removeBadge: z.boolean().describe("더 쓰지 않는다 — 배지 키를 없애 항상 false"),
94
+ }),
95
+ usage: subscriptionUsageSchema,
96
+ aiActions: z
97
+ .object({
98
+ limit: z.number().int().nullable(),
99
+ used: z.number().int(),
100
+ resetsAt: z.string().describe("한도가 초기화되는 시각(ISO 8601)"),
101
+ })
102
+ .describe("AI 작업(MCP 쓰기) 한도 요약 — 에이전트가 남은 작업 수를 판단할 때 쓴다"),
103
+ aiReads: z
104
+ .object({
105
+ limit: z.number().int().nullable(),
106
+ used: z.number().int(),
107
+ resetsAt: z.string().describe("한도가 초기화되는 시각(ISO 8601)"),
108
+ })
109
+ .describe("MCP 읽기 한도 요약"),
110
+ salesFeePercent: z.literal(0).describe("판매 수수료(%). 모든 요금제 0이다"),
111
+ });
112
+ export type StoreSubscription = z.infer<typeof storeSubscriptionSchema>;
113
+
114
+ /**
115
+ * 요금제 한도 오류의 `error.details` — 기존 오류 코드(`STORE_LIMIT_EXCEEDED`·`STOREFRONT_DOMAIN_LIMIT`·`WEBHOOK_LIMIT_EXCEEDED` 등)와
116
+ * 요금제 오류(`PLAN_LIMIT_EXCEEDED`·`PLAN_AI_ACTIONS_EXCEEDED`·`PLAN_AI_READS_EXCEEDED`·`PLAN_FEATURE_REQUIRED`·`PLAN_API_QUOTA_EXCEEDED`)가 같은 형식을 싣는다.
117
+ * 모든 필드가 선택이다(기존 코드는 요금제 판정 전 필드만 실을 수 있다).
118
+ */
119
+ export const planErrorDetailsSchema = z
120
+ .object({
121
+ feature: z.string().optional().describe("한도에 닿은 기능 키(예: `limits.ai_actions_monthly`)"),
122
+ limit: z.number().nullable().optional(),
123
+ used: z.number().optional(),
124
+ plan: responsePlanCodeSchema.optional(),
125
+ resetsAt: z.string().optional().describe("월 한도가 초기화되는 시각(ISO 8601)"),
126
+ upgradeUrl: z.string().optional().describe("요금제 화면 주소(상점 플랫폼)"),
127
+ reason: z
128
+ .string()
129
+ .optional()
130
+ .describe(
131
+ "상점 수 한도의 이유 — `OWNED_STORES`(소유 상점 수). `FREE_STORES`는 더 쓰지 않는다",
132
+ ),
133
+ })
134
+ .loose();
135
+ export type PlanErrorDetails = z.infer<typeof planErrorDetailsSchema>;
136
+
137
+ const limitOrContract = (description: string) =>
138
+ z.number().int().nullable().describe(`${description}. null이면 계약(엔터프라이즈)·한도 없음`);
139
+
140
+ /**
141
+ * 요금제 기능 키 값 — 요금제 비교표·상점 플랫폼 요금제 화면이 쓴다. 키는 서버 판정의 기능 키와 같다(추가만 한다). 상위 요금제는 하위 요금제
142
+ * 기능을 모두 포함한다
143
+ */
144
+ export const planEntitlementsSchema = z
145
+ .object({
146
+ "features.admin_api": z
147
+ .boolean()
148
+ .describe(
149
+ "관리 API(개인 액세스 토큰·외부 앱 토큰 호출). 상점 플랫폼·CLI·MCP는 모든 요금제에서 쓴다",
150
+ ),
151
+ "features.analytics": z.boolean().describe("애널리틱스 화면·조회 API"),
152
+ "limits.ai_reads_monthly": limitOrContract("MCP 읽기 월 한도"),
153
+ "limits.ai_actions_monthly": limitOrContract(
154
+ "MCP 쓰기(AI 작업) 월 한도 — 주문 처리는 막지 않는다",
155
+ ),
156
+ "limits.custom_domains": limitOrContract("내 도메인 수"),
157
+ "limits.webhooks": limitOrContract("웹훅 구독 수(0이면 쓸 수 없다)"),
158
+ "limits.storefront_preview_hours_monthly": limitOrContract("편집 환경 시간(월, 시간)"),
159
+ "limits.storefront_build_minutes_monthly": limitOrContract("빌드 시간(월, 분)"),
160
+ "limits.storefront_requests_monthly": limitOrContract(
161
+ "스토어프론트 월 트래픽(요청 수) — 넘어도 막지 않고 안내만 한다",
162
+ ),
163
+ "limits.admin_api_requests_monthly": limitOrContract(
164
+ "관리 API 월 요청 — 넘으면 속도만 낮춘다(주문 처리 제외)",
165
+ ),
166
+ "rate_limit.admin_api": z
167
+ .number()
168
+ .nullable()
169
+ .describe("관리 API 분당 속도 배수(기본 분당 600 × 배수)"),
170
+ "storefront.cpu_ms": z.number().int().describe("스토어프론트 사이트 요청당 CPU 상한(ms)"),
171
+ "storefront.subrequests": z
172
+ .number()
173
+ .int()
174
+ .describe("스토어프론트 사이트 요청당 하위 요청 상한"),
175
+ "storefront.rate_limit_per_minute": z
176
+ .number()
177
+ .int()
178
+ .describe("스토어프론트 남용 방지 요청 속도 상한(사이트당 분당)"),
179
+ "seats.included": limitOrContract(
180
+ "포함 구성원 석 — 구성원은 모든 요금제에서 무료·무제한이라 null",
181
+ ),
182
+ "retention.analytics_days": z.number().int().describe("애널리틱스 보관 일수"),
183
+ "retention.api_logs_days": limitOrContract("API 로그 보관 일수(0이면 API 로그 없음)"),
184
+ "retention.audit_days": limitOrContract("감사 로그 보관 일수"),
185
+ "support.tier": z
186
+ .union([z.enum(["COMMUNITY", "EMAIL", "EMAIL_PRIORITY", "DEDICATED", "SLA"]), z.string()])
187
+ .describe(
188
+ "지원 등급(`COMMUNITY`·`EMAIL`·`EMAIL_PRIORITY`·`DEDICATED`·`SLA`, 새 등급이 더해질 수 있다)",
189
+ ),
190
+ })
191
+ .describe("요금제 기능 키 값");
192
+ export type PlanEntitlementValues = z.infer<typeof planEntitlementsSchema>;
193
+
194
+ /** 공개 요금제 한 줄 — `GET /v1/public/plans` */
195
+ export const publicPlanSchema = z.object({
196
+ code: responsePlanCodeSchema,
197
+ name: z.string().describe("표시 이름(Free·Pro·프리미엄·엔터프라이즈)"),
198
+ version: z.number().int().describe("요금제 버전 — 값이 바뀌면 새 버전이 게시된다"),
199
+ sort: z.number().int(),
200
+ purchasable: z
201
+ .boolean()
202
+ .describe("셀프 결제로 고를 수 있는가(Free·엔터프라이즈는 아니다 — 엔터프라이즈는 계약)"),
203
+ prices: z.object({
204
+ currency: z.literal("KRW"),
205
+ monthly: z.number().int().nullable().describe("월 가격(공급가, 부가세 별도)"),
206
+ startingAt: z.boolean().describe("「부터」 가격(엔터프라이즈 계약)"),
207
+ vatIncluded: z
208
+ .object({
209
+ monthly: z.number().int().nullable(),
210
+ })
211
+ .describe("표시 가격(부가세 포함) — 요금제 비교표는 이 값을 보인다"),
212
+ }),
213
+ entitlements: planEntitlementsSchema,
214
+ });
215
+ export type PublicPlan = z.infer<typeof publicPlanSchema>;
216
+
217
+ /** 요금제 비교표 — `GET /v1/public/plans`(인증 없음, 캐시 가능). 값은 서버가 관리하고 언제든 바뀔 수 있다 */
218
+ export const publicPlansSchema = z.object({
219
+ vatPercent: z.number().int().describe("부가세율(%)"),
220
+ plans: z.array(publicPlanSchema).describe("공개 요금제의 지금 버전 — 정렬 순"),
221
+ });
222
+ export type PublicPlans = z.infer<typeof publicPlansSchema>;
@@ -8,7 +8,7 @@ import { z } from "zod";
8
8
  * 세는 것이다. 그 숫자가 규칙과 안내를 고치는 근거가 된다.
9
9
  *
10
10
  * **보내지 않는 것**: 파일 경로·소스 코드·위반 상세(`detail`)·토큰·사람 식별자. 규칙 id와 개수뿐이다.
11
- * 테넌트는 요청 토큰이 정한다(본문에 스토어 id가 없다). 옵트아웃은 클라이언트의 `SAYREN_TELEMETRY=0`이다.
11
+ * 테넌트는 요청 토큰이 정한다(본문에 상점 id가 없다). 옵트아웃은 클라이언트의 `SAYREN_TELEMETRY=0`이다.
12
12
  *
13
13
  * 알 수 없는 키는 저장하지 않고 버린다(`.strict()`를 쓰지 않는다) — 새 버전 클라이언트가 필드를
14
14
  * 하나 더 실었다고 계측 전체가 400으로 날아가면 그 버전의 숫자가 통째로 빈다.
@@ -64,7 +64,7 @@ export const mcpToolEventSchema = z.object({
64
64
  results: z
65
65
  .array(mcpRuleResultSchema)
66
66
  .max(MCP_TELEMETRY_MAX_RESULTS)
67
- // 규칙 하나당 한 줄이다. 같은 id를 여러 줄로 실으면 집계가 한 스토어의 한 규칙을 몇 번 세야
67
+ // 규칙 하나당 한 줄이다. 같은 id를 여러 줄로 실으면 집계가 한 상점의 한 규칙을 몇 번 세야
68
68
  // 하는지 정할 수 없고, 상한만큼 같은 규칙을 반복해 보내는 것이 저장을 부풀리는 길이 된다.
69
69
  .refine((results) => new Set(results.map((result) => result.ruleId)).size === results.length, {
70
70
  message: "같은 규칙 id를 두 번 실을 수 없습니다",
@@ -52,7 +52,7 @@ export interface UploadRejection {
52
52
 
53
53
  /**
54
54
  * 용도 제한 위반 여부. 통과하면 `null`.
55
- * 서버가 던지는 오류와 콘솔이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
55
+ * 서버가 던지는 오류와 상점 플랫폼이 먼저 띄우는 안내가 갈리지 않도록 한 함수로 판정한다.
56
56
  */
57
57
  export function uploadRejection(
58
58
  purpose: UploadPurpose,
@@ -28,7 +28,7 @@ export {
28
28
  const envelopeFields = {
29
29
  id: z.string().describe("이벤트 ID (`evt_…`, 테스트 전송은 `evt_test_…`). 중복 제거 키"),
30
30
  createdAt: z.string().describe("이벤트 발생 일시 (ISO 8601)"),
31
- storeId: z.string().describe("이벤트가 발생한 스토어 ID"),
31
+ storeId: z.string().describe("이벤트가 발생한 상점 ID"),
32
32
  sequence: z
33
33
  .number()
34
34
  .int()
package/src/server.ts CHANGED
@@ -5,8 +5,7 @@
5
5
 
6
6
  export {
7
7
  type DelegatedMemberToken,
8
- delegateMemberByEmail,
9
- delegateMemberByUid,
8
+ delegateMember,
10
9
  MemberDelegationError,
11
10
  } from "./member-delegation";
12
11
  export {