@sayren/store-sdk 0.30.10 → 0.30.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sayren/store-sdk",
3
- "version": "0.30.10",
3
+ "version": "0.30.12",
4
4
  "main": "./dist/index.mjs",
5
5
  "types": "./dist/index.d.mts",
6
6
  "exports": {
package/src/client.ts CHANGED
@@ -23,11 +23,14 @@ import {
23
23
  import { type ApiLogListOptions, apiLogPageSchema, apiLogSummarySchema } from "./schemas/api-log";
24
24
  import {
25
25
  type AppInstallationSettingsUpdate,
26
+ appInstallationActivitySchema,
26
27
  appInstallationSchema,
27
28
  appInstallationSelfSchema,
28
29
  appInstallationSettingsUpdateSchema,
29
30
  appInstallRequestSchema,
30
31
  appSessionSchema,
32
+ appStorageEntrySchema,
33
+ appStorageKeysSchema,
31
34
  storeAppDetailSchema,
32
35
  storeAppSchema,
33
36
  } from "./schemas/apps";
@@ -1469,6 +1472,20 @@ export function createStoreClient(options: StoreClientOptions) {
1469
1472
  ),
1470
1473
  remove: (installationId: string) =>
1471
1474
  storeHttp.requestVoid("DELETE", path`/apps/installations/${installationId}`),
1475
+ /** 실행 기록 — 최근 이벤트 전송 결과와 최근 7일 관리 API 호출 수 */
1476
+ activity: (installationId: string) =>
1477
+ storeHttp.request(
1478
+ "GET",
1479
+ path`/apps/installations/${installationId}/activity`,
1480
+ appInstallationActivitySchema,
1481
+ ),
1482
+ /** 권한 업데이트 동의 — 넓어진 새 버전(`pendingVersion`)으로 옮긴다(상점 플랫폼 전용) */
1483
+ consent: (installationId: string) =>
1484
+ storeHttp.request(
1485
+ "POST",
1486
+ path`/apps/installations/${installationId}/consent`,
1487
+ appInstallationSchema,
1488
+ ),
1472
1489
  /** 앱 화면 iframe에 넘길 세션 토큰(5분) */
1473
1490
  session: (installationId: string) =>
1474
1491
  storeHttp.request(
@@ -1481,6 +1498,20 @@ export function createStoreClient(options: StoreClientOptions) {
1481
1498
  /** 앱 설치 토큰 전용 — 자기 설치 */
1482
1499
  app: {
1483
1500
  installation: () => storeHttp.request("GET", "/app/installation", appInstallationSelfSchema),
1501
+ /** 설치 저장소(`ctx.storage`) — 없는 키는 404 `APP_STORAGE_KEY_NOT_FOUND`, 한도 초과는 409 `APP_STORAGE_LIMIT` */
1502
+ storage: {
1503
+ list: (params: { prefix?: string } = {}) =>
1504
+ storeHttp.request("GET", "/app/storage", appStorageKeysSchema, {
1505
+ query: params as unknown as Query,
1506
+ }),
1507
+ get: (key: string) =>
1508
+ storeHttp.request("GET", path`/app/storage/${key}`, appStorageEntrySchema),
1509
+ set: (key: string, value: unknown) =>
1510
+ storeHttp.request("PUT", path`/app/storage/${key}`, appStorageEntrySchema, {
1511
+ body: { value },
1512
+ }),
1513
+ delete: (key: string) => storeHttp.requestVoid("DELETE", path`/app/storage/${key}`),
1514
+ },
1484
1515
  },
1485
1516
 
1486
1517
  webhooks: {
@@ -233,6 +233,28 @@ export const APP_SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
233
233
  export const APP_SECRETS_MAX = 20;
234
234
  export const APP_EVENTS_MAX = 50;
235
235
  export const APP_NETWORK_HOSTS_MAX = 20;
236
+ export const APP_PUBLIC_HTTP_MAX = 20;
237
+ export const APP_SCHEDULES_MAX = 10;
238
+
239
+ /** HTTP 핸들러 키 — `"<메서드> <경로>"`(`@sayren/app` `defineApp({ http })`와 같은 형식) */
240
+ export const APP_HTTP_KEY_PATTERN = /^(GET|POST|PUT|PATCH|DELETE) (\/[A-Za-z0-9._~:/-]*)$/;
241
+ /** 예약 작업 이름 — 설정 키와 같은 형식 */
242
+ export const APP_SCHEDULE_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]{0,39}$/;
243
+ /** 예약 작업 간격 → 분 */
244
+ export const APP_SCHEDULE_INTERVALS = {
245
+ "15m": 15,
246
+ "30m": 30,
247
+ "1h": 60,
248
+ "3h": 180,
249
+ "6h": 360,
250
+ "12h": 720,
251
+ "1d": 1440,
252
+ } as const;
253
+ export type AppScheduleInterval = keyof typeof APP_SCHEDULE_INTERVALS;
254
+ const APP_SCHEDULE_INTERVAL_KEYS = Object.keys(APP_SCHEDULE_INTERVALS) as [
255
+ AppScheduleInterval,
256
+ ...AppScheduleInterval[],
257
+ ];
236
258
 
237
259
  /** 앱 목록에 보이는 아이콘·README(#22 앱 목록 시안) — 앱 폴더의 파일을 `sayren app deploy`가 버전과 함께 올린다 */
238
260
  export const APP_ICON_MAX_BYTES = 256 * 1024;
@@ -299,6 +321,22 @@ export const appManifestSchema = z.strictObject({
299
321
  adminPage: z.strictObject({ entry: z.string().regex(/^ui\/[A-Za-z0-9._/-]+\.html$/) }),
300
322
  })
301
323
  .optional(),
324
+ /** 공개 HTTP — 여기 적은 핸들러만 `https://{handle}.{앱 도메인}/hooks/{설치 id}/…`로 외부에서 부를 수 있다 */
325
+ http: z
326
+ .strictObject({
327
+ public: z.array(z.string().regex(APP_HTTP_KEY_PATTERN)).max(APP_PUBLIC_HTTP_MAX),
328
+ })
329
+ .optional(),
330
+ /** 예약 작업 — 이름마다 실행 간격 */
331
+ schedules: z
332
+ .record(
333
+ z.string().regex(APP_SCHEDULE_NAME_PATTERN),
334
+ z.strictObject({ every: z.enum(APP_SCHEDULE_INTERVAL_KEYS) }),
335
+ )
336
+ .refine((schedules) => Object.keys(schedules).length <= APP_SCHEDULES_MAX, {
337
+ message: `예약 작업은 ${APP_SCHEDULES_MAX}개까지입니다`,
338
+ })
339
+ .default({}),
302
340
  settings: z
303
341
  .record(z.string().regex(APP_SETTING_KEY_PATTERN), appSettingFieldSchema)
304
342
  .refine((fields) => Object.keys(fields).length <= APP_SETTINGS_MAX_FIELDS, {
@@ -351,6 +389,7 @@ export function appManifestProblems(manifest: AppManifest): AppManifestProblem[]
351
389
  unique(manifest.events, "events", "이벤트");
352
390
  unique(manifest.network.hosts, "network.hosts", "호스트");
353
391
  unique(manifest.secrets, "secrets", "비밀 값 이름");
392
+ unique(manifest.http?.public ?? [], "http.public", "공개 HTTP 핸들러");
354
393
  for (const event of manifest.events) {
355
394
  if (!scopesCoverEvent(manifest.scopes, event)) {
356
395
  problems.push({
@@ -399,6 +438,10 @@ export interface AppCapabilities {
399
438
  events: AppEventType[];
400
439
  networkHosts: string[];
401
440
  adminPage: boolean;
441
+ /** 공개 HTTP 핸들러 키(`"GET /track/:id"`). 옛 버전 행에는 없다 — 읽을 때 `?? []` */
442
+ publicHttp?: string[];
443
+ /** 예약 작업 이름 → 간격. 옛 버전 행에는 없다 — 읽을 때 `?? {}` */
444
+ schedules?: Record<string, AppScheduleInterval>;
402
445
  /** 필수 설정 키 — 하나라도 비어 있으면 이벤트 함수를 부르지 않는다 */
403
446
  requiredSettings: string[];
404
447
  secretSettings: string[];
@@ -411,6 +454,12 @@ export function appCapabilitiesOf(manifest: AppManifest): AppCapabilities {
411
454
  events: sorted(manifest.events) as AppEventType[],
412
455
  networkHosts: sorted(manifest.network.hosts),
413
456
  adminPage: Boolean(manifest.ui?.adminPage),
457
+ publicHttp: sorted(manifest.http?.public ?? []),
458
+ schedules: Object.fromEntries(
459
+ Object.entries(manifest.schedules)
460
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
461
+ .map(([name, schedule]) => [name, schedule.every]),
462
+ ),
414
463
  requiredSettings: Object.entries(manifest.settings)
415
464
  .filter(([, field]) => field.required)
416
465
  .map(([key]) => key)
@@ -427,7 +476,7 @@ export function appCapabilitiesOf(manifest: AppManifest): AppCapabilities {
427
476
  * 동의 문단은 accounts 설치 화면·상점 플랫폼 앱 상세·유저 문서가 같은 문장을 쓴다.
428
477
  */
429
478
  export interface AppCapabilityDefinition {
430
- key: "scopes" | "network" | "events" | "ui";
479
+ key: "scopes" | "network" | "events" | "ui" | "publicHttp" | "schedules";
431
480
  /** 동의 화면의 문단 제목 */
432
481
  title: string;
433
482
  /** 동의 화면에 보일 줄 — 없으면 문단을 그리지 않는다 */
@@ -473,8 +522,59 @@ export const APP_CAPABILITIES: readonly AppCapabilityDefinition[] = [
473
522
  describe: (c) => (c.adminPage ? ["상점 플랫폼에 이 앱의 화면을 더합니다"] : []),
474
523
  widens: (p, n) => !p.adminPage && n.adminPage,
475
524
  },
525
+ {
526
+ key: "publicHttp",
527
+ title: "외부에서 이 앱을 부르는 주소",
528
+ describe: (c) => c.publicHttp ?? [],
529
+ widens: (p, n) => grew(p.publicHttp ?? [], n.publicHttp ?? []),
530
+ },
531
+ {
532
+ key: "schedules",
533
+ title: "주기적으로 실행되는 작업",
534
+ describe: (c) =>
535
+ Object.entries(c.schedules ?? {}).map(
536
+ ([name, every]) => `${name} · ${appScheduleIntervalLabel(every)}마다`,
537
+ ),
538
+ widens: (p, n) => {
539
+ const previous = p.schedules ?? {};
540
+ return Object.entries(n.schedules ?? {}).some(
541
+ ([name, every]) =>
542
+ !previous[name] ||
543
+ APP_SCHEDULE_INTERVALS[every] <
544
+ APP_SCHEDULE_INTERVALS[previous[name] as AppScheduleInterval],
545
+ );
546
+ },
547
+ },
476
548
  ];
477
549
 
550
+ /** 요청(메서드·경로)이 HTTP 핸들러 키에 맞는가 — `:이름`은 경로 한 마디에 맞는다(`@sayren/app`의 핸들러 고르기와 같은 규칙) */
551
+ export function appHttpKeyMatches(key: string, method: string, path: string): boolean {
552
+ const match = APP_HTTP_KEY_PATTERN.exec(key.trim().replace(/\s+/g, " "));
553
+ if (!match || match[1] !== method.toUpperCase()) return false;
554
+ const pattern = (match[2] as string).split("/");
555
+ const actual = (path.split("?")[0] ?? path).split("/");
556
+ if (pattern.length !== actual.length) return false;
557
+ return pattern.every((part, i) => (part.startsWith(":") ? actual[i] !== "" : part === actual[i]));
558
+ }
559
+
560
+ /** 응답용 예약 작업 목록(이름순) — 옛 버전 행(`schedules` 없음)은 빈 목록 */
561
+ export function appScheduleListOf(
562
+ capabilities: Pick<AppCapabilities, "schedules">,
563
+ ): Array<{ name: string; every: AppScheduleInterval }> {
564
+ // jsonb는 키를 길이순으로 저장한다 — 이름순으로 맞춘다
565
+ return Object.entries(capabilities.schedules ?? {})
566
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
567
+ .map(([name, every]) => ({ name, every }));
568
+ }
569
+
570
+ /** 예약 간격의 표시 이름 — 동의 화면·상점 플랫폼이 같이 쓴다 */
571
+ export function appScheduleIntervalLabel(every: AppScheduleInterval): string {
572
+ const minutes = APP_SCHEDULE_INTERVALS[every];
573
+ if (minutes < 60) return `${minutes}분`;
574
+ if (minutes < 1440) return `${minutes / 60}시간`;
575
+ return "하루";
576
+ }
577
+
478
578
  /** 재동의가 필요한 capability 키 — 비어 있으면 release가 모든 설치에 곧바로 반영된다 */
479
579
  export function appManifestWidens(
480
580
  previous: AppCapabilities,
@@ -7,6 +7,7 @@ import {
7
7
  appHandleSchema,
8
8
  appSettingFieldSchema,
9
9
  } from "./app-manifest";
10
+ import { webhookDeliveryErrorCodeSchema, webhookDeliveryStatusSchema } from "./misc";
10
11
 
11
12
  /**
12
13
  * 설치 앱 관리 API 계약 (이슈 #22) — `/v1/apps/installations/**`(상점이 설치한 앱), `/v1/app/installation`(설치 토큰 자기 조회).
@@ -28,6 +29,10 @@ export const appInstallationStatusSchema = z.enum(["ACTIVE", "UNINSTALLED", "SUS
28
29
  export type AppInstallationStatus = z.infer<typeof appInstallationStatusSchema>;
29
30
 
30
31
  /** 설치 앱의 앱 정보 — 상점이 보는 범위만(개발자 비밀 값·소스·빌드는 없다) */
32
+ /** 상점이 보는 앱 공개 범위 — 개발 중(개발자 조직) · 이 상점 전용 · 공개 */
33
+ export const appVisibilitySchema = z.enum(["DEVELOPMENT", "PRIVATE", "PUBLIC"]);
34
+ export type AppVisibility = z.infer<typeof appVisibilitySchema>;
35
+
31
36
  export const appInstallationAppSchema = z.object({
32
37
  appId: z.string(),
33
38
  handle: z.string().describe("앱 handle. 앱 화면 주소의 라벨"),
@@ -60,6 +65,22 @@ export const appInstallationSettingsUpdateSchema = z.object({
60
65
  });
61
66
  export type AppInstallationSettingsUpdate = z.infer<typeof appInstallationSettingsUpdateSchema>;
62
67
 
68
+ /** 예약 작업 한 줄 — 매니페스트 `schedules`의 이름과 간격 */
69
+ export const appScheduleSummarySchema = z.object({
70
+ name: z.string(),
71
+ every: z.enum(["15m", "30m", "1h", "3h", "6h", "12h", "1d"]),
72
+ });
73
+ export type AppScheduleSummary = z.infer<typeof appScheduleSummarySchema>;
74
+
75
+ const publicHttpField = z
76
+ .array(z.string())
77
+ .default([])
78
+ .describe("외부에서 부를 수 있는 HTTP 핸들러(`GET /track/:id`)");
79
+ const schedulesField = z
80
+ .array(appScheduleSummarySchema)
81
+ .default([])
82
+ .describe("주기적으로 실행되는 작업");
83
+
63
84
  export const appInstallationSchema = z.object({
64
85
  installationId: z.string(),
65
86
  storeId: z.string(),
@@ -71,10 +92,24 @@ export const appInstallationSchema = z.object({
71
92
  .int()
72
93
  .nullable()
73
94
  .describe("재동의가 필요한 새 버전. 없으면 null"),
95
+ pendingVersion: z
96
+ .object({
97
+ number: z.number().int(),
98
+ scopes: z.array(z.string()),
99
+ networkHosts: z.array(z.string()),
100
+ events: z.array(appEventTypeSchema),
101
+ publicHttp: publicHttpField,
102
+ schedules: schedulesField,
103
+ })
104
+ .nullable()
105
+ .optional()
106
+ .describe("재동의가 필요한 새 버전이 요청하는 권한·외부 호스트·이벤트(전체). 없으면 null"),
74
107
  grantedScopes: z.array(z.string()).describe("동의한 스코프"),
75
108
  grantedNetworkHosts: z.array(z.string()).describe("동의한 외부 호출 호스트"),
76
109
  events: z.array(appEventTypeSchema).describe("이 설치가 받는 이벤트"),
77
110
  adminPage: z.boolean().describe("상점 플랫폼 앱 화면이 있는가"),
111
+ publicHttp: publicHttpField,
112
+ schedules: schedulesField,
78
113
  /** 설치 결과의 캐시가 아니라 실행 버전 매니페스트의 요약 */
79
114
  settings: appInstallationSettingsSchema,
80
115
  installedBy: z.object({ userId: z.string(), name: z.string().nullable() }).nullable(),
@@ -84,6 +119,26 @@ export const appInstallationSchema = z.object({
84
119
  });
85
120
  export type AppInstallation = z.infer<typeof appInstallationSchema>;
86
121
 
122
+ /** 실행 기록 한 줄 — 앱에 보낸 이벤트 하나의 전송 결과. 로그 본문은 개발자만 본다(매뉴얼 「앱 설치와 관리」) */
123
+ export const appInstallationRunSchema = z.object({
124
+ deliveryId: z.string(),
125
+ eventType: z.string().describe("이벤트 이름(`ORDER.PAID`·`APP.INSTALLED` 등)"),
126
+ status: webhookDeliveryStatusSchema,
127
+ errorCode: webhookDeliveryErrorCodeSchema.nullable(),
128
+ attemptCount: z.number().int(),
129
+ createdAt: z.string(),
130
+ attemptedAt: z.string().nullable(),
131
+ nextAttemptAt: z.string().nullable(),
132
+ });
133
+ export type AppInstallationRun = z.infer<typeof appInstallationRunSchema>;
134
+
135
+ /** `GET /v1/apps/installations/{id}/activity` — 최근 이벤트 전송 결과(50건)와 최근 7일 관리 API 호출 수 */
136
+ export const appInstallationActivitySchema = z.object({
137
+ runs: z.array(appInstallationRunSchema),
138
+ apiCalls7d: z.number().int().describe("최근 7일 이 설치가 부른 관리 API 수"),
139
+ });
140
+ export type AppInstallationActivity = z.infer<typeof appInstallationActivitySchema>;
141
+
87
142
  /**
88
143
  * `POST /v1/apps/installations`(상점 플랫폼 전용) — 둘 중 하나다.
89
144
  * - `{ result }`: accounts 설치 동의 결과(1회용 JWT). 개발자 포털 [내 상점에 설치]가 이 길로 온다
@@ -127,6 +182,9 @@ export const storeAppSchema = z.object({
127
182
  developerName: z.string(),
128
183
  versionNumber: z.number().int().describe("현재 버전"),
129
184
  installationId: z.string().nullable().describe("이 상점에 설치돼 있으면 설치 id"),
185
+ visibility: appVisibilitySchema
186
+ .default("DEVELOPMENT")
187
+ .describe("공개 범위 — 개발 중(심사 전) · 이 상점 전용 · 공개(심사 통과)"),
130
188
  });
131
189
  export type StoreApp = z.infer<typeof storeAppSchema>;
132
190
 
@@ -137,6 +195,8 @@ export const storeAppDetailSchema = storeAppSchema.extend({
137
195
  scopes: z.array(z.string()).describe("설치 때 동의하는 스코프(Scope)"),
138
196
  events: z.array(appEventTypeSchema).describe("받는 이벤트(Hook)"),
139
197
  networkHosts: z.array(z.string()).describe("앱이 데이터를 보내는 외부 호스트"),
198
+ publicHttp: publicHttpField,
199
+ schedules: schedulesField,
140
200
  settings: z.record(z.string(), appSettingFieldSchema).describe("설치 때 채우는 설정 필드"),
141
201
  updatedAt: z.string().nullable().describe("현재 버전을 release한 시각"),
142
202
  });
@@ -162,6 +222,33 @@ export const appInstallationSelfSchema = z.object({
162
222
  });
163
223
  export type AppInstallationSelf = z.infer<typeof appInstallationSelfSchema>;
164
224
 
225
+ /** 설치 저장소(`ctx.storage`, 매뉴얼 「앱 코드 › 저장소」) — 설치 토큰 전용 */
226
+ export const APP_STORAGE_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,200}$/;
227
+ export const APP_STORAGE_MAX_KEYS = 1_000;
228
+ /** 설치당 전체 크기(키 + JSON 값, UTF-8 바이트) */
229
+ export const APP_STORAGE_MAX_BYTES = 1024 * 1024;
230
+
231
+ export const appStorageEntrySchema = z.object({
232
+ key: z.string(),
233
+ value: z.unknown().describe("저장한 JSON 값"),
234
+ updatedAt: z.string(),
235
+ });
236
+ export type AppStorageEntry = z.infer<typeof appStorageEntrySchema>;
237
+
238
+ export const appStorageKeysSchema = z.object({
239
+ keys: z.array(z.string()).describe("키 목록(이름순)"),
240
+ });
241
+ export type AppStorageKeys = z.infer<typeof appStorageKeysSchema>;
242
+
243
+ export const appStoragePutSchema = z.object({
244
+ value: z.unknown().refine((value) => value !== undefined, { message: "value가 필요합니다" }),
245
+ });
246
+ export type AppStoragePut = z.infer<typeof appStoragePutSchema>;
247
+
248
+ export const appStorageListQuerySchema = z.object({
249
+ prefix: z.string().max(200).optional(),
250
+ });
251
+
165
252
  export type { AppCapabilities };
166
253
 
167
254
  // ─── 개발자 API (플랫폼 플레인 `/platform/v1/developer/**`, 공개 문서 제외 — CLI·상점 플랫폼 개발자 화면이 쓴다) ───
@@ -218,6 +305,8 @@ export const appVersionSchema = z.object({
218
305
  events: z.array(z.string()),
219
306
  networkHosts: z.array(z.string()),
220
307
  adminPage: z.boolean(),
308
+ publicHttp: z.array(z.string()).optional(),
309
+ schedules: z.record(z.string(), z.string()).optional(),
221
310
  requiredSettings: z.array(z.string()),
222
311
  secretSettings: z.array(z.string()),
223
312
  }),
@@ -233,6 +322,19 @@ export const appVersionSchema = z.object({
233
322
  });
234
323
  export type AppVersion = z.infer<typeof appVersionSchema>;
235
324
 
325
+ /** 앱 공개 심사(#22, 매뉴얼 「앱 공개 범위」) — 개발자가 신청하고 운영자가 승인·반려한다 */
326
+ export const appListingReviewStatusSchema = z.enum(["PENDING", "APPROVED", "REJECTED"]);
327
+ export type AppListingReviewStatus = z.infer<typeof appListingReviewStatusSchema>;
328
+ export const appListingReviewSchema = z.object({
329
+ reviewId: z.string(),
330
+ status: appListingReviewStatusSchema,
331
+ versionNumber: z.number().int().describe("신청 때 현재 버전"),
332
+ reason: z.string().nullable().describe("반려 사유"),
333
+ requestedAt: z.string(),
334
+ reviewedAt: z.string().nullable(),
335
+ });
336
+ export type AppListingReview = z.infer<typeof appListingReviewSchema>;
337
+
236
338
  export const developerAppSchema = z.object({
237
339
  appId: z.string(),
238
340
  orgId: z.string(),
@@ -246,6 +348,15 @@ export const developerAppSchema = z.object({
246
348
  status: appStatusSchema,
247
349
  suspendedReason: z.string().nullable(),
248
350
  currentVersionNumber: z.number().int().nullable(),
351
+ privateStoreId: z
352
+ .string()
353
+ .nullable()
354
+ .default(null)
355
+ .describe("비공개 앱이 묶인 상점. 개발 중·퍼블릭 앱은 null"),
356
+ listingReview: appListingReviewSchema
357
+ .nullable()
358
+ .default(null)
359
+ .describe("마지막 공개 신청. 없으면 null"),
249
360
  installations: z.object({
250
361
  active: z.number().int(),
251
362
  pendingReconsent: z.number().int(),
@@ -259,6 +370,8 @@ export const developerAppCreateSchema = z.object({
259
370
  handle: appHandleSchema,
260
371
  name: z.string().trim().min(1).max(60),
261
372
  description: z.string().trim().max(500).optional(),
373
+ /** 비공개 앱 — 이 상점 하나에만 설치된다. 만드는 사람이 그 상점의 OWNER·ADMIN이어야 한다 */
374
+ privateStoreId: z.string().min(1).optional(),
262
375
  });
263
376
  export type DeveloperAppCreate = z.infer<typeof developerAppCreateSchema>;
264
377
 
@@ -314,6 +427,15 @@ export const devStoreSchema = z.object({
314
427
  });
315
428
  export type DevStore = z.infer<typeof devStoreSchema>;
316
429
 
430
+ /** `GET /platform/v1/developer/stores` — 비공개 앱을 묶을 수 있는 상점(내가 소유자·관리자, 운영 중) */
431
+ export const developerStoreSchema = z.object({
432
+ storeId: z.string(),
433
+ name: z.string(),
434
+ role: z.enum(["OWNER", "ADMIN"]),
435
+ development: z.boolean().describe("개발 상점인가"),
436
+ });
437
+ export type DeveloperStore = z.infer<typeof developerStoreSchema>;
438
+
317
439
  /** 개발자가 보는 설치 한 줄 */
318
440
  export const developerInstallationSchema = z.object({
319
441
  installationId: z.string(),