@sayren/store-sdk 0.30.9 → 0.30.11
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 +840 -459
- package/dist/index.mjs +2460 -2344
- package/package.json +1 -1
- package/src/client.ts +32 -3
- package/src/schemas/app-manifest.ts +78 -1
- package/src/schemas/apps.ts +103 -3
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
|
+
storeAppDetailSchema,
|
|
33
|
+
storeAppSchema,
|
|
31
34
|
} from "./schemas/apps";
|
|
32
35
|
import {
|
|
33
36
|
type ApprovalDecisionRequest,
|
|
@@ -1432,11 +1435,23 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
1432
1435
|
|
|
1433
1436
|
/** 설치 앱(#22, 매뉴얼 「앱 설치와 관리 › 관리 API」) — 설치·설정·제거·세션은 상점 플랫폼 OWNER·ADMIN만 */
|
|
1434
1437
|
apps: {
|
|
1438
|
+
/** 이 상점에 설치할 수 있는 앱 — 앱 목록·상세(아이콘·README·Scope·Hook·설정 필드) */
|
|
1439
|
+
catalog: {
|
|
1440
|
+
list: () => storeHttp.request("GET", "/apps/catalog", z.array(storeAppSchema)),
|
|
1441
|
+
get: (appId: string) =>
|
|
1442
|
+
storeHttp.request("GET", path`/apps/catalog/${appId}`, storeAppDetailSchema),
|
|
1443
|
+
},
|
|
1435
1444
|
installations: {
|
|
1436
|
-
/**
|
|
1437
|
-
|
|
1445
|
+
/**
|
|
1446
|
+
* 설치 — 문자열이면 accounts 동의 화면의 설치 결과(1회용 JWT), 객체면 상점 플랫폼 설치 화면에서 동의한 앱과 설정이다
|
|
1447
|
+
*/
|
|
1448
|
+
install: (
|
|
1449
|
+
input: string | { appId: string; settings?: AppInstallationSettingsUpdate["values"] },
|
|
1450
|
+
) =>
|
|
1438
1451
|
storeHttp.request("POST", "/apps/installations", appInstallationSchema, {
|
|
1439
|
-
body: appInstallRequestSchema.parse(
|
|
1452
|
+
body: appInstallRequestSchema.parse(
|
|
1453
|
+
typeof input === "string" ? { result: input } : input,
|
|
1454
|
+
),
|
|
1440
1455
|
}),
|
|
1441
1456
|
list: () => storeHttp.request("GET", "/apps/installations", z.array(appInstallationSchema)),
|
|
1442
1457
|
get: (installationId: string) =>
|
|
@@ -1455,6 +1470,20 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
1455
1470
|
),
|
|
1456
1471
|
remove: (installationId: string) =>
|
|
1457
1472
|
storeHttp.requestVoid("DELETE", path`/apps/installations/${installationId}`),
|
|
1473
|
+
/** 실행 기록 — 최근 이벤트 전송 결과와 최근 7일 관리 API 호출 수 */
|
|
1474
|
+
activity: (installationId: string) =>
|
|
1475
|
+
storeHttp.request(
|
|
1476
|
+
"GET",
|
|
1477
|
+
path`/apps/installations/${installationId}/activity`,
|
|
1478
|
+
appInstallationActivitySchema,
|
|
1479
|
+
),
|
|
1480
|
+
/** 권한 업데이트 동의 — 넓어진 새 버전(`pendingVersion`)으로 옮긴다(상점 플랫폼 전용) */
|
|
1481
|
+
consent: (installationId: string) =>
|
|
1482
|
+
storeHttp.request(
|
|
1483
|
+
"POST",
|
|
1484
|
+
path`/apps/installations/${installationId}/consent`,
|
|
1485
|
+
appInstallationSchema,
|
|
1486
|
+
),
|
|
1458
1487
|
/** 앱 화면 iframe에 넘길 세션 토큰(5분) */
|
|
1459
1488
|
session: (installationId: string) =>
|
|
1460
1489
|
storeHttp.request(
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { isStoreScope, STORE_SCOPE_RESOURCES, type StoreScopeResource } from "./scope";
|
|
3
3
|
import { describeScope } from "./scope-descriptions";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
KNOWN_WEBHOOK_EVENT_TYPES,
|
|
6
|
+
type WebhookEventOf,
|
|
7
|
+
type WebhookEventType,
|
|
8
|
+
} from "./webhook-event";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* 서드파티 앱 매니페스트 `sayren.app.json` (이슈 #22) — CLI(`sayren app`)·api(빌드 `VERIFY`·버전 판정·동의 화면 자료)·런타임이
|
|
@@ -84,6 +88,36 @@ export const APP_LIFECYCLE_EVENT_TYPES = [
|
|
|
84
88
|
export type AppLifecycleEventType = (typeof APP_LIFECYCLE_EVENT_TYPES)[number];
|
|
85
89
|
export type AppEventType = WebhookEventType | AppLifecycleEventType;
|
|
86
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 생명주기 이벤트의 `data`(매뉴얼 「앱 이벤트」 표). api가 보내는 값과 `@sayren/app`의 핸들러 타입이 이 한 곳을 쓴다.
|
|
93
|
+
* 키를 바꾸면 앱 코드가 깨진다 — 더하기만 한다
|
|
94
|
+
*/
|
|
95
|
+
export interface AppLifecycleEventDataMap {
|
|
96
|
+
"APP.INSTALLED": {
|
|
97
|
+
installationId: string;
|
|
98
|
+
storeId: string;
|
|
99
|
+
versionNumber: number;
|
|
100
|
+
grantedScopes: string[];
|
|
101
|
+
};
|
|
102
|
+
"APP.SETTINGS_UPDATED": { installationId: string; storeId: string; changedKeys: string[] };
|
|
103
|
+
"APP.SCOPES_UPDATED": {
|
|
104
|
+
installationId: string;
|
|
105
|
+
storeId: string;
|
|
106
|
+
versionNumber: number;
|
|
107
|
+
grantedScopes: string[];
|
|
108
|
+
};
|
|
109
|
+
"APP.UNINSTALLED": { installationId: string; storeId: string };
|
|
110
|
+
"APP.STORE_REDACT": { installationId: string; storeId: string };
|
|
111
|
+
"APP.CUSTOMER_REDACT": { storeId: string; memberId: string };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 이벤트 타입 하나의 `data` — 웹훅 이벤트는 `WebhookEventOf<T>["data"]`, 생명주기 이벤트는 위 표다 */
|
|
115
|
+
export type AppEventDataOf<T extends AppEventType> = T extends AppLifecycleEventType
|
|
116
|
+
? AppLifecycleEventDataMap[T]
|
|
117
|
+
: T extends WebhookEventType
|
|
118
|
+
? WebhookEventOf<T>["data"]
|
|
119
|
+
: never;
|
|
120
|
+
|
|
87
121
|
/**
|
|
88
122
|
* 이벤트를 받는 데 필요한 스코프 — 동의 스코프가 이것을 덮지 못하면 분배하지 않고, 매니페스트 `scopes`가 덮지 못하면 빌드가 실패한다.
|
|
89
123
|
* 생명주기 이벤트는 스코프가 없다(null). 새 웹훅 이벤트를 더하면 이 표도 더해야 테스트가 통과한다
|
|
@@ -200,6 +234,37 @@ export const APP_SECRETS_MAX = 20;
|
|
|
200
234
|
export const APP_EVENTS_MAX = 50;
|
|
201
235
|
export const APP_NETWORK_HOSTS_MAX = 20;
|
|
202
236
|
|
|
237
|
+
/** 앱 목록에 보이는 아이콘·README(#22 앱 목록 시안) — 앱 폴더의 파일을 `sayren app deploy`가 버전과 함께 올린다 */
|
|
238
|
+
export const APP_ICON_MAX_BYTES = 256 * 1024;
|
|
239
|
+
export const APP_ICON_MIN_PX = 512;
|
|
240
|
+
export const APP_README_MAX_BYTES = 64 * 1024;
|
|
241
|
+
export const APP_ICON_CONTENT_TYPES = ["image/png", "image/svg+xml"] as const;
|
|
242
|
+
export type AppIconContentType = (typeof APP_ICON_CONTENT_TYPES)[number];
|
|
243
|
+
const APP_FOLDER_FILE = /^(?!\/)(?!.*\.\.)[A-Za-z0-9._/-]+$/;
|
|
244
|
+
|
|
245
|
+
/** 아이콘 파일 검사 — 문제가 없으면 null. CLI `app verify`·`deploy`와 api 빌드 `VERIFY`가 같은 규칙을 쓴다 */
|
|
246
|
+
export function appIconProblem(bytes: Uint8Array, contentType: AppIconContentType): string | null {
|
|
247
|
+
if (bytes.byteLength === 0) return "아이콘 파일이 비어 있습니다";
|
|
248
|
+
if (bytes.byteLength > APP_ICON_MAX_BYTES) {
|
|
249
|
+
return `아이콘이 ${Math.ceil(bytes.byteLength / 1024)}KB입니다. 상한은 ${APP_ICON_MAX_BYTES / 1024}KB입니다`;
|
|
250
|
+
}
|
|
251
|
+
if (contentType === "image/svg+xml") {
|
|
252
|
+
const head = new TextDecoder().decode(bytes.subarray(0, 1024));
|
|
253
|
+
return /<svg[\s>]/i.test(head) ? null : "SVG 파일이 아닙니다";
|
|
254
|
+
}
|
|
255
|
+
const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
256
|
+
if (bytes.byteLength < 24 || signature.some((b, i) => bytes[i] !== b))
|
|
257
|
+
return "PNG 파일이 아닙니다";
|
|
258
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
259
|
+
const width = view.getUint32(16);
|
|
260
|
+
const height = view.getUint32(20);
|
|
261
|
+
if (width !== height) return `아이콘은 정사각형이어야 합니다(지금 ${width}×${height})`;
|
|
262
|
+
if (width < APP_ICON_MIN_PX) {
|
|
263
|
+
return `아이콘은 ${APP_ICON_MIN_PX}×${APP_ICON_MIN_PX} 이상이어야 합니다(지금 ${width}×${height})`;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
203
268
|
export const appManifestSchema = z.strictObject({
|
|
204
269
|
handle: appHandleSchema,
|
|
205
270
|
name: z.string().trim().min(1).max(60),
|
|
@@ -207,6 +272,18 @@ export const appManifestSchema = z.strictObject({
|
|
|
207
272
|
/** https 이미지 주소. 동의 화면·상점 플랫폼 목록에 보인다 */
|
|
208
273
|
iconUrl: z.url().startsWith("https://").optional(),
|
|
209
274
|
supportEmail: z.email().optional(),
|
|
275
|
+
/** 앱 폴더의 아이콘 파일(PNG·SVG, 정사각형, PNG는 512px 이상, 256KB 이하). 있으면 `iconUrl`보다 먼저 보인다 */
|
|
276
|
+
icon: z
|
|
277
|
+
.string()
|
|
278
|
+
.regex(APP_FOLDER_FILE)
|
|
279
|
+
.refine((path) => /\.(png|svg)$/i.test(path), { message: "아이콘은 .png 또는 .svg 파일입니다" })
|
|
280
|
+
.optional(),
|
|
281
|
+
/** 앱 상세에 보일 README(마크다운, 64KB 이하). 적지 않으면 앱 폴더의 README.md를 쓴다 */
|
|
282
|
+
readme: z
|
|
283
|
+
.string()
|
|
284
|
+
.regex(APP_FOLDER_FILE)
|
|
285
|
+
.refine((path) => /\.md$/i.test(path), { message: "README는 .md 파일입니다" })
|
|
286
|
+
.optional(),
|
|
210
287
|
/** 실행 위치 — A1은 코드 업로드 앱만. 외부 런타임(`external`)은 A3에서 연다 */
|
|
211
288
|
runtime: z.literal("code").default("code"),
|
|
212
289
|
/** 설치 때 전체 동의하는 스코프 */
|
package/src/schemas/apps.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
|
+
APP_ICON_CONTENT_TYPES,
|
|
4
|
+
APP_README_MAX_BYTES,
|
|
3
5
|
type AppCapabilities,
|
|
4
6
|
appEventTypeSchema,
|
|
5
7
|
appHandleSchema,
|
|
6
8
|
appSettingFieldSchema,
|
|
7
9
|
} from "./app-manifest";
|
|
10
|
+
import { webhookDeliveryErrorCodeSchema, webhookDeliveryStatusSchema } from "./misc";
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* 설치 앱 관리 API 계약 (이슈 #22) — `/v1/apps/installations/**`(상점이 설치한 앱), `/v1/app/installation`(설치 토큰 자기 조회).
|
|
@@ -69,6 +72,16 @@ export const appInstallationSchema = z.object({
|
|
|
69
72
|
.int()
|
|
70
73
|
.nullable()
|
|
71
74
|
.describe("재동의가 필요한 새 버전. 없으면 null"),
|
|
75
|
+
pendingVersion: z
|
|
76
|
+
.object({
|
|
77
|
+
number: z.number().int(),
|
|
78
|
+
scopes: z.array(z.string()),
|
|
79
|
+
networkHosts: z.array(z.string()),
|
|
80
|
+
events: z.array(appEventTypeSchema),
|
|
81
|
+
})
|
|
82
|
+
.nullable()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("재동의가 필요한 새 버전이 요청하는 권한·외부 호스트·이벤트(전체). 없으면 null"),
|
|
72
85
|
grantedScopes: z.array(z.string()).describe("동의한 스코프"),
|
|
73
86
|
grantedNetworkHosts: z.array(z.string()).describe("동의한 외부 호출 호스트"),
|
|
74
87
|
events: z.array(appEventTypeSchema).describe("이 설치가 받는 이벤트"),
|
|
@@ -82,12 +95,84 @@ export const appInstallationSchema = z.object({
|
|
|
82
95
|
});
|
|
83
96
|
export type AppInstallation = z.infer<typeof appInstallationSchema>;
|
|
84
97
|
|
|
85
|
-
/**
|
|
86
|
-
export const
|
|
87
|
-
|
|
98
|
+
/** 실행 기록 한 줄 — 앱에 보낸 이벤트 하나의 전송 결과. 로그 본문은 개발자만 본다(매뉴얼 「앱 설치와 관리」) */
|
|
99
|
+
export const appInstallationRunSchema = z.object({
|
|
100
|
+
deliveryId: z.string(),
|
|
101
|
+
eventType: z.string().describe("이벤트 이름(`ORDER.PAID`·`APP.INSTALLED` 등)"),
|
|
102
|
+
status: webhookDeliveryStatusSchema,
|
|
103
|
+
errorCode: webhookDeliveryErrorCodeSchema.nullable(),
|
|
104
|
+
attemptCount: z.number().int(),
|
|
105
|
+
createdAt: z.string(),
|
|
106
|
+
attemptedAt: z.string().nullable(),
|
|
107
|
+
nextAttemptAt: z.string().nullable(),
|
|
108
|
+
});
|
|
109
|
+
export type AppInstallationRun = z.infer<typeof appInstallationRunSchema>;
|
|
110
|
+
|
|
111
|
+
/** `GET /v1/apps/installations/{id}/activity` — 최근 이벤트 전송 결과(50건)와 최근 7일 관리 API 호출 수 */
|
|
112
|
+
export const appInstallationActivitySchema = z.object({
|
|
113
|
+
runs: z.array(appInstallationRunSchema),
|
|
114
|
+
apiCalls7d: z.number().int().describe("최근 7일 이 설치가 부른 관리 API 수"),
|
|
88
115
|
});
|
|
116
|
+
export type AppInstallationActivity = z.infer<typeof appInstallationActivitySchema>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* `POST /v1/apps/installations`(상점 플랫폼 전용) — 둘 중 하나다.
|
|
120
|
+
* - `{ result }`: accounts 설치 동의 결과(1회용 JWT). 개발자 포털 [내 상점에 설치]가 이 길로 온다
|
|
121
|
+
* - `{ appId, settings }`: 상점 플랫폼 앱 목록 › 설치 화면에서 동의하고 설정까지 채워 바로 설치한다
|
|
122
|
+
*/
|
|
123
|
+
export const appInstallRequestSchema = z.union([
|
|
124
|
+
z.object({
|
|
125
|
+
result: z
|
|
126
|
+
.string()
|
|
127
|
+
.min(1)
|
|
128
|
+
.describe("accounts가 발급한 설치 결과 JWT(`typ: sayren-install+jwt`)"),
|
|
129
|
+
}),
|
|
130
|
+
z.object({
|
|
131
|
+
appId: z.string().min(1),
|
|
132
|
+
settings: z
|
|
133
|
+
.record(z.string(), settingsValueSchema)
|
|
134
|
+
.default({})
|
|
135
|
+
.describe("설치와 함께 저장할 설정. 비밀 필드도 여기서 넣는다"),
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
89
138
|
export type AppInstallRequest = z.infer<typeof appInstallRequestSchema>;
|
|
90
139
|
|
|
140
|
+
/** 앱 목록 정보 — 현재 버전이 올린 아이콘·README(없으면 매니페스트 `iconUrl`) */
|
|
141
|
+
export const appListingSchema = z.object({
|
|
142
|
+
description: z.string().nullable(),
|
|
143
|
+
iconUrl: z
|
|
144
|
+
.string()
|
|
145
|
+
.nullable()
|
|
146
|
+
.describe("아이콘 주소. 앱 폴더에서 올린 아이콘은 data URI, 매니페스트 `iconUrl`은 https 주소"),
|
|
147
|
+
readme: z.string().nullable().describe("README 마크다운 원문"),
|
|
148
|
+
});
|
|
149
|
+
export type AppListing = z.infer<typeof appListingSchema>;
|
|
150
|
+
|
|
151
|
+
/** `GET /v1/apps/catalog` — 이 상점에 설치할 수 있는 앱(A1: 내가 속한 개발자 조직의 앱) */
|
|
152
|
+
export const storeAppSchema = z.object({
|
|
153
|
+
appId: z.string(),
|
|
154
|
+
handle: z.string(),
|
|
155
|
+
name: z.string(),
|
|
156
|
+
description: z.string().nullable(),
|
|
157
|
+
iconUrl: z.string().nullable(),
|
|
158
|
+
developerName: z.string(),
|
|
159
|
+
versionNumber: z.number().int().describe("현재 버전"),
|
|
160
|
+
installationId: z.string().nullable().describe("이 상점에 설치돼 있으면 설치 id"),
|
|
161
|
+
});
|
|
162
|
+
export type StoreApp = z.infer<typeof storeAppSchema>;
|
|
163
|
+
|
|
164
|
+
/** `GET /v1/apps/catalog/{appId}` — 앱 상세·설치 화면 */
|
|
165
|
+
export const storeAppDetailSchema = storeAppSchema.extend({
|
|
166
|
+
readme: z.string().nullable(),
|
|
167
|
+
supportEmail: z.string().nullable(),
|
|
168
|
+
scopes: z.array(z.string()).describe("설치 때 동의하는 스코프(Scope)"),
|
|
169
|
+
events: z.array(appEventTypeSchema).describe("받는 이벤트(Hook)"),
|
|
170
|
+
networkHosts: z.array(z.string()).describe("앱이 데이터를 보내는 외부 호스트"),
|
|
171
|
+
settings: z.record(z.string(), appSettingFieldSchema).describe("설치 때 채우는 설정 필드"),
|
|
172
|
+
updatedAt: z.string().nullable().describe("현재 버전을 release한 시각"),
|
|
173
|
+
});
|
|
174
|
+
export type StoreAppDetail = z.infer<typeof storeAppDetailSchema>;
|
|
175
|
+
|
|
91
176
|
/** `POST /v1/apps/installations/{id}/session` — 앱 화면 iframe이 쓸 세션 토큰(5분) */
|
|
92
177
|
export const appSessionSchema = z.object({
|
|
93
178
|
token: z.string(),
|
|
@@ -168,6 +253,11 @@ export const appVersionSchema = z.object({
|
|
|
168
253
|
secretSettings: z.array(z.string()),
|
|
169
254
|
}),
|
|
170
255
|
build: appBuildSchema.nullable(),
|
|
256
|
+
listing: appListingSchema
|
|
257
|
+
.nullable()
|
|
258
|
+
.optional()
|
|
259
|
+
.default(null)
|
|
260
|
+
.describe("이 버전이 올린 아이콘·설명·README"),
|
|
171
261
|
releasedAt: z.string().nullable(),
|
|
172
262
|
revokedAt: z.string().nullable(),
|
|
173
263
|
createdAt: z.string(),
|
|
@@ -218,6 +308,16 @@ export const appArtifactUploadSchema = z.object({
|
|
|
218
308
|
.max(2_000)
|
|
219
309
|
.default([]),
|
|
220
310
|
message: z.string().trim().max(200).optional(),
|
|
311
|
+
/** 앱 폴더의 아이콘·README — 매니페스트 `icon`·`readme`(없으면 README.md)를 CLI가 싣는다 */
|
|
312
|
+
listing: z
|
|
313
|
+
.object({
|
|
314
|
+
icon: z
|
|
315
|
+
.object({ contentType: z.enum(APP_ICON_CONTENT_TYPES), content: z.string().min(1) })
|
|
316
|
+
.nullable()
|
|
317
|
+
.default(null),
|
|
318
|
+
readme: z.string().max(APP_README_MAX_BYTES).nullable().default(null),
|
|
319
|
+
})
|
|
320
|
+
.default({ icon: null, readme: null }),
|
|
221
321
|
});
|
|
222
322
|
export type AppArtifactUpload = z.infer<typeof appArtifactUploadSchema>;
|
|
223
323
|
|