@sayren/store-sdk 0.40.0 → 0.42.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.
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * 관리 API 요청 본문을 모르는 키까지 거르는 스키마로 바꾼다(이슈 #193, 2026-10-24). `z.object`는 모르는 키를 조용히 버려서
5
+ * 오타·잘못된 자리의 필드(예: 옵션 값에 둔 추가금)가 400 없이 사라지고 기본값으로 저장됐다. 원본은 응답 스키마와 하위 스키마를
6
+ * 공유하므로 고치지 않고, 안쪽 객체까지 `catchall: never`로 복제한다. `z.looseObject`처럼 catchall을 정한 객체는 그대로 둔다.
7
+ * 설명·예시(전역 등록부의 메타)는 복제본에 옮겨 OpenAPI 문서가 같다
8
+ */
9
+ export function strictRequest<T extends z.ZodType>(schema: T): T {
10
+ return strictOf(schema, new Map()) as T;
11
+ }
12
+
13
+ // biome-ignore lint/suspicious/noExplicitAny: zod 내부 정의(def)를 형태별로 다시 짜는 곳이라 형태 하나로 묶을 수 없다
14
+ type Def = any;
15
+
16
+ function strictOf(schema: z.ZodType, seen: Map<z.ZodType, z.ZodType>): z.ZodType {
17
+ const done = seen.get(schema);
18
+ if (done) return done;
19
+ const def: Def = schema._zod.def;
20
+ const clone = (patch: Def): z.ZodType => {
21
+ const next = schema.clone({ ...def, ...patch });
22
+ const meta = z.globalRegistry.get(schema);
23
+ if (meta) z.globalRegistry.add(next, meta);
24
+ return next;
25
+ };
26
+ let next: z.ZodType;
27
+ switch (def.type) {
28
+ case "object":
29
+ next = clone({
30
+ shape: Object.fromEntries(
31
+ Object.entries(def.shape as Record<string, z.ZodType>).map(([key, value]) => [
32
+ key,
33
+ strictOf(value, seen),
34
+ ]),
35
+ ),
36
+ catchall: def.catchall ?? z.never(),
37
+ });
38
+ break;
39
+ case "array":
40
+ next = clone({ element: strictOf(def.element, seen) });
41
+ break;
42
+ case "optional":
43
+ case "nullable":
44
+ case "default":
45
+ case "prefault":
46
+ case "nonoptional":
47
+ case "readonly":
48
+ case "catch":
49
+ next = clone({ innerType: strictOf(def.innerType, seen) });
50
+ break;
51
+ case "union":
52
+ next = clone({ options: def.options.map((option: z.ZodType) => strictOf(option, seen)) });
53
+ break;
54
+ case "intersection":
55
+ next = clone({ left: strictOf(def.left, seen), right: strictOf(def.right, seen) });
56
+ break;
57
+ case "pipe":
58
+ next = clone({ in: strictOf(def.in, seen) });
59
+ break;
60
+ case "record":
61
+ next = clone({ valueType: strictOf(def.valueType, seen) });
62
+ break;
63
+ case "tuple":
64
+ next = clone({
65
+ items: def.items.map((item: z.ZodType) => strictOf(item, seen)),
66
+ rest: def.rest ? strictOf(def.rest, seen) : def.rest,
67
+ });
68
+ break;
69
+ case "lazy": {
70
+ let inner: z.ZodType | undefined;
71
+ next = clone({
72
+ getter: () => {
73
+ inner ??= strictOf(def.getter(), seen);
74
+ return inner;
75
+ },
76
+ });
77
+ break;
78
+ }
79
+ default:
80
+ next = schema;
81
+ }
82
+ seen.set(schema, next);
83
+ return next;
84
+ }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { strictRequest } from "./strict";
2
3
 
3
4
  /**
4
5
  * 업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다.
@@ -100,18 +101,20 @@ export function uploadRejection(
100
101
  * 끝나면 `url`이 공개 주소가 된다. 서버가 객체 키를 정하므로 요청은 파일명만 보낸다
101
102
  * (확장자만 쓰이고 이름 자체는 저장되지 않는다).
102
103
  */
103
- export const uploadRequestSchema = z.object({
104
- filename: z.string().min(1).max(255),
105
- purpose: uploadPurposeSchema
106
- .default("description")
107
- .describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
108
- size: z
109
- .number()
110
- .int()
111
- .positive()
112
- .optional()
113
- .describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다"),
114
- });
104
+ export const uploadRequestSchema = strictRequest(
105
+ z.object({
106
+ filename: z.string().min(1).max(255),
107
+ purpose: uploadPurposeSchema
108
+ .default("description")
109
+ .describe("업로드 용도. 허용 확장자와 크기 상한이 용도별로 다르다"),
110
+ size: z
111
+ .number()
112
+ .int()
113
+ .positive()
114
+ .optional()
115
+ .describe("파일 바이트 크기. 주면 용도별 상한을 발급 전에 검사한다"),
116
+ }),
117
+ );
115
118
 
116
119
  export type UploadRequest = z.infer<typeof uploadRequestSchema>;
117
120
 
package/src/session.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { base64UrlFromBytes } from "./base64";
2
- import { toApiError, unwrapData } from "./http";
2
+ import { SAYREN_API_BASE_URL, toApiError, unwrapData } from "./http";
3
3
  import {
4
4
  buildAuthorizeUrl,
5
5
  createPkcePair,
@@ -50,8 +50,8 @@ export interface StoreAuthServer {
50
50
  }
51
51
 
52
52
  export interface StoreSessionOptions {
53
- /** api 주소(예: https://api.example.com/v1) — `userinfo()`의 `GET /me`에 쓴다 */
54
- baseUrl: string;
53
+ /** api 주소(버전 경로 포함) — `userinfo()`의 `GET /me`에 쓴다. 생략하면 운영 주소(`https://api.sayren.io/v1`) */
54
+ baseUrl?: string;
55
55
  /** 인가 서버(accounts) 주소들 */
56
56
  authServer: StoreAuthServer;
57
57
  /** OAuth public client id (예: avarlabs) */
@@ -114,9 +114,12 @@ export interface StoreSession {
114
114
  }
115
115
 
116
116
  export function createStoreSession(options: StoreSessionOptions): StoreSession {
117
- const { baseUrl, clientId, storage, authServer } = options;
117
+ const { clientId, storage, authServer } = options;
118
+ const baseUrl = options.baseUrl ?? SAYREN_API_BASE_URL;
118
119
  const { revokeUrl, tokenUrl, resource } = authServer;
119
- const fetchFn = options.fetch ?? globalThis.fetch;
120
+ // 주입이 없으면 부를 때마다 지금의 전역 fetch를 쓴다(http.ts와 같다)
121
+ const fetchFn: typeof globalThis.fetch =
122
+ options.fetch ?? ((input, init) => globalThis.fetch(input, init));
120
123
  const listeners = new Set<() => void>();
121
124
 
122
125
  function notify(): void {
@@ -1,22 +0,0 @@
1
- //#region src/webhook-headers.ts
2
- /** 웹훅 발송 요청 헤더 상수 (공개 계약). zod 의존 없이 서버 엔트리포인트에서도 가볍게 쓴다 */
3
- /** 서명 헤더. 형식: `t=<unix초>,v1=<hex>[,v1=<hex>]` */
4
- const WEBHOOK_SIGNATURE_HEADER = "Sayren-Signature";
5
- /** 이벤트 ID 헤더. 본문 `id`와 같다. 중복 제거 키 */
6
- const WEBHOOK_EVENT_ID_HEADER = "Sayren-Event-Id";
7
- /** 이벤트 타입 헤더. 본문 `type`과 같다 */
8
- const WEBHOOK_EVENT_TYPE_HEADER = "Sayren-Event-Type";
9
- /** 전송 기록 ID 헤더. 전송 기록의 `deliveryId`와 같다 */
10
- const WEBHOOK_DELIVERY_ID_HEADER = "Sayren-Delivery-Id";
11
- /** 발송 요청의 `User-Agent` 값 */
12
- const WEBHOOK_USER_AGENT = "Sayren-Webhooks/1.0";
13
- /** 수신 측 권장 타임스탬프 허용 오차(초) */
14
- const WEBHOOK_DEFAULT_TOLERANCE_SEC = 300;
15
- const WEBHOOK_HEADERS = {
16
- signature: WEBHOOK_SIGNATURE_HEADER,
17
- eventId: WEBHOOK_EVENT_ID_HEADER,
18
- eventType: WEBHOOK_EVENT_TYPE_HEADER,
19
- deliveryId: WEBHOOK_DELIVERY_ID_HEADER
20
- };
21
- //#endregion
22
- export { WEBHOOK_HEADERS as a, WEBHOOK_EVENT_TYPE_HEADER as i, WEBHOOK_DELIVERY_ID_HEADER as n, WEBHOOK_SIGNATURE_HEADER as o, WEBHOOK_EVENT_ID_HEADER as r, WEBHOOK_USER_AGENT as s, WEBHOOK_DEFAULT_TOLERANCE_SEC as t };