@gencow/core 0.1.6 → 0.1.8

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,47 @@
1
+ /**
2
+ * packages/core/src/auth-config.ts
3
+ *
4
+ * Gencow Auth 설정 타입 및 defineAuth() 함수.
5
+ * 사용자 앱의 gencow/auth.ts 에서 사용.
6
+ *
7
+ * shadcn 패턴: auth.ts는 사용자가 소유하고 직접 수정할 수 있는 파일.
8
+ * defineAuth()는 타입 안전한 설정 헬퍼일 뿐, 런타임 로직은 server에서 처리.
9
+ */
10
+ export interface AuthEmailVerification {
11
+ /** 가입 시 인증 메일 자동 발송 (default: true) */
12
+ sendOnSignUp?: boolean;
13
+ /** 이메일 미인증 시 로그인 차단 (default: true) */
14
+ requireEmailVerification?: boolean;
15
+ /** 인증 완료 후 자동 로그인 (default: true) */
16
+ autoSignInAfterVerification?: boolean;
17
+ /** 인증 메일 발송 함수 — 사용자가 직접 구현 */
18
+ sendVerificationEmail: (data: {
19
+ user: {
20
+ email: string;
21
+ name: string;
22
+ };
23
+ url: string;
24
+ token: string;
25
+ }) => Promise<void>;
26
+ }
27
+ export interface GencowAuthConfig {
28
+ emailVerification?: AuthEmailVerification;
29
+ }
30
+ /**
31
+ * Auth 설정 정의 헬퍼.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // gencow/auth.ts
36
+ * import { defineAuth } from "gencow";
37
+ *
38
+ * export default defineAuth({
39
+ * emailVerification: {
40
+ * sendVerificationEmail: async ({ user, url }) => {
41
+ * // 이메일 발송 로직
42
+ * },
43
+ * },
44
+ * });
45
+ * ```
46
+ */
47
+ export declare function defineAuth(config: GencowAuthConfig): GencowAuthConfig;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * packages/core/src/auth-config.ts
3
+ *
4
+ * Gencow Auth 설정 타입 및 defineAuth() 함수.
5
+ * 사용자 앱의 gencow/auth.ts 에서 사용.
6
+ *
7
+ * shadcn 패턴: auth.ts는 사용자가 소유하고 직접 수정할 수 있는 파일.
8
+ * defineAuth()는 타입 안전한 설정 헬퍼일 뿐, 런타임 로직은 server에서 처리.
9
+ */
10
+ // ─── defineAuth() ────────────────────────────────────────
11
+ /**
12
+ * Auth 설정 정의 헬퍼.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // gencow/auth.ts
17
+ * import { defineAuth } from "gencow";
18
+ *
19
+ * export default defineAuth({
20
+ * emailVerification: {
21
+ * sendVerificationEmail: async ({ user, url }) => {
22
+ * // 이메일 발송 로직
23
+ * },
24
+ * },
25
+ * });
26
+ * ```
27
+ */
28
+ export function defineAuth(config) {
29
+ return config;
30
+ }
package/dist/index.d.ts CHANGED
@@ -15,3 +15,5 @@ export { withRetry } from "./retry";
15
15
  export type { RetryOptions } from "./retry";
16
16
  export { cronJobs } from "./crons";
17
17
  export type { CronJobsBuilder, CronJobDef, IntervalOptions, DailyOptions, WeeklyOptions } from "./crons";
18
+ export { defineAuth } from "./auth-config";
19
+ export type { GencowAuthConfig, AuthEmailVerification } from "./auth-config";
package/dist/index.js CHANGED
@@ -9,3 +9,4 @@ export { createScheduler, getSchedulerInfo } from "./scheduler";
9
9
  export { v, parseArgs, GencowValidationError } from "./v";
10
10
  export { withRetry } from "./retry";
11
11
  export { cronJobs } from "./crons";
12
+ export { defineAuth } from "./auth-config";
@@ -55,6 +55,10 @@ export interface AIContext {
55
55
  temperature?: number;
56
56
  maxTokens?: number;
57
57
  }) => Promise<AIResult>;
58
+ /** 텍스트 임베딩 (단일) — ctx.ai.embed("검색 텍스트") */
59
+ embed: (text: string) => Promise<number[]>;
60
+ /** 배치 임베딩 — ctx.ai.embedMany(["텍스트1", "텍스트2"]) */
61
+ embedMany: (texts: string[]) => Promise<number[][]>;
58
62
  }
59
63
  export interface GencowCtx {
60
64
  /** Drizzle DB 인스턴스 — ctx.db.select().from(table) */
@@ -149,13 +153,43 @@ export declare function query<TSchema = any, TReturn = any>(key: string, handler
149
153
  public?: boolean;
150
154
  handler: QueryHandler<InferArgs<TSchema>, TReturn>;
151
155
  }): QueryDef<TSchema, TReturn>;
152
- export declare function mutation<TSchema = any, TReturn = any>(invalidatesOrDef: string[] | {
156
+ /**
157
+ * mutation — 데이터 변경 함수를 선언적으로 등록합니다.
158
+ *
159
+ * 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * // ✅ 권장: query와 동일한 (name, def) 패턴
164
+ * mutation("tasks.create", {
165
+ * invalidates: [],
166
+ * args: { title: v.string() },
167
+ * handler: async (ctx, args) => { ... },
168
+ * });
169
+ *
170
+ * // ✅ 객체 스타일 (하위 호환)
171
+ * mutation({
172
+ * name: "tasks.create",
173
+ * invalidates: [],
174
+ * handler: async (ctx) => { ... },
175
+ * });
176
+ *
177
+ * // ⚠️ Legacy 배열 스타일 (비권장)
178
+ * mutation(["tasks.list"], handler, "tasks.create");
179
+ * ```
180
+ */
181
+ export declare function mutation<TSchema = any, TReturn = any>(nameOrInvalidatesOrDef: string | string[] | {
153
182
  name?: string;
154
183
  args?: TSchema;
155
184
  public?: boolean;
156
185
  invalidates: string[];
157
186
  handler: MutationHandler<InferArgs<TSchema>, TReturn>;
158
- }, handler?: MutationHandler<InferArgs<TSchema>, TReturn>, name?: string): MutationDef<TSchema, TReturn>;
187
+ }, handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | {
188
+ invalidates?: string[];
189
+ args?: TSchema;
190
+ public?: boolean;
191
+ handler: MutationHandler<InferArgs<TSchema>, TReturn>;
192
+ }, name?: string): MutationDef<TSchema, TReturn>;
159
193
  /**
160
194
  * 커스텀 HTTP 엔드포인트를 선언적으로 등록합니다.
161
195
  * query/mutation은 RPC 패턴이지만, httpAction은 RESTful HTTP 라우트를 직접 정의합니다.
package/dist/reactive.js CHANGED
@@ -36,25 +36,64 @@ export function query(key, handlerOrDef) {
36
36
  return def;
37
37
  }
38
38
  let mutationCounter = 0;
39
- export function mutation(invalidatesOrDef, handler, name) {
39
+ /**
40
+ * mutation — 데이터 변경 함수를 선언적으로 등록합니다.
41
+ *
42
+ * 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // ✅ 권장: query와 동일한 (name, def) 패턴
47
+ * mutation("tasks.create", {
48
+ * invalidates: [],
49
+ * args: { title: v.string() },
50
+ * handler: async (ctx, args) => { ... },
51
+ * });
52
+ *
53
+ * // ✅ 객체 스타일 (하위 호환)
54
+ * mutation({
55
+ * name: "tasks.create",
56
+ * invalidates: [],
57
+ * handler: async (ctx) => { ... },
58
+ * });
59
+ *
60
+ * // ⚠️ Legacy 배열 스타일 (비권장)
61
+ * mutation(["tasks.list"], handler, "tasks.create");
62
+ * ```
63
+ */
64
+ export function mutation(nameOrInvalidatesOrDef, handlerOrDef, name) {
40
65
  let invalidates;
41
66
  let argsSchema;
42
67
  let actualHandler;
43
68
  let mutName;
44
69
  let isPublic = false;
45
- if (Array.isArray(invalidatesOrDef)) {
70
+ if (typeof nameOrInvalidatesOrDef === "string") {
71
+ // New primary style: mutation("name", { invalidates?, args?, public?, handler })
72
+ mutName = nameOrInvalidatesOrDef;
73
+ const def = handlerOrDef;
74
+ invalidates = def.invalidates || [];
75
+ actualHandler = def.handler;
76
+ argsSchema = def.args;
77
+ isPublic = def.public === true;
78
+ }
79
+ else if (Array.isArray(nameOrInvalidatesOrDef)) {
46
80
  // Legacy style: mutation([...], handler, "name")
47
- invalidates = invalidatesOrDef;
48
- actualHandler = handler;
81
+ invalidates = nameOrInvalidatesOrDef;
82
+ actualHandler = handlerOrDef;
49
83
  mutName = name || `mutation_${++mutationCounter}`;
50
84
  }
51
85
  else {
52
- // New object style: mutation({ name?, invalidates, args?, public?, handler })
53
- invalidates = invalidatesOrDef.invalidates;
54
- actualHandler = invalidatesOrDef.handler;
55
- argsSchema = invalidatesOrDef.args;
56
- isPublic = invalidatesOrDef.public === true;
57
- mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
86
+ // Object style: mutation({ name?, invalidates, args?, public?, handler })
87
+ invalidates = nameOrInvalidatesOrDef.invalidates;
88
+ actualHandler = nameOrInvalidatesOrDef.handler;
89
+ argsSchema = nameOrInvalidatesOrDef.args;
90
+ isPublic = nameOrInvalidatesOrDef.public === true;
91
+ mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
92
+ }
93
+ // 이름 미지정 시 경고 — 디버깅 지원
94
+ if (mutName.startsWith("mutation_")) {
95
+ console.warn(`[gencow] mutation registered without explicit name → "${mutName}". ` +
96
+ `Use mutation("myMutation", { handler }) for better debugging.`);
58
97
  }
59
98
  const def = {
60
99
  name: mutName,
package/dist/storage.js CHANGED
@@ -104,13 +104,21 @@ export function storageRoutes(storage, rawSql, storageDir) {
104
104
  if (!meta) {
105
105
  return c.json({ error: "Not found" }, 404);
106
106
  }
107
+ // Bun 런타임에서는 Bun.file()을 사용하여 바이너리 무결성 보장
108
+ // Node의 fs.readFile()은 Bun에서 Buffer 인코딩 문제가 있을 수 있음
109
+ const headers = {
110
+ "Content-Type": meta.type,
111
+ "Content-Disposition": `inline; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
112
+ "Cache-Control": "public, max-age=31536000, immutable",
113
+ };
114
+ // Bun은 Bun.file()로 직접 BunFile(Blob) 생성 → Response에 그대로 전달
115
+ if (typeof globalThis.Bun !== "undefined") {
116
+ const bunFile = Bun.file(meta.path);
117
+ return new Response(bunFile, { headers });
118
+ }
119
+ // Node.js 폴백
107
120
  const file = await fs.readFile(meta.path);
108
- return new Response(file, {
109
- headers: {
110
- "Content-Type": meta.type,
111
- "Content-Disposition": `attachment; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
112
- "Content-Length": String(meta.size),
113
- },
114
- });
121
+ headers["Content-Length"] = String(file.byteLength);
122
+ return c.body(file, 200, headers);
115
123
  };
116
124
  }
package/package.json CHANGED
@@ -1,38 +1,40 @@
1
1
  {
2
- "name": "@gencow/core",
3
- "version": "0.1.6",
4
- "description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
2
+ "name": "@gencow/core",
3
+ "version": "0.1.8",
4
+ "description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./server": {
14
+ "import": "./dist/server.js",
15
+ "types": "./dist/server.d.ts"
16
+ }
12
17
  },
13
- "./server": {
14
- "import": "./dist/server.js",
15
- "types": "./dist/server.d.ts"
18
+ "files": [
19
+ "dist/",
20
+ "src/"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "typecheck": "tsc --noEmit",
25
+ "prepublishOnly": "npm run build",
26
+ "postinstall": "tsc"
27
+ },
28
+ "dependencies": {
29
+ "@electric-sql/pglite": "^0.3.15",
30
+ "drizzle-orm": "^0.45.1",
31
+ "hono": "^4.12.0",
32
+ "node-cron": "^4.2.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/bun": "^1.3.9",
36
+ "@types/node": "^25.3.0",
37
+ "@types/node-cron": "^3.0.11",
38
+ "typescript": "^5.9.3"
16
39
  }
17
- },
18
- "files": [
19
- "dist/",
20
- "src/"
21
- ],
22
- "dependencies": {
23
- "@electric-sql/pglite": "^0.3.15",
24
- "drizzle-orm": "^0.45.1",
25
- "hono": "^4.12.0",
26
- "node-cron": "^4.2.1"
27
- },
28
- "devDependencies": {
29
- "@types/bun": "^1.3.9",
30
- "@types/node": "^25.3.0",
31
- "@types/node-cron": "^3.0.11",
32
- "typescript": "^5.9.3"
33
- },
34
- "scripts": {
35
- "build": "tsc",
36
- "typecheck": "tsc --noEmit"
37
- }
38
- }
40
+ }
@@ -235,3 +235,80 @@ describe("Secure by Default — public 플래그", () => {
235
235
  expect(priv?.isPublic).toBe(false);
236
236
  });
237
237
  });
238
+
239
+ // ─── mutation("name", def) 새 시그니처 테스트 ────────────────────────────────
240
+
241
+ describe("mutation(name, def) — query와 동일 패턴", () => {
242
+ it("mutation('name', { handler })로 등록하면 name이 올바르게 설정된다", () => {
243
+ const m = mutation("newsig.basic", {
244
+ handler: async () => ({ ok: true }),
245
+ });
246
+ expect((m as any).name || (getRegisteredMutations().find(x => x.invalidates.length === 0 && x.handler === (m as any).handler) as any)?.name).toBeDefined();
247
+ const all = getRegisteredMutations();
248
+ const found = all.find(x => x.name === "newsig.basic");
249
+ expect(found).toBeDefined();
250
+ expect(found!.isPublic).toBe(false);
251
+ });
252
+
253
+ it("mutation('name', { public: true })로 등록하면 isPublic === true", () => {
254
+ const m = mutation("newsig.public", {
255
+ public: true,
256
+ handler: async () => ({ ok: true }),
257
+ });
258
+ expect(m.isPublic).toBe(true);
259
+ });
260
+
261
+ it("invalidates 미지정 시 빈 배열이 기본값", () => {
262
+ const m = mutation("newsig.noInvalidates", {
263
+ handler: async () => ({ ok: true }),
264
+ });
265
+ const all = getRegisteredMutations();
266
+ const found = all.find(x => x.name === "newsig.noInvalidates");
267
+ expect(found!.invalidates).toEqual([]);
268
+ });
269
+
270
+ it("invalidates 지정 시 올바르게 전달된다", () => {
271
+ const m = mutation("newsig.withInvalidates", {
272
+ invalidates: ["tasks.list", "tasks.get"],
273
+ handler: async () => ({ ok: true }),
274
+ });
275
+ const all = getRegisteredMutations();
276
+ const found = all.find(x => x.name === "newsig.withInvalidates");
277
+ expect(found!.invalidates).toEqual(["tasks.list", "tasks.get"]);
278
+ });
279
+
280
+ it("기존 객체 스타일도 여전히 동작한다 (하위 호환)", () => {
281
+ const m = mutation({
282
+ name: "newsig.compat.object",
283
+ invalidates: ["a.list"],
284
+ handler: async () => ({ ok: true }),
285
+ });
286
+ const all = getRegisteredMutations();
287
+ const found = all.find(x => x.name === "newsig.compat.object");
288
+ expect(found).toBeDefined();
289
+ expect(found!.invalidates).toEqual(["a.list"]);
290
+ });
291
+
292
+ it("기존 배열 스타일도 여전히 동작한다 (하위 호환)", () => {
293
+ const m = mutation(["b.list"], async () => ({ ok: true }), "newsig.compat.array");
294
+ const all = getRegisteredMutations();
295
+ const found = all.find(x => x.name === "newsig.compat.array");
296
+ expect(found).toBeDefined();
297
+ expect(found!.invalidates).toEqual(["b.list"]);
298
+ });
299
+
300
+ it("이름 미지정 시 console.warn이 호출된다", () => {
301
+ const warnSpy = mock(() => {});
302
+ const originalWarn = console.warn;
303
+ console.warn = warnSpy;
304
+
305
+ mutation(["c.list"], async () => ({ ok: true }));
306
+
307
+ expect(warnSpy).toHaveBeenCalled();
308
+ const warnMsg = warnSpy.mock.calls[0][0] as string;
309
+ expect(warnMsg).toContain("[gencow]");
310
+ expect(warnMsg).toContain("without explicit name");
311
+
312
+ console.warn = originalWarn;
313
+ });
314
+ });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * packages/core/src/auth-config.ts
3
+ *
4
+ * Gencow Auth 설정 타입 및 defineAuth() 함수.
5
+ * 사용자 앱의 gencow/auth.ts 에서 사용.
6
+ *
7
+ * shadcn 패턴: auth.ts는 사용자가 소유하고 직접 수정할 수 있는 파일.
8
+ * defineAuth()는 타입 안전한 설정 헬퍼일 뿐, 런타임 로직은 server에서 처리.
9
+ */
10
+
11
+ // ─── Email Verification ──────────────────────────────────
12
+
13
+ export interface AuthEmailVerification {
14
+ /** 가입 시 인증 메일 자동 발송 (default: true) */
15
+ sendOnSignUp?: boolean;
16
+ /** 이메일 미인증 시 로그인 차단 (default: true) */
17
+ requireEmailVerification?: boolean;
18
+ /** 인증 완료 후 자동 로그인 (default: true) */
19
+ autoSignInAfterVerification?: boolean;
20
+ /** 인증 메일 발송 함수 — 사용자가 직접 구현 */
21
+ sendVerificationEmail: (data: {
22
+ user: { email: string; name: string };
23
+ url: string;
24
+ token: string;
25
+ }) => Promise<void>;
26
+ }
27
+
28
+ // ─── Auth Config ─────────────────────────────────────────
29
+
30
+ export interface GencowAuthConfig {
31
+ emailVerification?: AuthEmailVerification;
32
+ // 확장 예정:
33
+ // socialProviders?: { ... }
34
+ // passwordPolicy?: { ... }
35
+ // sessionExpiry?: number
36
+ }
37
+
38
+ // ─── defineAuth() ────────────────────────────────────────
39
+
40
+ /**
41
+ * Auth 설정 정의 헬퍼.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // gencow/auth.ts
46
+ * import { defineAuth } from "gencow";
47
+ *
48
+ * export default defineAuth({
49
+ * emailVerification: {
50
+ * sendVerificationEmail: async ({ user, url }) => {
51
+ * // 이메일 발송 로직
52
+ * },
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ export function defineAuth(config: GencowAuthConfig): GencowAuthConfig {
58
+ return config;
59
+ }
package/src/index.ts CHANGED
@@ -16,5 +16,7 @@ export { withRetry } from "./retry";
16
16
  export type { RetryOptions } from "./retry";
17
17
  export { cronJobs } from "./crons";
18
18
  export type { CronJobsBuilder, CronJobDef, IntervalOptions, DailyOptions, WeeklyOptions } from "./crons";
19
+ export { defineAuth } from "./auth-config";
20
+ export type { GencowAuthConfig, AuthEmailVerification } from "./auth-config";
19
21
 
20
22
 
package/src/reactive.ts CHANGED
@@ -61,6 +61,10 @@ export interface AIContext {
61
61
  temperature?: number;
62
62
  maxTokens?: number;
63
63
  }) => Promise<AIResult>;
64
+ /** 텍스트 임베딩 (단일) — ctx.ai.embed("검색 텍스트") */
65
+ embed: (text: string) => Promise<number[]>;
66
+ /** 배치 임베딩 — ctx.ai.embedMany(["텍스트1", "텍스트2"]) */
67
+ embedMany: (texts: string[]) => Promise<number[][]>;
64
68
  }
65
69
 
66
70
  export interface GencowCtx {
@@ -216,9 +220,34 @@ export function query<TSchema = any, TReturn = any>(
216
220
 
217
221
  let mutationCounter = 0;
218
222
 
223
+ /**
224
+ * mutation — 데이터 변경 함수를 선언적으로 등록합니다.
225
+ *
226
+ * 3가지 호출 방식 지원 (query와 동일한 패턴 우선):
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * // ✅ 권장: query와 동일한 (name, def) 패턴
231
+ * mutation("tasks.create", {
232
+ * invalidates: [],
233
+ * args: { title: v.string() },
234
+ * handler: async (ctx, args) => { ... },
235
+ * });
236
+ *
237
+ * // ✅ 객체 스타일 (하위 호환)
238
+ * mutation({
239
+ * name: "tasks.create",
240
+ * invalidates: [],
241
+ * handler: async (ctx) => { ... },
242
+ * });
243
+ *
244
+ * // ⚠️ Legacy 배열 스타일 (비권장)
245
+ * mutation(["tasks.list"], handler, "tasks.create");
246
+ * ```
247
+ */
219
248
  export function mutation<TSchema = any, TReturn = any>(
220
- invalidatesOrDef: string[] | { name?: string; args?: TSchema; public?: boolean; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
221
- handler?: MutationHandler<InferArgs<TSchema>, TReturn>,
249
+ nameOrInvalidatesOrDef: string | string[] | { name?: string; args?: TSchema; public?: boolean; invalidates: string[]; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
250
+ handlerOrDef?: MutationHandler<InferArgs<TSchema>, TReturn> | { invalidates?: string[]; args?: TSchema; public?: boolean; handler: MutationHandler<InferArgs<TSchema>, TReturn> },
222
251
  name?: string
223
252
  ): MutationDef<TSchema, TReturn> {
224
253
  let invalidates: string[];
@@ -227,19 +256,36 @@ export function mutation<TSchema = any, TReturn = any>(
227
256
  let mutName: string;
228
257
  let isPublic = false;
229
258
 
230
- if (Array.isArray(invalidatesOrDef)) {
259
+ if (typeof nameOrInvalidatesOrDef === "string") {
260
+ // New primary style: mutation("name", { invalidates?, args?, public?, handler })
261
+ mutName = nameOrInvalidatesOrDef;
262
+ const def = handlerOrDef as { invalidates?: string[]; args?: TSchema; public?: boolean; handler: MutationHandler<InferArgs<TSchema>, TReturn> };
263
+ invalidates = def.invalidates || [];
264
+ actualHandler = def.handler;
265
+ argsSchema = def.args;
266
+ isPublic = def.public === true;
267
+ } else if (Array.isArray(nameOrInvalidatesOrDef)) {
231
268
  // Legacy style: mutation([...], handler, "name")
232
- invalidates = invalidatesOrDef;
233
- actualHandler = handler!;
269
+ invalidates = nameOrInvalidatesOrDef;
270
+ actualHandler = handlerOrDef as MutationHandler<InferArgs<TSchema>, TReturn>;
234
271
  mutName = name || `mutation_${++mutationCounter}`;
235
272
  } else {
236
- // New object style: mutation({ name?, invalidates, args?, public?, handler })
237
- invalidates = invalidatesOrDef.invalidates;
238
- actualHandler = invalidatesOrDef.handler;
239
- argsSchema = invalidatesOrDef.args;
240
- isPublic = invalidatesOrDef.public === true;
241
- mutName = invalidatesOrDef.name || name || `mutation_${++mutationCounter}`;
273
+ // Object style: mutation({ name?, invalidates, args?, public?, handler })
274
+ invalidates = nameOrInvalidatesOrDef.invalidates;
275
+ actualHandler = nameOrInvalidatesOrDef.handler;
276
+ argsSchema = nameOrInvalidatesOrDef.args;
277
+ isPublic = nameOrInvalidatesOrDef.public === true;
278
+ mutName = nameOrInvalidatesOrDef.name || (typeof name === "string" ? name : "") || `mutation_${++mutationCounter}`;
242
279
  }
280
+
281
+ // 이름 미지정 시 경고 — 디버깅 지원
282
+ if (mutName.startsWith("mutation_")) {
283
+ console.warn(
284
+ `[gencow] mutation registered without explicit name → "${mutName}". ` +
285
+ `Use mutation("myMutation", { handler }) for better debugging.`
286
+ );
287
+ }
288
+
243
289
  const def: MutationDef<TSchema, TReturn> & { name: string } = {
244
290
  name: mutName,
245
291
  invalidates,
package/src/storage.ts CHANGED
@@ -162,13 +162,23 @@ export function storageRoutes(
162
162
  return c.json({ error: "Not found" }, 404);
163
163
  }
164
164
 
165
+ // Bun 런타임에서는 Bun.file()을 사용하여 바이너리 무결성 보장
166
+ // Node의 fs.readFile()은 Bun에서 Buffer 인코딩 문제가 있을 수 있음
167
+ const headers: Record<string, string> = {
168
+ "Content-Type": meta.type,
169
+ "Content-Disposition": `inline; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
170
+ "Cache-Control": "public, max-age=31536000, immutable",
171
+ };
172
+
173
+ // Bun은 Bun.file()로 직접 BunFile(Blob) 생성 → Response에 그대로 전달
174
+ if (typeof globalThis.Bun !== "undefined") {
175
+ const bunFile = Bun.file(meta.path);
176
+ return new Response(bunFile, { headers });
177
+ }
178
+
179
+ // Node.js 폴백
165
180
  const file = await fs.readFile(meta.path);
166
- return new Response(file, {
167
- headers: {
168
- "Content-Type": meta.type,
169
- "Content-Disposition": `attachment; filename="${encodeURIComponent(meta.name)}"; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
170
- "Content-Length": String(meta.size),
171
- },
172
- });
181
+ headers["Content-Length"] = String(file.byteLength);
182
+ return c.body(file, 200, headers);
173
183
  };
174
184
  }