@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/dist/index.d.mts +637 -193
- package/dist/index.mjs +2534 -2369
- package/package.json +1 -1
- package/src/client.ts +31 -0
- package/src/schemas/app-manifest.ts +101 -1
- package/src/schemas/apps.ts +122 -0
package/package.json
CHANGED
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,
|
package/src/schemas/apps.ts
CHANGED
|
@@ -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(),
|