@sayren/store-sdk 0.11.0 → 0.13.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.
- package/dist/index.d.mts +298 -78
- package/dist/index.mjs +113 -6
- package/package.json +1 -1
- package/src/client.ts +12 -0
- package/src/http.ts +16 -1
- package/src/index.ts +1 -0
- package/src/schemas/api-log.ts +12 -1
- package/src/schemas/misc.ts +134 -1
- package/src/schemas/telemetry.ts +74 -0
package/dist/index.mjs
CHANGED
|
@@ -34,6 +34,11 @@ const errorDetailSchema = z.object({
|
|
|
34
34
|
message: z.string(),
|
|
35
35
|
traceId: z.string().optional(),
|
|
36
36
|
/**
|
|
37
|
+
* 이 에러 코드를 설명하는 문서 URL(선택). 서버가 코드별 매핑을 가진 코드에만 실린다 —
|
|
38
|
+
* 없으면 필드가 아예 없으므로 소비측은 있을 때만 쓴다
|
|
39
|
+
*/
|
|
40
|
+
docs: z.url().optional().catch(void 0),
|
|
41
|
+
/**
|
|
37
42
|
* 에러 코드별 보조 데이터(선택). 예: 관리 API `LIVE_GATE_NOT_MET`·`LIVE_PAYMENTS_DISABLED`의
|
|
38
43
|
* `{ conditions: LiveGateCondition[] }`. 형태는 에러 코드마다 다르므로 소비측이 코드별 스키마로 다시 파싱한다
|
|
39
44
|
*/
|
|
@@ -69,13 +74,15 @@ var ApiError = class extends Error {
|
|
|
69
74
|
traceId;
|
|
70
75
|
body;
|
|
71
76
|
details;
|
|
72
|
-
|
|
77
|
+
docs;
|
|
78
|
+
constructor(status, code, message, traceId, body, details, docs) {
|
|
73
79
|
super(message);
|
|
74
80
|
this.status = status;
|
|
75
81
|
this.code = code;
|
|
76
82
|
this.traceId = traceId;
|
|
77
83
|
this.body = body;
|
|
78
84
|
this.details = details;
|
|
85
|
+
this.docs = docs;
|
|
79
86
|
this.name = "ApiError";
|
|
80
87
|
}
|
|
81
88
|
};
|
|
@@ -88,7 +95,7 @@ var ApiError = class extends Error {
|
|
|
88
95
|
function toApiError(status, raw) {
|
|
89
96
|
const envelope = envelopeSchema.safeParse(raw);
|
|
90
97
|
const detail = envelope.success ? envelope.data.error : errorDetailSchema.safeParse(raw).data ?? null;
|
|
91
|
-
if (detail) return new ApiError(status, detail.code, detail.message, detail.traceId, raw, detail.details);
|
|
98
|
+
if (detail) return new ApiError(status, detail.code, detail.message, detail.traceId, raw, detail.details, detail.docs);
|
|
92
99
|
const oauth = envelope.success ? null : oauthErrorSchema.safeParse(raw).data;
|
|
93
100
|
if (oauth) return new ApiError(status, oauth.error, oauth.error_description ?? oauth.error, void 0, raw);
|
|
94
101
|
return new ApiError(status, "UNKNOWN", `HTTP ${status}`, void 0, raw);
|
|
@@ -467,7 +474,8 @@ const apiLogListQuerySchema = z.object({
|
|
|
467
474
|
principalType: z.union([z.string(), z.array(z.string())]).transform((value) => (Array.isArray(value) ? value : [value]).flatMap((part) => part.split(",")).map((part) => part.trim()).filter((part) => part.length > 0)).pipe(z.array(apiLogPrincipalTypeSchema).min(1)).optional().describe("호출 주체 유형 필터. 여러 개는 쉼표로 구분한다 (예: `PAT,CLIENT`). 파싱 결과는 배열로 정규화된다"),
|
|
468
475
|
principalId: z.string().optional().describe("호출 주체 ID 필터"),
|
|
469
476
|
status: apiLogStatusFilterSchema.optional(),
|
|
470
|
-
q: z.string().optional().describe("경로 부분 일치
|
|
477
|
+
q: z.string().optional().describe("경로 부분 일치 검색어. 리소스 id(예: `ord_…`)를 넣으면 그 리소스를 건드린 요청만 남으므로 웹훅 전송 기록의 `resource.id`와 짝지어 쓴다"),
|
|
478
|
+
traceId: z.string().optional().describe("추적 ID 필터(정확히 일치). 에러 응답 `error.traceId` 값을 그대로 넣어 그 요청 한 건을 찾는다"),
|
|
471
479
|
cursor: z.string().optional().describe("이전 응답의 `nextCursor`. 생략하면 가장 최신부터"),
|
|
472
480
|
limit: z.coerce.number().int().min(1).max(100).optional().describe("최대 건수 (1~100, 기본 50)")
|
|
473
481
|
});
|
|
@@ -1180,6 +1188,44 @@ const webhookDeliveryErrorCodeSchema = z.enum([
|
|
|
1180
1188
|
"NON_2XX",
|
|
1181
1189
|
"WEBHOOK_SUSPENDED"
|
|
1182
1190
|
]).describe("전송 실패 사유. `TIMEOUT`=10초 안에 응답 없음, `CONNECTION_FAILED`=연결 실패(DNS·TLS 포함), `BLOCKED_ADDRESS`=사설·루프백 등 전송이 허용되지 않는 주소, `NON_2XX`=2xx가 아닌 응답(리다이렉트 포함), `WEBHOOK_SUSPENDED`=구독 정지로 대기 중인 전송이 종료됨");
|
|
1191
|
+
/**
|
|
1192
|
+
* 실패 원인 분류의 **현재** 값. 응답 스키마(`webhookFailureReasonSchema`)는 문자열로 열려 있다 —
|
|
1193
|
+
* 서버가 값을 더해도 이전 SDK가 목록 파싱에 실패하지 않게 한다(택배 추적 `stage`와 같은 이유).
|
|
1194
|
+
* 모르는 값은 `errorCode`로 판단한다.
|
|
1195
|
+
*/
|
|
1196
|
+
const WEBHOOK_FAILURE_REASONS = [
|
|
1197
|
+
"DNS",
|
|
1198
|
+
"TLS",
|
|
1199
|
+
"TIMEOUT",
|
|
1200
|
+
"CONNECTION",
|
|
1201
|
+
"HTTP_4XX",
|
|
1202
|
+
"HTTP_5XX",
|
|
1203
|
+
"HTTP_OTHER",
|
|
1204
|
+
"SSRF_BLOCKED",
|
|
1205
|
+
"SUSPENDED"
|
|
1206
|
+
];
|
|
1207
|
+
const webhookFailureReasonSchema = z.string().describe("실패 원인 분류(진단용). 현재 값은 `DNS`=호스트 이름을 주소로 바꾸지 못함, `TLS`=인증서·핸드셰이크 실패, `TIMEOUT`=10초 안에 응답 없음, `CONNECTION`=연결 거부·끊김, `HTTP_4XX`·`HTTP_5XX`=그 대역의 응답, `HTTP_OTHER`=2xx·4xx·5xx가 아닌 응답(리다이렉트 등), `SSRF_BLOCKED`=사설·루프백 등 전송이 허용되지 않는 주소, `SUSPENDED`=구독 정지로 대기 중인 전송이 종료됨. `errorCode`보다 잘게 나눈 값이며 둘은 함께 쓸 수 있다. 값이 추가될 수 있으므로 모르는 값은 `errorCode`로 판단한다");
|
|
1208
|
+
/**
|
|
1209
|
+
* 대상 리소스 종류의 **현재** 값. 응답 스키마는 문자열로 열려 있다(새 이벤트가 새 리소스를 가리킬 수 있다).
|
|
1210
|
+
*/
|
|
1211
|
+
const WEBHOOK_RESOURCE_TYPES = [
|
|
1212
|
+
"ORDER",
|
|
1213
|
+
"CLAIM",
|
|
1214
|
+
"PRODUCT",
|
|
1215
|
+
"REVIEW",
|
|
1216
|
+
"INQUIRY"
|
|
1217
|
+
];
|
|
1218
|
+
const webhookResourceTypeSchema = z.string().describe("이벤트가 가리키는 리소스 종류. 현재 값은 `ORDER`·`CLAIM`·`PRODUCT`·`REVIEW`·`INQUIRY`이고 값이 추가될 수 있다");
|
|
1219
|
+
/** 이벤트가 가리키는 대상 리소스 — 전송 기록을 주문·상품과 잇는 키 */
|
|
1220
|
+
const webhookDeliveryResourceSchema = z.object({
|
|
1221
|
+
type: webhookResourceTypeSchema,
|
|
1222
|
+
id: z.string().describe("리소스 ID. 전송 기록 조회의 `resourceId`, API 로그 조회의 `q`로 그대로 넣을 수 있다")
|
|
1223
|
+
});
|
|
1224
|
+
/** 발송 요청 헤더 한 줄 (값 마스킹) */
|
|
1225
|
+
const webhookRequestHeaderSchema = z.object({
|
|
1226
|
+
name: z.string().describe("헤더 이름"),
|
|
1227
|
+
value: z.string().nullable().describe("헤더 값. 서명(`Sayren-Signature`)처럼 비밀이 섞이는 헤더는 이름만 주고 값은 null이다")
|
|
1228
|
+
});
|
|
1183
1229
|
/** 웹훅 전송 기록 한 건 (전송 기록 조회 항목) */
|
|
1184
1230
|
const webhookDeliverySchema = z.object({
|
|
1185
1231
|
deliveryId: z.string().describe("전송 기록 ID. 수신 요청의 `Sayren-Delivery-Id` 헤더 값"),
|
|
@@ -1189,8 +1235,10 @@ const webhookDeliverySchema = z.object({
|
|
|
1189
1235
|
status: webhookDeliveryStatusSchema,
|
|
1190
1236
|
responseStatusCode: z.number().int().nullable().describe("마지막 시도의 HTTP 응답 코드. 미시도·연결 실패·타임아웃·차단이면 null"),
|
|
1191
1237
|
responseTimeMs: z.number().int().nullable().describe("마지막 시도의 응답 시간(ms). 미시도·연결 실패면 null"),
|
|
1192
|
-
responseBodyExcerpt: z.string().nullable().describe("마지막 시도의 응답 본문 앞부분(최대
|
|
1238
|
+
responseBodyExcerpt: z.string().nullable().describe("마지막 시도의 응답 본문 앞부분(최대 4,096자). 응답이 없으면 null. 가린 뒤에 자른다. 키 이름에 `authorization`·`token`·`secret`·`password`·`key`·`signature`·`credential`·`cookie`가 **들어 있으면**(부분 일치, 대소문자 무시. 예: `apiKey`·`access_token`·`keyword`) 그 값을 `***`로 가린다. 값이 객체·배열이면 통째로 가린다. 평문 `Authorization: …` 같은 헤더 줄, `Bearer <토큰>`, 쿼리 형식 `access_token=…`, 끝에서 잘려 닫히지 않은 값도 가린다"),
|
|
1193
1239
|
errorCode: webhookDeliveryErrorCodeSchema.nullable().describe("마지막 시도의 실패 사유. 성공했거나 미시도면 null"),
|
|
1240
|
+
failureReason: webhookFailureReasonSchema.nullable().describe("마지막 시도의 실패 원인 분류. 성공했거나 미시도면 null. `errorCode`를 더 잘게 나눈 진단 값이다"),
|
|
1241
|
+
resource: webhookDeliveryResourceSchema.nullable().describe("이벤트가 가리키는 대상 리소스. 리소스를 특정할 수 없는 이벤트면 null. 이 값으로 전송 기록(`resourceId`)과 API 로그(`q`)를 오갈 수 있다"),
|
|
1194
1242
|
attemptCount: z.number().int().min(0).describe("지금까지의 시도 횟수. 최초 시도 1회 + 재시도 최대 4회"),
|
|
1195
1243
|
createdAt: z.string().describe("전송 기록이 적재된 일시 (ISO 8601)"),
|
|
1196
1244
|
attemptedAt: z.string().nullable().describe("마지막 시도 일시 (ISO 8601). 미시도면 null"),
|
|
@@ -1198,9 +1246,20 @@ const webhookDeliverySchema = z.object({
|
|
|
1198
1246
|
redeliveryOf: z.string().nullable().describe("수동 재전송으로 생긴 기록이면 원본 전송 기록 ID. 아니면 null"),
|
|
1199
1247
|
redeliveredAs: z.string().nullable().describe("이 기록을 원본으로 수동 재전송해 생긴 기록의 전송 기록 ID. 아직 재전송하지 않았으면 null. 재전송은 원본당 1회다")
|
|
1200
1248
|
});
|
|
1249
|
+
/** 발송 본문 최대 노출 길이(바이트). 넘으면 잘라 주고 `payloadTruncated`가 true다 */
|
|
1250
|
+
const WEBHOOK_PAYLOAD_EXCERPT_MAX_BYTES = 16384;
|
|
1251
|
+
/** 웹훅 전송 기록 상세 — `GET /webhooks/{id}/deliveries/{deliveryId}`. 목록 항목 + 요청 본문·헤더 */
|
|
1252
|
+
const webhookDeliveryDetailSchema = webhookDeliverySchema.extend({
|
|
1253
|
+
payload: z.string().describe(`수신 서버로 보낸 요청 본문 그대로(JSON 문자열). 서명은 이 문자열을 대상으로 계산한다. 최대 ${WEBHOOK_PAYLOAD_EXCERPT_MAX_BYTES / 1024}KB까지 주고 넘으면 잘라 준다`),
|
|
1254
|
+
payloadTruncated: z.boolean().describe("본문이 길어서 잘렸으면 true. true면 `payload`는 유효한 JSON이 아니다"),
|
|
1255
|
+
payloadMasked: z.boolean().describe("민감 키 마스킹(`responseBodyExcerpt`와 같은 키 이름 규칙)이 본문을 바꿨으면 true. true면 `payload`는 보낸 본문과 달라 서명을 맞춰 볼 수 없다. 이벤트 본문에는 보통 민감 키가 없어 false다"),
|
|
1256
|
+
requestHeaders: z.array(webhookRequestHeaderSchema).describe("발송 요청에 실은 헤더. 값은 전송 시점 그대로이나 서명 헤더는 이름만 주고 값은 null이다. `Content-Length`는 실제로 보낸 본문(마스킹·자르기 전)의 바이트 수다. 매 시도가 같은 헤더 집합을 쓰고 서명·타임스탬프만 달라진다")
|
|
1257
|
+
});
|
|
1201
1258
|
/** 전송 기록 조회 쿼리 — `GET /webhooks/{id}/deliveries` */
|
|
1202
1259
|
const webhookDeliveryListQuerySchema = z.object({
|
|
1203
1260
|
status: webhookDeliveryStatusSchema.optional().describe("전송 상태 필터. 생략하면 전체"),
|
|
1261
|
+
eventType: eventTypeSchema.optional().describe("이벤트 타입 필터. 생략하면 전체"),
|
|
1262
|
+
resourceId: z.string().optional().describe("대상 리소스 ID 필터(정확히 일치). 주문·클레임·상품·리뷰·문의 id를 넣으면 그 리소스의 이벤트만 남는다. 같은 id로 API 로그를 `q`로 검색하면 그 리소스를 건드린 API 요청을 볼 수 있다"),
|
|
1204
1263
|
limit: z.coerce.number().int().min(1).max(100).optional().describe("최대 건수 (1~100, 기본 50). 최신 적재순으로 반환하며 보관 기간은 30일이다")
|
|
1205
1264
|
});
|
|
1206
1265
|
/** 실패 건 일괄 재전송 응답 — `POST /webhooks/{id}/deliveries/redeliver-failed` */
|
|
@@ -1210,7 +1269,11 @@ const webhookTestRequestSchema = z.object({ eventType: eventTypeSchema.describe(
|
|
|
1210
1269
|
const webhookTestResultSchema = z.object({
|
|
1211
1270
|
responseStatusCode: z.number().int().nullable().describe("수신 서버의 HTTP 응답 코드. 연결 실패·타임아웃·차단이면 null"),
|
|
1212
1271
|
responseTimeMs: z.number().int().nullable().describe("응답 시간(ms). 연결 실패면 null"),
|
|
1213
|
-
errorCode: webhookDeliveryErrorCodeSchema.nullable().describe("실패 사유. 2xx 응답이면 null")
|
|
1272
|
+
errorCode: webhookDeliveryErrorCodeSchema.nullable().describe("실패 사유. 2xx 응답이면 null"),
|
|
1273
|
+
failureReason: webhookFailureReasonSchema.nullable().describe("실패 원인 분류. 2xx 응답이면 null"),
|
|
1274
|
+
responseBodyExcerpt: z.string().nullable().describe("응답 본문 앞부분(최대 4,096자). 규칙은 전송 기록의 `responseBodyExcerpt`와 같다. 응답이 없으면 null"),
|
|
1275
|
+
requestHeaders: z.array(webhookRequestHeaderSchema).describe("보낸 요청 헤더. 서명 헤더는 이름만 주고 값은 null이다"),
|
|
1276
|
+
payload: z.string().describe("보낸 요청 본문 그대로(JSON 문자열). 샘플 이벤트다")
|
|
1214
1277
|
});
|
|
1215
1278
|
const webhookRequestSchema = z.object({
|
|
1216
1279
|
url: z.url().startsWith("https://", "웹훅 URL은 HTTPS만 허용됩니다"),
|
|
@@ -3392,6 +3455,12 @@ get: (params) => storeHttp.request("GET", "/sales-report", salesReportSchema, {
|
|
|
3392
3455
|
/** 전송 기록 (최신 적재순, 기본 50건·최대 100건, 보관 30일) */
|
|
3393
3456
|
listDeliveries: (webhookId, params) => storeHttp.request("GET", path`/webhooks/${webhookId}/deliveries`, z.array(webhookDeliverySchema), { query: params }),
|
|
3394
3457
|
/**
|
|
3458
|
+
* 전송 기록 한 건의 상세. 목록 항목에 보낸 요청 본문(최대 16KB)과 요청 헤더가 더해진다.
|
|
3459
|
+
* 실패 원인 분류(`failureReason`)·응답 본문(`responseBodyExcerpt`)으로 원인을 찾고,
|
|
3460
|
+
* `resource.id`를 API 로그 조회의 `q`로 넘겨 같은 리소스의 API 요청과 맞춰 본다.
|
|
3461
|
+
*/
|
|
3462
|
+
getDelivery: (webhookId, deliveryId) => storeHttp.request("GET", path`/webhooks/${webhookId}/deliveries/${deliveryId}`, webhookDeliveryDetailSchema),
|
|
3463
|
+
/**
|
|
3395
3464
|
* FAILED 전송 기록 한 건을 같은 eventId로 다시 보낸다(202). 새로 적재된 기록을 돌려주며
|
|
3396
3465
|
* `redeliveryOf`가 원본 deliveryId다. 원본이 FAILED가 아니면 409 `DELIVERY_NOT_FAILED`,
|
|
3397
3466
|
* 원본이 이미 재전송됐으면 409 `DELIVERY_ALREADY_REDELIVERED`(원본당 1회, 원본의 `redeliveredAs` 참고),
|
|
@@ -3599,6 +3668,44 @@ const STORE_ROLE_SCOPES = {
|
|
|
3599
3668
|
]
|
|
3600
3669
|
};
|
|
3601
3670
|
//#endregion
|
|
3671
|
+
//#region src/schemas/telemetry.ts
|
|
3672
|
+
/**
|
|
3673
|
+
* MCP 도구 호출 계측 (`POST /v1/telemetry/mcp`, 이슈 #34).
|
|
3674
|
+
*
|
|
3675
|
+
* `@sayren/mcp`가 `verify_storefront` 같은 도구를 돌린 뒤 결과 요약을 보낸다. 목적은 하나다 —
|
|
3676
|
+
* AI가 만든 스토어프론트가 **처음에** 규칙을 통과하는지, 통과하지 못하면 어떤 규칙에서 막히는지를
|
|
3677
|
+
* 세는 것이다. 그 숫자가 규칙과 안내를 고치는 근거가 된다.
|
|
3678
|
+
*
|
|
3679
|
+
* **보내지 않는 것**: 파일 경로·소스 코드·위반 상세(`detail`)·토큰·사람 식별자. 규칙 id와 개수뿐이다.
|
|
3680
|
+
* 테넌트는 요청 토큰이 정한다(본문에 스토어 id가 없다). 옵트아웃은 클라이언트의 `SAYREN_TELEMETRY=0`이다.
|
|
3681
|
+
*
|
|
3682
|
+
* 알 수 없는 키는 저장하지 않고 버린다(`.strict()`를 쓰지 않는다) — 새 버전 클라이언트가 필드를
|
|
3683
|
+
* 하나 더 실었다고 계측 전체가 400으로 날아가면 그 버전의 숫자가 통째로 빈다.
|
|
3684
|
+
*/
|
|
3685
|
+
/** 규칙 하나의 검사 결과. `ruleId`는 늘어나므로 enum이 아니라 형식만 좁힌다 */
|
|
3686
|
+
const mcpRuleResultSchema = z.object({
|
|
3687
|
+
ruleId: z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "규칙 id는 소문자·숫자·하이픈입니다").describe("검사한 규칙 id (예: `payment-popup-prepare`)"),
|
|
3688
|
+
status: z.enum(["pass", "violation"]).describe("통과했는가"),
|
|
3689
|
+
count: z.number().int().min(0).max(1e4).describe("이 규칙으로 잡힌 위반 수. 통과면 0")
|
|
3690
|
+
});
|
|
3691
|
+
/**
|
|
3692
|
+
* 한 번에 실을 수 있는 규칙 결과 수 — 규칙 수(현재 16개)의 두 배까지다.
|
|
3693
|
+
*
|
|
3694
|
+
* 규칙 수 수준으로 좁게 둔다. 이 배열이 그대로 `mcp_tool_events.results` jsonb가 되므로 상한이
|
|
3695
|
+
* 넉넉하면 한 행의 크기가 그만큼 커지고, 집계가 그 jsonb를 펼친다.
|
|
3696
|
+
*/
|
|
3697
|
+
const MCP_TELEMETRY_MAX_RESULTS = 32;
|
|
3698
|
+
const mcpToolEventSchema = z.object({
|
|
3699
|
+
tool: z.string().regex(/^[a-z][a-z0-9_]{0,39}$/, "도구 이름 형식이 올바르지 않습니다").describe("도구 이름 (예: `verify_storefront`). CLI가 부른 검사는 `create`"),
|
|
3700
|
+
clientVersion: z.string().max(40).regex(/^[\w.+-]+$/, "버전 형식이 올바르지 않습니다").optional().describe("`@sayren/mcp` 버전. 버전별로 첫 통과율이 나아졌는지 본다"),
|
|
3701
|
+
sessionId: z.string().min(8).max(64).regex(/^[A-Za-z0-9_-]+$/, "세션 id 형식이 올바르지 않습니다").describe("클라이언트 프로세스당 랜덤 값. 사람·기기를 식별하지 않는다"),
|
|
3702
|
+
attempt: z.number().int().min(1).max(1e3).describe("이 세션 안에서 같은 도구를 부른 순번(1부터)"),
|
|
3703
|
+
durationMs: z.number().int().min(0).max(36e5).optional().describe("도구 실행 시간(ms)"),
|
|
3704
|
+
ok: z.boolean().describe("위반이 없었는가"),
|
|
3705
|
+
checkedFiles: z.number().int().min(0).max(1e5).describe("검사한 소스 파일 수"),
|
|
3706
|
+
results: z.array(mcpRuleResultSchema).max(32).refine((results) => new Set(results.map((result) => result.ruleId)).size === results.length, { message: "같은 규칙 id를 두 번 실을 수 없습니다" }).describe("규칙별 통과·위반. 규칙 id는 중복될 수 없고 파일 경로와 위반 상세는 싣지 않는다")
|
|
3707
|
+
});
|
|
3708
|
+
//#endregion
|
|
3602
3709
|
//#region src/schemas/webhook-event.ts
|
|
3603
3710
|
const envelopeFields = {
|
|
3604
3711
|
id: z.string().describe("이벤트 ID (`evt_…`, 테스트 전송은 `evt_test_…`). 중복 제거 키"),
|
|
@@ -3831,4 +3938,4 @@ function createStoreSession(options) {
|
|
|
3831
3938
|
};
|
|
3832
3939
|
}
|
|
3833
3940
|
//#endregion
|
|
3834
|
-
export { ApiError, CASH_RECEIPT_STATUS_VALUES, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_OPTION_GROUPS, MAX_PRODUCT_IMAGES, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, REFUND_POLICY_DEFAULTS, REMOTE_AREA_ZIP_RANGES, RESERVED_STORE_CODES, SHIPPING_POLICY_DEFAULTS, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, UPLOAD_PURPOSE_LIMITS, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, cashReceiptSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimReturnAddressSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, consentActionSchema, consentKindSchema, consentSourceSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerConsentListQuerySchema, customerConsentSchema, customerConsentStateSchema, customerConsentsSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryBundleKeyOf, deliveryFeeWaiverSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, policyDocumentUrlSchema, policyVersionSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productRefundPolicyOverride, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, proratedRefundBase, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, quoteDelivery, refreshUserToken, refundPolicyPatchSchema, refundPolicySchema, rejectClaimRequestSchema, remoteAreaLabelOf, remoteAreaOf, remoteAreaSchema, remoteSurchargeOf, resolveRefundPolicy, resolveShippingPolicy, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, shippingPolicyPatchSchema, shippingPolicySchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingStageResponseSchema, trackingUpdateResultSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema };
|
|
3941
|
+
export { ApiError, CASH_RECEIPT_STATUS_VALUES, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_AUTO_TRANSITION_DAYS, MAX_OPTION_GROUPS, MAX_PRODUCT_IMAGES, MAX_VALUES_PER_GROUP, MAX_VARIANTS, MCP_TELEMETRY_MAX_RESULTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, REFUND_POLICY_DEFAULTS, REMOTE_AREA_ZIP_RANGES, RESERVED_STORE_CODES, SHIPPING_POLICY_DEFAULTS, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, UPLOAD_PURPOSE_LIMITS, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_FAILURE_REASONS, WEBHOOK_HEADERS, WEBHOOK_PAYLOAD_EXCERPT_MAX_BYTES, WEBHOOK_RESOURCE_TYPES, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsCheckoutOutcomeSchema, analyticsCheckoutSchema, analyticsDeviceRowSchema, analyticsDevicesSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsPagesQuerySchema, analyticsPagesSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsRealtimeSchema, analyticsSearchQuerySchema, analyticsSearchSchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, cashReceiptSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimReturnAddressSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, consentActionSchema, consentKindSchema, consentSourceSchema, createPkcePair, createStoreClient, createStoreSession, customerAddressSchema, customerConsentListQuerySchema, customerConsentSchema, customerConsentStateSchema, customerConsentsSchema, customerDetailSchema, customerInquiryCategorySchema, customerInquiryListQuerySchema, customerListQuerySchema, customerOrderSchema, customerSortSchema, customerStatsSchema, customerSummarySchema, customerWithdrawalBlockedDetailsSchema, deliveryBundleKeyOf, deliveryFeeWaiverSchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, easyPayProviderResponseSchema, easyPayProviderSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, loginProviderSchema, loginProviderStatusSchema, loginProviderTestSchema, loginSettingsSchema, maskedSecretSchema, mcpRuleResultSchema, mcpToolEventSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, policyDocumentUrlSchema, policyVersionSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageRejection, productImageUrlSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productRefundPolicyOverride, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, proratedRefundBase, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, quoteDelivery, refreshUserToken, refundPolicyPatchSchema, refundPolicySchema, rejectClaimRequestSchema, remoteAreaLabelOf, remoteAreaOf, remoteAreaSchema, remoteSurchargeOf, resolveRefundPolicy, resolveShippingPolicy, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, saveLoginProviderRequestSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, shippingPolicyPatchSchema, shippingPolicySchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingStageResponseSchema, trackingUpdateResultSchema, updateLoginProviderRequestSchema, updateLoginSettingsRequestSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadExtensionOf, uploadPurposeSchema, uploadRejection, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryDetailSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliveryResourceSchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookFailureReasonSchema, webhookRedeliverFailedResultSchema, webhookRequestHeaderSchema, webhookRequestSchema, webhookResourceTypeSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema, withdrawCustomerRequestSchema };
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -101,6 +101,7 @@ import {
|
|
|
101
101
|
type WebhookRequest,
|
|
102
102
|
type WebhookTestRequest,
|
|
103
103
|
webhookCreatedSchema,
|
|
104
|
+
webhookDeliveryDetailSchema,
|
|
104
105
|
webhookDeliverySchema,
|
|
105
106
|
webhookRedeliverFailedResultSchema,
|
|
106
107
|
webhookRequestSchema,
|
|
@@ -984,6 +985,17 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
984
985
|
z.array(webhookDeliverySchema),
|
|
985
986
|
{ query: params as Query },
|
|
986
987
|
),
|
|
988
|
+
/**
|
|
989
|
+
* 전송 기록 한 건의 상세. 목록 항목에 보낸 요청 본문(최대 16KB)과 요청 헤더가 더해진다.
|
|
990
|
+
* 실패 원인 분류(`failureReason`)·응답 본문(`responseBodyExcerpt`)으로 원인을 찾고,
|
|
991
|
+
* `resource.id`를 API 로그 조회의 `q`로 넘겨 같은 리소스의 API 요청과 맞춰 본다.
|
|
992
|
+
*/
|
|
993
|
+
getDelivery: (webhookId: string, deliveryId: string) =>
|
|
994
|
+
storeHttp.request(
|
|
995
|
+
"GET",
|
|
996
|
+
path`/webhooks/${webhookId}/deliveries/${deliveryId}`,
|
|
997
|
+
webhookDeliveryDetailSchema,
|
|
998
|
+
),
|
|
987
999
|
/**
|
|
988
1000
|
* FAILED 전송 기록 한 건을 같은 eventId로 다시 보낸다(202). 새로 적재된 기록을 돌려주며
|
|
989
1001
|
* `redeliveryOf`가 원본 deliveryId다. 원본이 FAILED가 아니면 409 `DELIVERY_NOT_FAILED`,
|
package/src/http.ts
CHANGED
|
@@ -5,6 +5,11 @@ export const errorDetailSchema = z.object({
|
|
|
5
5
|
code: z.string(),
|
|
6
6
|
message: z.string(),
|
|
7
7
|
traceId: z.string().optional(),
|
|
8
|
+
/**
|
|
9
|
+
* 이 에러 코드를 설명하는 문서 URL(선택). 서버가 코드별 매핑을 가진 코드에만 실린다 —
|
|
10
|
+
* 없으면 필드가 아예 없으므로 소비측은 있을 때만 쓴다
|
|
11
|
+
*/
|
|
12
|
+
docs: z.url().optional().catch(undefined),
|
|
8
13
|
/**
|
|
9
14
|
* 에러 코드별 보조 데이터(선택). 예: 관리 API `LIVE_GATE_NOT_MET`·`LIVE_PAYMENTS_DISABLED`의
|
|
10
15
|
* `{ conditions: LiveGateCondition[] }`. 형태는 에러 코드마다 다르므로 소비측이 코드별 스키마로 다시 파싱한다
|
|
@@ -50,6 +55,8 @@ export class ApiError extends Error {
|
|
|
50
55
|
public readonly body?: unknown,
|
|
51
56
|
/** 엔벨로프 `$.error.details` — 에러 코드별 보조 데이터. 없으면 undefined */
|
|
52
57
|
public readonly details?: Record<string, unknown>,
|
|
58
|
+
/** 엔벨로프 `$.error.docs` — 이 에러 코드를 설명하는 문서 URL. 매핑이 없으면 undefined */
|
|
59
|
+
public readonly docs?: string,
|
|
53
60
|
) {
|
|
54
61
|
super(message);
|
|
55
62
|
this.name = "ApiError";
|
|
@@ -68,7 +75,15 @@ export function toApiError(status: number, raw: unknown): ApiError {
|
|
|
68
75
|
? envelope.data.error
|
|
69
76
|
: (errorDetailSchema.safeParse(raw).data ?? null);
|
|
70
77
|
if (detail) {
|
|
71
|
-
return new ApiError(
|
|
78
|
+
return new ApiError(
|
|
79
|
+
status,
|
|
80
|
+
detail.code,
|
|
81
|
+
detail.message,
|
|
82
|
+
detail.traceId,
|
|
83
|
+
raw,
|
|
84
|
+
detail.details,
|
|
85
|
+
detail.docs,
|
|
86
|
+
);
|
|
72
87
|
}
|
|
73
88
|
const oauth = envelope.success ? null : oauthErrorSchema.safeParse(raw).data;
|
|
74
89
|
if (oauth) {
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export * from "./schemas/refund-policy";
|
|
|
28
28
|
export * from "./schemas/scope";
|
|
29
29
|
export * from "./schemas/shipping-policy";
|
|
30
30
|
export * from "./schemas/store";
|
|
31
|
+
export * from "./schemas/telemetry";
|
|
31
32
|
export * from "./schemas/upload";
|
|
32
33
|
export * from "./schemas/webhook-event";
|
|
33
34
|
export {
|
package/src/schemas/api-log.ts
CHANGED
|
@@ -68,7 +68,18 @@ export const apiLogListQuerySchema = z.object({
|
|
|
68
68
|
),
|
|
69
69
|
principalId: z.string().optional().describe("호출 주체 ID 필터"),
|
|
70
70
|
status: apiLogStatusFilterSchema.optional(),
|
|
71
|
-
q: z
|
|
71
|
+
q: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe(
|
|
75
|
+
"경로 부분 일치 검색어. 리소스 id(예: `ord_…`)를 넣으면 그 리소스를 건드린 요청만 남으므로 웹훅 전송 기록의 `resource.id`와 짝지어 쓴다",
|
|
76
|
+
),
|
|
77
|
+
traceId: z
|
|
78
|
+
.string()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe(
|
|
81
|
+
"추적 ID 필터(정확히 일치). 에러 응답 `error.traceId` 값을 그대로 넣어 그 요청 한 건을 찾는다",
|
|
82
|
+
),
|
|
72
83
|
cursor: z.string().optional().describe("이전 응답의 `nextCursor`. 생략하면 가장 최신부터"),
|
|
73
84
|
limit: z.coerce.number().int().min(1).max(100).optional().describe("최대 건수 (1~100, 기본 50)"),
|
|
74
85
|
});
|
package/src/schemas/misc.ts
CHANGED
|
@@ -208,6 +208,71 @@ export const webhookDeliveryErrorCodeSchema = z
|
|
|
208
208
|
);
|
|
209
209
|
export type WebhookDeliveryErrorCode = z.infer<typeof webhookDeliveryErrorCodeSchema>;
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* 실패 원인 분류의 **현재** 값. 응답 스키마(`webhookFailureReasonSchema`)는 문자열로 열려 있다 —
|
|
213
|
+
* 서버가 값을 더해도 이전 SDK가 목록 파싱에 실패하지 않게 한다(택배 추적 `stage`와 같은 이유).
|
|
214
|
+
* 모르는 값은 `errorCode`로 판단한다.
|
|
215
|
+
*/
|
|
216
|
+
export const WEBHOOK_FAILURE_REASONS = [
|
|
217
|
+
"DNS",
|
|
218
|
+
"TLS",
|
|
219
|
+
"TIMEOUT",
|
|
220
|
+
"CONNECTION",
|
|
221
|
+
"HTTP_4XX",
|
|
222
|
+
"HTTP_5XX",
|
|
223
|
+
"HTTP_OTHER",
|
|
224
|
+
"SSRF_BLOCKED",
|
|
225
|
+
"SUSPENDED",
|
|
226
|
+
] as const;
|
|
227
|
+
/** 알려진 실패 원인. 응답에는 이 밖의 문자열이 올 수 있다 */
|
|
228
|
+
export type WebhookFailureReason = (typeof WEBHOOK_FAILURE_REASONS)[number];
|
|
229
|
+
|
|
230
|
+
export const webhookFailureReasonSchema = z
|
|
231
|
+
.string()
|
|
232
|
+
.describe(
|
|
233
|
+
"실패 원인 분류(진단용). 현재 값은 `DNS`=호스트 이름을 주소로 바꾸지 못함, `TLS`=인증서·핸드셰이크 실패, " +
|
|
234
|
+
"`TIMEOUT`=10초 안에 응답 없음, `CONNECTION`=연결 거부·끊김, `HTTP_4XX`·`HTTP_5XX`=그 대역의 응답, " +
|
|
235
|
+
"`HTTP_OTHER`=2xx·4xx·5xx가 아닌 응답(리다이렉트 등), `SSRF_BLOCKED`=사설·루프백 등 전송이 허용되지 않는 주소, " +
|
|
236
|
+
"`SUSPENDED`=구독 정지로 대기 중인 전송이 종료됨. `errorCode`보다 잘게 나눈 값이며 둘은 함께 쓸 수 있다. " +
|
|
237
|
+
"값이 추가될 수 있으므로 모르는 값은 `errorCode`로 판단한다",
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 대상 리소스 종류의 **현재** 값. 응답 스키마는 문자열로 열려 있다(새 이벤트가 새 리소스를 가리킬 수 있다).
|
|
242
|
+
*/
|
|
243
|
+
export const WEBHOOK_RESOURCE_TYPES = ["ORDER", "CLAIM", "PRODUCT", "REVIEW", "INQUIRY"] as const;
|
|
244
|
+
/** 알려진 리소스 종류. 응답에는 이 밖의 문자열이 올 수 있다 */
|
|
245
|
+
export type WebhookResourceType = (typeof WEBHOOK_RESOURCE_TYPES)[number];
|
|
246
|
+
|
|
247
|
+
export const webhookResourceTypeSchema = z
|
|
248
|
+
.string()
|
|
249
|
+
.describe(
|
|
250
|
+
"이벤트가 가리키는 리소스 종류. 현재 값은 `ORDER`·`CLAIM`·`PRODUCT`·`REVIEW`·`INQUIRY`이고 값이 추가될 수 있다",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
/** 이벤트가 가리키는 대상 리소스 — 전송 기록을 주문·상품과 잇는 키 */
|
|
254
|
+
export const webhookDeliveryResourceSchema = z.object({
|
|
255
|
+
type: webhookResourceTypeSchema,
|
|
256
|
+
id: z
|
|
257
|
+
.string()
|
|
258
|
+
.describe(
|
|
259
|
+
"리소스 ID. 전송 기록 조회의 `resourceId`, API 로그 조회의 `q`로 그대로 넣을 수 있다",
|
|
260
|
+
),
|
|
261
|
+
});
|
|
262
|
+
export type WebhookDeliveryResource = z.infer<typeof webhookDeliveryResourceSchema>;
|
|
263
|
+
|
|
264
|
+
/** 발송 요청 헤더 한 줄 (값 마스킹) */
|
|
265
|
+
export const webhookRequestHeaderSchema = z.object({
|
|
266
|
+
name: z.string().describe("헤더 이름"),
|
|
267
|
+
value: z
|
|
268
|
+
.string()
|
|
269
|
+
.nullable()
|
|
270
|
+
.describe(
|
|
271
|
+
"헤더 값. 서명(`Sayren-Signature`)처럼 비밀이 섞이는 헤더는 이름만 주고 값은 null이다",
|
|
272
|
+
),
|
|
273
|
+
});
|
|
274
|
+
export type WebhookRequestHeader = z.infer<typeof webhookRequestHeaderSchema>;
|
|
275
|
+
|
|
211
276
|
/** 웹훅 전송 기록 한 건 (전송 기록 조회 항목) */
|
|
212
277
|
export const webhookDeliverySchema = z.object({
|
|
213
278
|
deliveryId: z.string().describe("전송 기록 ID. 수신 요청의 `Sayren-Delivery-Id` 헤더 값"),
|
|
@@ -232,10 +297,27 @@ export const webhookDeliverySchema = z.object({
|
|
|
232
297
|
responseBodyExcerpt: z
|
|
233
298
|
.string()
|
|
234
299
|
.nullable()
|
|
235
|
-
.describe(
|
|
300
|
+
.describe(
|
|
301
|
+
"마지막 시도의 응답 본문 앞부분(최대 4,096자). 응답이 없으면 null. 가린 뒤에 자른다. " +
|
|
302
|
+
"키 이름에 `authorization`·`token`·`secret`·`password`·`key`·`signature`·`credential`·`cookie`가 " +
|
|
303
|
+
"**들어 있으면**(부분 일치, 대소문자 무시. 예: `apiKey`·`access_token`·`keyword`) 그 값을 `***`로 가린다. " +
|
|
304
|
+
"값이 객체·배열이면 통째로 가린다. 평문 `Authorization: …` 같은 헤더 줄, `Bearer <토큰>`, " +
|
|
305
|
+
"쿼리 형식 `access_token=…`, 끝에서 잘려 닫히지 않은 값도 가린다",
|
|
306
|
+
),
|
|
236
307
|
errorCode: webhookDeliveryErrorCodeSchema
|
|
237
308
|
.nullable()
|
|
238
309
|
.describe("마지막 시도의 실패 사유. 성공했거나 미시도면 null"),
|
|
310
|
+
failureReason: webhookFailureReasonSchema
|
|
311
|
+
.nullable()
|
|
312
|
+
.describe(
|
|
313
|
+
"마지막 시도의 실패 원인 분류. 성공했거나 미시도면 null. `errorCode`를 더 잘게 나눈 진단 값이다",
|
|
314
|
+
),
|
|
315
|
+
resource: webhookDeliveryResourceSchema
|
|
316
|
+
.nullable()
|
|
317
|
+
.describe(
|
|
318
|
+
"이벤트가 가리키는 대상 리소스. 리소스를 특정할 수 없는 이벤트면 null. " +
|
|
319
|
+
"이 값으로 전송 기록(`resourceId`)과 API 로그(`q`)를 오갈 수 있다",
|
|
320
|
+
),
|
|
239
321
|
attemptCount: z
|
|
240
322
|
.number()
|
|
241
323
|
.int()
|
|
@@ -260,9 +342,47 @@ export const webhookDeliverySchema = z.object({
|
|
|
260
342
|
});
|
|
261
343
|
export type WebhookDelivery = z.infer<typeof webhookDeliverySchema>;
|
|
262
344
|
|
|
345
|
+
/** 발송 본문 최대 노출 길이(바이트). 넘으면 잘라 주고 `payloadTruncated`가 true다 */
|
|
346
|
+
export const WEBHOOK_PAYLOAD_EXCERPT_MAX_BYTES = 16 * 1024;
|
|
347
|
+
|
|
348
|
+
/** 웹훅 전송 기록 상세 — `GET /webhooks/{id}/deliveries/{deliveryId}`. 목록 항목 + 요청 본문·헤더 */
|
|
349
|
+
export const webhookDeliveryDetailSchema = webhookDeliverySchema.extend({
|
|
350
|
+
payload: z
|
|
351
|
+
.string()
|
|
352
|
+
.describe(
|
|
353
|
+
"수신 서버로 보낸 요청 본문 그대로(JSON 문자열). 서명은 이 문자열을 대상으로 계산한다. " +
|
|
354
|
+
`최대 ${WEBHOOK_PAYLOAD_EXCERPT_MAX_BYTES / 1024}KB까지 주고 넘으면 잘라 준다`,
|
|
355
|
+
),
|
|
356
|
+
payloadTruncated: z
|
|
357
|
+
.boolean()
|
|
358
|
+
.describe("본문이 길어서 잘렸으면 true. true면 `payload`는 유효한 JSON이 아니다"),
|
|
359
|
+
payloadMasked: z
|
|
360
|
+
.boolean()
|
|
361
|
+
.describe(
|
|
362
|
+
"민감 키 마스킹(`responseBodyExcerpt`와 같은 키 이름 규칙)이 본문을 바꿨으면 true. " +
|
|
363
|
+
"true면 `payload`는 보낸 본문과 달라 서명을 맞춰 볼 수 없다. 이벤트 본문에는 보통 민감 키가 없어 false다",
|
|
364
|
+
),
|
|
365
|
+
requestHeaders: z
|
|
366
|
+
.array(webhookRequestHeaderSchema)
|
|
367
|
+
.describe(
|
|
368
|
+
"발송 요청에 실은 헤더. 값은 전송 시점 그대로이나 서명 헤더는 이름만 주고 값은 null이다. " +
|
|
369
|
+
"`Content-Length`는 실제로 보낸 본문(마스킹·자르기 전)의 바이트 수다. " +
|
|
370
|
+
"매 시도가 같은 헤더 집합을 쓰고 서명·타임스탬프만 달라진다",
|
|
371
|
+
),
|
|
372
|
+
});
|
|
373
|
+
export type WebhookDeliveryDetail = z.infer<typeof webhookDeliveryDetailSchema>;
|
|
374
|
+
|
|
263
375
|
/** 전송 기록 조회 쿼리 — `GET /webhooks/{id}/deliveries` */
|
|
264
376
|
export const webhookDeliveryListQuerySchema = z.object({
|
|
265
377
|
status: webhookDeliveryStatusSchema.optional().describe("전송 상태 필터. 생략하면 전체"),
|
|
378
|
+
eventType: eventTypeSchema.optional().describe("이벤트 타입 필터. 생략하면 전체"),
|
|
379
|
+
resourceId: z
|
|
380
|
+
.string()
|
|
381
|
+
.optional()
|
|
382
|
+
.describe(
|
|
383
|
+
"대상 리소스 ID 필터(정확히 일치). 주문·클레임·상품·리뷰·문의 id를 넣으면 그 리소스의 이벤트만 남는다. " +
|
|
384
|
+
"같은 id로 API 로그를 `q`로 검색하면 그 리소스를 건드린 API 요청을 볼 수 있다",
|
|
385
|
+
),
|
|
266
386
|
limit: z.coerce
|
|
267
387
|
.number()
|
|
268
388
|
.int()
|
|
@@ -299,6 +419,19 @@ export const webhookTestResultSchema = z.object({
|
|
|
299
419
|
.describe("수신 서버의 HTTP 응답 코드. 연결 실패·타임아웃·차단이면 null"),
|
|
300
420
|
responseTimeMs: z.number().int().nullable().describe("응답 시간(ms). 연결 실패면 null"),
|
|
301
421
|
errorCode: webhookDeliveryErrorCodeSchema.nullable().describe("실패 사유. 2xx 응답이면 null"),
|
|
422
|
+
failureReason: webhookFailureReasonSchema
|
|
423
|
+
.nullable()
|
|
424
|
+
.describe("실패 원인 분류. 2xx 응답이면 null"),
|
|
425
|
+
responseBodyExcerpt: z
|
|
426
|
+
.string()
|
|
427
|
+
.nullable()
|
|
428
|
+
.describe(
|
|
429
|
+
"응답 본문 앞부분(최대 4,096자). 규칙은 전송 기록의 `responseBodyExcerpt`와 같다. 응답이 없으면 null",
|
|
430
|
+
),
|
|
431
|
+
requestHeaders: z
|
|
432
|
+
.array(webhookRequestHeaderSchema)
|
|
433
|
+
.describe("보낸 요청 헤더. 서명 헤더는 이름만 주고 값은 null이다"),
|
|
434
|
+
payload: z.string().describe("보낸 요청 본문 그대로(JSON 문자열). 샘플 이벤트다"),
|
|
302
435
|
});
|
|
303
436
|
export type WebhookTestResult = z.infer<typeof webhookTestResultSchema>;
|
|
304
437
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP 도구 호출 계측 (`POST /v1/telemetry/mcp`, 이슈 #34).
|
|
5
|
+
*
|
|
6
|
+
* `@sayren/mcp`가 `verify_storefront` 같은 도구를 돌린 뒤 결과 요약을 보낸다. 목적은 하나다 —
|
|
7
|
+
* AI가 만든 스토어프론트가 **처음에** 규칙을 통과하는지, 통과하지 못하면 어떤 규칙에서 막히는지를
|
|
8
|
+
* 세는 것이다. 그 숫자가 규칙과 안내를 고치는 근거가 된다.
|
|
9
|
+
*
|
|
10
|
+
* **보내지 않는 것**: 파일 경로·소스 코드·위반 상세(`detail`)·토큰·사람 식별자. 규칙 id와 개수뿐이다.
|
|
11
|
+
* 테넌트는 요청 토큰이 정한다(본문에 스토어 id가 없다). 옵트아웃은 클라이언트의 `SAYREN_TELEMETRY=0`이다.
|
|
12
|
+
*
|
|
13
|
+
* 알 수 없는 키는 저장하지 않고 버린다(`.strict()`를 쓰지 않는다) — 새 버전 클라이언트가 필드를
|
|
14
|
+
* 하나 더 실었다고 계측 전체가 400으로 날아가면 그 버전의 숫자가 통째로 빈다.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** 규칙 하나의 검사 결과. `ruleId`는 늘어나므로 enum이 아니라 형식만 좁힌다 */
|
|
18
|
+
export const mcpRuleResultSchema = z.object({
|
|
19
|
+
ruleId: z
|
|
20
|
+
.string()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(64)
|
|
23
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, "규칙 id는 소문자·숫자·하이픈입니다")
|
|
24
|
+
.describe("검사한 규칙 id (예: `payment-popup-prepare`)"),
|
|
25
|
+
status: z.enum(["pass", "violation"]).describe("통과했는가"),
|
|
26
|
+
count: z.number().int().min(0).max(10_000).describe("이 규칙으로 잡힌 위반 수. 통과면 0"),
|
|
27
|
+
});
|
|
28
|
+
export type McpRuleResult = z.infer<typeof mcpRuleResultSchema>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 한 번에 실을 수 있는 규칙 결과 수 — 규칙 수(현재 16개)의 두 배까지다.
|
|
32
|
+
*
|
|
33
|
+
* 규칙 수 수준으로 좁게 둔다. 이 배열이 그대로 `mcp_tool_events.results` jsonb가 되므로 상한이
|
|
34
|
+
* 넉넉하면 한 행의 크기가 그만큼 커지고, 집계가 그 jsonb를 펼친다.
|
|
35
|
+
*/
|
|
36
|
+
export const MCP_TELEMETRY_MAX_RESULTS = 32;
|
|
37
|
+
|
|
38
|
+
export const mcpToolEventSchema = z.object({
|
|
39
|
+
tool: z
|
|
40
|
+
.string()
|
|
41
|
+
.regex(/^[a-z][a-z0-9_]{0,39}$/, "도구 이름 형식이 올바르지 않습니다")
|
|
42
|
+
.describe("도구 이름 (예: `verify_storefront`). CLI가 부른 검사는 `create`"),
|
|
43
|
+
clientVersion: z
|
|
44
|
+
.string()
|
|
45
|
+
.max(40)
|
|
46
|
+
.regex(/^[\w.+-]+$/, "버전 형식이 올바르지 않습니다")
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("`@sayren/mcp` 버전. 버전별로 첫 통과율이 나아졌는지 본다"),
|
|
49
|
+
sessionId: z
|
|
50
|
+
.string()
|
|
51
|
+
.min(8)
|
|
52
|
+
.max(64)
|
|
53
|
+
.regex(/^[A-Za-z0-9_-]+$/, "세션 id 형식이 올바르지 않습니다")
|
|
54
|
+
.describe("클라이언트 프로세스당 랜덤 값. 사람·기기를 식별하지 않는다"),
|
|
55
|
+
attempt: z
|
|
56
|
+
.number()
|
|
57
|
+
.int()
|
|
58
|
+
.min(1)
|
|
59
|
+
.max(1_000)
|
|
60
|
+
.describe("이 세션 안에서 같은 도구를 부른 순번(1부터)"),
|
|
61
|
+
durationMs: z.number().int().min(0).max(3_600_000).optional().describe("도구 실행 시간(ms)"),
|
|
62
|
+
ok: z.boolean().describe("위반이 없었는가"),
|
|
63
|
+
checkedFiles: z.number().int().min(0).max(100_000).describe("검사한 소스 파일 수"),
|
|
64
|
+
results: z
|
|
65
|
+
.array(mcpRuleResultSchema)
|
|
66
|
+
.max(MCP_TELEMETRY_MAX_RESULTS)
|
|
67
|
+
// 규칙 하나당 한 줄이다. 같은 id를 여러 줄로 실으면 집계가 한 스토어의 한 규칙을 몇 번 세야
|
|
68
|
+
// 하는지 정할 수 없고, 상한만큼 같은 규칙을 반복해 보내는 것이 저장을 부풀리는 길이 된다.
|
|
69
|
+
.refine((results) => new Set(results.map((result) => result.ruleId)).size === results.length, {
|
|
70
|
+
message: "같은 규칙 id를 두 번 실을 수 없습니다",
|
|
71
|
+
})
|
|
72
|
+
.describe("규칙별 통과·위반. 규칙 id는 중복될 수 없고 파일 경로와 위반 상세는 싣지 않는다"),
|
|
73
|
+
});
|
|
74
|
+
export type McpToolEvent = z.infer<typeof mcpToolEventSchema>;
|