@gencow/core 0.1.2 → 0.1.4

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.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Provides: query, mutation, storage, scheduler, auth
5
5
  * All with Convex-compatible DX patterns.
6
6
  */
7
- export type { GencowCtx, AuthCtx, UserIdentity, QueryDef, MutationDef, RealtimeCtx } from "./reactive";
8
- export { query, mutation, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations } from "./reactive";
7
+ export type { GencowCtx, AuthCtx, UserIdentity, QueryDef, MutationDef, RealtimeCtx, HttpActionDef, HttpActionRequest, HttpActionResponse, HttpActionHandler, AIContext, AIMessage, AIResult } from "./reactive";
8
+ export { query, mutation, httpAction, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
9
9
  export type { Storage } from "./storage";
10
10
  export { createScheduler, getSchedulerInfo } from "./scheduler";
11
11
  export type { Scheduler } from "./scheduler";
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Provides: query, mutation, storage, scheduler, auth
5
5
  * All with Convex-compatible DX patterns.
6
6
  */
7
- export { query, mutation, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations } from "./reactive";
7
+ export { query, mutation, httpAction, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
8
8
  export { createScheduler, getSchedulerInfo } from "./scheduler";
9
9
  export { v, parseArgs, GencowValidationError } from "./v";
10
10
  export { withRetry } from "./retry";
@@ -33,6 +33,29 @@ export interface RealtimeCtx {
33
33
  * Convex의 ctx 패턴과 동일하게, 이 객체를 통해서만 DB/Storage/Auth에 접근 가능.
34
34
  * fs, child_process 등 원시 Node.js API는 노출되지 않음.
35
35
  */
36
+ export interface AIMessage {
37
+ role: "user" | "system" | "assistant";
38
+ content: string;
39
+ }
40
+ export interface AIResult {
41
+ text: string;
42
+ usage: {
43
+ promptTokens: number;
44
+ completionTokens: number;
45
+ totalTokens: number;
46
+ };
47
+ creditsCharged: number;
48
+ model: string;
49
+ }
50
+ export interface AIContext {
51
+ /** AI 텍스트 생성 — ctx.ai.chat({ model, messages }) */
52
+ chat: (opts: {
53
+ model?: string;
54
+ messages: AIMessage[];
55
+ temperature?: number;
56
+ maxTokens?: number;
57
+ }) => Promise<AIResult>;
58
+ }
36
59
  export interface GencowCtx {
37
60
  /** Drizzle DB 인스턴스 — ctx.db.select().from(table) */
38
61
  db: any;
@@ -46,9 +69,43 @@ export interface GencowCtx {
46
69
  realtime: RealtimeCtx;
47
70
  /** 재시도 — ctx.retry(fn, opts) — exponential backoff + jitter */
48
71
  retry: <T>(fn: () => Promise<T>, options?: import("./retry").RetryOptions) => Promise<T>;
72
+ /** AI 헬퍼 — ctx.ai.chat({ model, messages }) — 로컬: 직접 호출, BaaS: Control Plane 프록시 */
73
+ ai?: AIContext;
49
74
  }
50
75
  type QueryHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
51
76
  type MutationHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
77
+ /**
78
+ * httpAction 핸들러의 Request/Response 타입.
79
+ * Hono의 Context를 직접 받아 full HTTP 제어 가능.
80
+ */
81
+ export type HttpActionHandler = (ctx: GencowCtx, req: HttpActionRequest) => Promise<HttpActionResponse>;
82
+ export interface HttpActionRequest {
83
+ /** HTTP method (GET, POST, PUT, DELETE, PATCH) */
84
+ method: string;
85
+ /** Full URL */
86
+ url: string;
87
+ /** URL path (e.g. /api/cli/auth-start) */
88
+ path: string;
89
+ /** Route params (e.g. { id: "abc" } for /api/apps/:id) */
90
+ params: Record<string, string>;
91
+ /** Query string params */
92
+ query: Record<string, string>;
93
+ /** Request headers */
94
+ headers: Record<string, string>;
95
+ /** Parse JSON body */
96
+ json: <T = unknown>() => Promise<T>;
97
+ /** Parse form data */
98
+ formData: () => Promise<FormData>;
99
+ /** Raw body as ArrayBuffer */
100
+ arrayBuffer: () => Promise<ArrayBuffer>;
101
+ /** Raw body as text */
102
+ text: () => Promise<string>;
103
+ }
104
+ export interface HttpActionResponse {
105
+ status?: number;
106
+ headers?: Record<string, string>;
107
+ body?: unknown;
108
+ }
52
109
  export interface QueryDef<TSchema = any, TReturn = any> {
53
110
  key: string;
54
111
  handler: QueryHandler<InferArgs<TSchema>, TReturn>;
@@ -67,6 +124,17 @@ export interface MutationDef<TSchema = any, TReturn = any> {
67
124
  _args?: InferArgs<TSchema>;
68
125
  _return?: TReturn;
69
126
  }
127
+ /** httpAction 정의. 커스텀 HTTP 엔드포인트를 선언적으로 등록. */
128
+ export interface HttpActionDef {
129
+ /** HTTP method */
130
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
131
+ /** URL path (Hono 패턴 — /api/cli/:code 형태 지원) */
132
+ path: string;
133
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 */
134
+ isPublic: boolean;
135
+ /** 핸들러 */
136
+ handler: HttpActionHandler;
137
+ }
70
138
  declare global {
71
139
  var __gencow_queryRegistry: Map<string, QueryDef<any, any>>;
72
140
  var __gencow_mutationRegistry: (MutationDef<any, any> & {
@@ -74,6 +142,7 @@ declare global {
74
142
  })[];
75
143
  var __gencow_subscribers: Map<string, Set<WSContext>>;
76
144
  var __gencow_connectedClients: Set<WSContext>;
145
+ var __gencow_httpActionRegistry: HttpActionDef[];
77
146
  }
78
147
  export declare function query<TSchema = any, TReturn = any>(key: string, handlerOrDef: QueryHandler<InferArgs<TSchema>, TReturn> | {
79
148
  args?: TSchema;
@@ -87,6 +156,38 @@ export declare function mutation<TSchema = any, TReturn = any>(invalidatesOrDef:
87
156
  invalidates: string[];
88
157
  handler: MutationHandler<InferArgs<TSchema>, TReturn>;
89
158
  }, handler?: MutationHandler<InferArgs<TSchema>, TReturn>, name?: string): MutationDef<TSchema, TReturn>;
159
+ /**
160
+ * 커스텀 HTTP 엔드포인트를 선언적으로 등록합니다.
161
+ * query/mutation은 RPC 패턴이지만, httpAction은 RESTful HTTP 라우트를 직접 정의합니다.
162
+ *
163
+ * 사용 사례:
164
+ * - CLI Device Auth (POST /api/cli/auth-start)
165
+ * - 파일 업로드 (POST /api/apps/:id/deploy)
166
+ * - Webhook 수신
167
+ * - 외부 서비스 콜백
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * import { httpAction } from "@gencow/core";
172
+ *
173
+ * export const authStart = httpAction({
174
+ * method: "POST",
175
+ * path: "/api/cli/auth-start",
176
+ * public: true,
177
+ * handler: async (ctx, req) => {
178
+ * const code = crypto.randomUUID().slice(0, 8);
179
+ * return { body: { code, url: `https://gencow.com/cli-auth?code=${code}` } };
180
+ * },
181
+ * });
182
+ * ```
183
+ */
184
+ export declare function httpAction(def: {
185
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
186
+ path: string;
187
+ public?: boolean;
188
+ handler: HttpActionHandler;
189
+ }): HttpActionDef;
190
+ export declare function getRegisteredHttpActions(): HttpActionDef[];
90
191
  export declare function subscribe(queryKey: string, ws: WSContext): void;
91
192
  export declare function unsubscribe(ws: WSContext): void;
92
193
  /** Register a raw WS connection without any query subscription (e.g. admin dashboard) */
package/dist/reactive.js CHANGED
@@ -6,8 +6,11 @@ if (!globalThis.__gencow_subscribers)
6
6
  globalThis.__gencow_subscribers = new Map();
7
7
  if (!globalThis.__gencow_connectedClients)
8
8
  globalThis.__gencow_connectedClients = new Set();
9
+ if (!globalThis.__gencow_httpActionRegistry)
10
+ globalThis.__gencow_httpActionRegistry = [];
9
11
  const queryRegistry = globalThis.__gencow_queryRegistry;
10
12
  const mutationRegistry = globalThis.__gencow_mutationRegistry;
13
+ const httpActionRegistry = globalThis.__gencow_httpActionRegistry;
11
14
  const subscribers = globalThis.__gencow_subscribers;
12
15
  /**
13
16
  * Every WebSocket client that ever establishes a connection is tracked here.
@@ -63,6 +66,45 @@ export function mutation(invalidatesOrDef, handler, name) {
63
66
  mutationRegistry.push(def);
64
67
  return def;
65
68
  }
69
+ // ─── httpAction (커스텀 HTTP 엔드포인트) ────────────────
70
+ /**
71
+ * 커스텀 HTTP 엔드포인트를 선언적으로 등록합니다.
72
+ * query/mutation은 RPC 패턴이지만, httpAction은 RESTful HTTP 라우트를 직접 정의합니다.
73
+ *
74
+ * 사용 사례:
75
+ * - CLI Device Auth (POST /api/cli/auth-start)
76
+ * - 파일 업로드 (POST /api/apps/:id/deploy)
77
+ * - Webhook 수신
78
+ * - 외부 서비스 콜백
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { httpAction } from "@gencow/core";
83
+ *
84
+ * export const authStart = httpAction({
85
+ * method: "POST",
86
+ * path: "/api/cli/auth-start",
87
+ * public: true,
88
+ * handler: async (ctx, req) => {
89
+ * const code = crypto.randomUUID().slice(0, 8);
90
+ * return { body: { code, url: `https://gencow.com/cli-auth?code=${code}` } };
91
+ * },
92
+ * });
93
+ * ```
94
+ */
95
+ export function httpAction(def) {
96
+ const actionDef = {
97
+ method: def.method,
98
+ path: def.path,
99
+ isPublic: def.public === true,
100
+ handler: def.handler,
101
+ };
102
+ httpActionRegistry.push(actionDef);
103
+ return actionDef;
104
+ }
105
+ export function getRegisteredHttpActions() {
106
+ return [...httpActionRegistry];
107
+ }
66
108
  // ─── WebSocket subscription management ──────────────────
67
109
  export function subscribe(queryKey, ws) {
68
110
  connectedClients.add(ws);
package/dist/storage.js CHANGED
@@ -1,6 +1,16 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import * as crypto from "crypto";
4
+ // ─── Constants ──────────────────────────────────────────
5
+ /** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
6
+ const MAX_FILE_SIZE = 50 * 1024 * 1024;
7
+ function formatBytes(bytes) {
8
+ if (bytes < 1024)
9
+ return `${bytes}B`;
10
+ if (bytes < 1024 * 1024)
11
+ return `${(bytes / 1024).toFixed(1)}KB`;
12
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
13
+ }
4
14
  // ─── Implementation ─────────────────────────────────────
5
15
  const metaStore = new Map();
6
16
  /**
@@ -16,6 +26,10 @@ export function createStorage(dir = "./uploads") {
16
26
  fs.mkdir(dir, { recursive: true }).catch(() => { });
17
27
  return {
18
28
  async store(file, filename) {
29
+ // 크기 제한 검증
30
+ if (file.size > MAX_FILE_SIZE) {
31
+ throw new Error(`File too large: ${formatBytes(file.size)} exceeds limit of ${formatBytes(MAX_FILE_SIZE)}`);
32
+ }
19
33
  const id = crypto.randomUUID();
20
34
  const name = filename || (file instanceof File ? file.name : `${id}.bin`);
21
35
  const filePath = path.join(dir, id);
@@ -33,6 +47,10 @@ export function createStorage(dir = "./uploads") {
33
47
  async storeBuffer(buffer, filename, type = "application/octet-stream") {
34
48
  const id = crypto.randomUUID();
35
49
  const filePath = path.join(dir, id);
50
+ // 크기 제한 검증
51
+ if (buffer.length > MAX_FILE_SIZE) {
52
+ throw new Error(`File too large: ${formatBytes(buffer.length)} exceeds limit of ${formatBytes(MAX_FILE_SIZE)}`);
53
+ }
36
54
  await fs.writeFile(filePath, buffer);
37
55
  metaStore.set(id, {
38
56
  id,
package/package.json CHANGED
@@ -1,39 +1,38 @@
1
1
  {
2
- "name": "@gencow/core",
3
- "version": "0.1.2",
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
- }
2
+ "name": "@gencow/core",
3
+ "version": "0.1.4",
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"
17
12
  },
18
- "files": [
19
- "dist/",
20
- "src/"
21
- ],
22
- "scripts": {
23
- "build": "tsc",
24
- "typecheck": "tsc --noEmit",
25
- "prepublishOnly": "npm run build"
26
- },
27
- "dependencies": {
28
- "@electric-sql/pglite": "^0.3.15",
29
- "drizzle-orm": "^0.45.1",
30
- "hono": "^4.12.0",
31
- "node-cron": "^4.2.1"
32
- },
33
- "devDependencies": {
34
- "@types/bun": "^1.3.9",
35
- "@types/node": "^25.3.0",
36
- "@types/node-cron": "^3.0.11",
37
- "typescript": "^5.9.3"
13
+ "./server": {
14
+ "import": "./dist/server.js",
15
+ "types": "./dist/server.d.ts"
38
16
  }
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
+ }
39
38
  }
package/src/index.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * All with Convex-compatible DX patterns.
6
6
  */
7
7
 
8
- export type { GencowCtx, AuthCtx, UserIdentity, QueryDef, MutationDef, RealtimeCtx } from "./reactive";
9
- export { query, mutation, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations } from "./reactive";
8
+ export type { GencowCtx, AuthCtx, UserIdentity, QueryDef, MutationDef, RealtimeCtx, HttpActionDef, HttpActionRequest, HttpActionResponse, HttpActionHandler, AIContext, AIMessage, AIResult } from "./reactive";
9
+ export { query, mutation, httpAction, invalidateQueries, buildRealtimeCtx, subscribe, unsubscribe, registerClient, deregisterClient, handleWsMessage, getQueryHandler, getQueryDef, getRegisteredQueries, getRegisteredMutations, getRegisteredHttpActions } from "./reactive";
10
10
  export type { Storage } from "./storage";
11
11
  export { createScheduler, getSchedulerInfo } from "./scheduler";
12
12
  export type { Scheduler } from "./scheduler";
package/src/reactive.ts CHANGED
@@ -39,6 +39,30 @@ export interface RealtimeCtx {
39
39
  * Convex의 ctx 패턴과 동일하게, 이 객체를 통해서만 DB/Storage/Auth에 접근 가능.
40
40
  * fs, child_process 등 원시 Node.js API는 노출되지 않음.
41
41
  */
42
+ // ─── AI Context ─────────────────────────────────────────
43
+
44
+ export interface AIMessage {
45
+ role: "user" | "system" | "assistant";
46
+ content: string;
47
+ }
48
+
49
+ export interface AIResult {
50
+ text: string;
51
+ usage: { promptTokens: number; completionTokens: number; totalTokens: number };
52
+ creditsCharged: number;
53
+ model: string;
54
+ }
55
+
56
+ export interface AIContext {
57
+ /** AI 텍스트 생성 — ctx.ai.chat({ model, messages }) */
58
+ chat: (opts: {
59
+ model?: string;
60
+ messages: AIMessage[];
61
+ temperature?: number;
62
+ maxTokens?: number;
63
+ }) => Promise<AIResult>;
64
+ }
65
+
42
66
  export interface GencowCtx {
43
67
  /** Drizzle DB 인스턴스 — ctx.db.select().from(table) */
44
68
  db: any; // typed per-app via generic
@@ -52,6 +76,8 @@ export interface GencowCtx {
52
76
  realtime: RealtimeCtx;
53
77
  /** 재시도 — ctx.retry(fn, opts) — exponential backoff + jitter */
54
78
  retry: <T>(fn: () => Promise<T>, options?: import("./retry").RetryOptions) => Promise<T>;
79
+ /** AI 헬퍼 — ctx.ai.chat({ model, messages }) — 로컬: 직접 호출, BaaS: Control Plane 프록시 */
80
+ ai?: AIContext;
55
81
  }
56
82
 
57
83
  // ─── Types ──────────────────────────────────────────────
@@ -61,6 +87,41 @@ type QueryHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) =>
61
87
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
88
  type MutationHandler<TArgs = any, TReturn = any> = (ctx: GencowCtx, args: TArgs) => Promise<TReturn>;
63
89
 
90
+ /**
91
+ * httpAction 핸들러의 Request/Response 타입.
92
+ * Hono의 Context를 직접 받아 full HTTP 제어 가능.
93
+ */
94
+ export type HttpActionHandler = (ctx: GencowCtx, req: HttpActionRequest) => Promise<HttpActionResponse>;
95
+
96
+ export interface HttpActionRequest {
97
+ /** HTTP method (GET, POST, PUT, DELETE, PATCH) */
98
+ method: string;
99
+ /** Full URL */
100
+ url: string;
101
+ /** URL path (e.g. /api/cli/auth-start) */
102
+ path: string;
103
+ /** Route params (e.g. { id: "abc" } for /api/apps/:id) */
104
+ params: Record<string, string>;
105
+ /** Query string params */
106
+ query: Record<string, string>;
107
+ /** Request headers */
108
+ headers: Record<string, string>;
109
+ /** Parse JSON body */
110
+ json: <T = unknown>() => Promise<T>;
111
+ /** Parse form data */
112
+ formData: () => Promise<FormData>;
113
+ /** Raw body as ArrayBuffer */
114
+ arrayBuffer: () => Promise<ArrayBuffer>;
115
+ /** Raw body as text */
116
+ text: () => Promise<string>;
117
+ }
118
+
119
+ export interface HttpActionResponse {
120
+ status?: number;
121
+ headers?: Record<string, string>;
122
+ body?: unknown;
123
+ }
124
+
64
125
  export interface QueryDef<TSchema = any, TReturn = any> {
65
126
  key: string;
66
127
  handler: QueryHandler<InferArgs<TSchema>, TReturn>;
@@ -81,6 +142,18 @@ export interface MutationDef<TSchema = any, TReturn = any> {
81
142
  _return?: TReturn;
82
143
  }
83
144
 
145
+ /** httpAction 정의. 커스텀 HTTP 엔드포인트를 선언적으로 등록. */
146
+ export interface HttpActionDef {
147
+ /** HTTP method */
148
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
149
+ /** URL path (Hono 패턴 — /api/cli/:code 형태 지원) */
150
+ path: string;
151
+ /** true = 인증 없이 접근 가능, false(기본) = auth 필수 */
152
+ isPublic: boolean;
153
+ /** 핸들러 */
154
+ handler: HttpActionHandler;
155
+ }
156
+
84
157
  // ─── Registry ───────────────────────────────────────────
85
158
  //
86
159
  // globalThis 기반 — 서버 번들(인라인)과 node_modules/@gencow/core 양쪽에서
@@ -96,15 +169,19 @@ declare global {
96
169
  var __gencow_subscribers: Map<string, Set<WSContext>>;
97
170
  // eslint-disable-next-line no-var
98
171
  var __gencow_connectedClients: Set<WSContext>;
172
+ // eslint-disable-next-line no-var
173
+ var __gencow_httpActionRegistry: HttpActionDef[];
99
174
  }
100
175
 
101
176
  if (!globalThis.__gencow_queryRegistry) globalThis.__gencow_queryRegistry = new Map();
102
177
  if (!globalThis.__gencow_mutationRegistry) globalThis.__gencow_mutationRegistry = [];
103
178
  if (!globalThis.__gencow_subscribers) globalThis.__gencow_subscribers = new Map();
104
179
  if (!globalThis.__gencow_connectedClients) globalThis.__gencow_connectedClients = new Set();
180
+ if (!globalThis.__gencow_httpActionRegistry) globalThis.__gencow_httpActionRegistry = [];
105
181
 
106
182
  const queryRegistry = globalThis.__gencow_queryRegistry;
107
183
  const mutationRegistry = globalThis.__gencow_mutationRegistry;
184
+ const httpActionRegistry = globalThis.__gencow_httpActionRegistry;
108
185
  const subscribers = globalThis.__gencow_subscribers;
109
186
 
110
187
  /**
@@ -174,6 +251,53 @@ export function mutation<TSchema = any, TReturn = any>(
174
251
  return def;
175
252
  }
176
253
 
254
+ // ─── httpAction (커스텀 HTTP 엔드포인트) ────────────────
255
+
256
+ /**
257
+ * 커스텀 HTTP 엔드포인트를 선언적으로 등록합니다.
258
+ * query/mutation은 RPC 패턴이지만, httpAction은 RESTful HTTP 라우트를 직접 정의합니다.
259
+ *
260
+ * 사용 사례:
261
+ * - CLI Device Auth (POST /api/cli/auth-start)
262
+ * - 파일 업로드 (POST /api/apps/:id/deploy)
263
+ * - Webhook 수신
264
+ * - 외부 서비스 콜백
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * import { httpAction } from "@gencow/core";
269
+ *
270
+ * export const authStart = httpAction({
271
+ * method: "POST",
272
+ * path: "/api/cli/auth-start",
273
+ * public: true,
274
+ * handler: async (ctx, req) => {
275
+ * const code = crypto.randomUUID().slice(0, 8);
276
+ * return { body: { code, url: `https://gencow.com/cli-auth?code=${code}` } };
277
+ * },
278
+ * });
279
+ * ```
280
+ */
281
+ export function httpAction(def: {
282
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
283
+ path: string;
284
+ public?: boolean;
285
+ handler: HttpActionHandler;
286
+ }): HttpActionDef {
287
+ const actionDef: HttpActionDef = {
288
+ method: def.method,
289
+ path: def.path,
290
+ isPublic: def.public === true,
291
+ handler: def.handler,
292
+ };
293
+ httpActionRegistry.push(actionDef);
294
+ return actionDef;
295
+ }
296
+
297
+ export function getRegisteredHttpActions(): HttpActionDef[] {
298
+ return [...httpActionRegistry];
299
+ }
300
+
177
301
  // ─── WebSocket subscription management ──────────────────
178
302
 
179
303
  export function subscribe(queryKey: string, ws: WSContext) {
package/src/storage.ts CHANGED
@@ -2,6 +2,17 @@ import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import * as crypto from "crypto";
4
4
 
5
+ // ─── Constants ──────────────────────────────────────────
6
+
7
+ /** 파일 업로드 최대 크기: 50MB (하드코딩 — 사용자가 오버라이드 불가) */
8
+ const MAX_FILE_SIZE = 50 * 1024 * 1024;
9
+
10
+ function formatBytes(bytes: number): string {
11
+ if (bytes < 1024) return `${bytes}B`;
12
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
13
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
14
+ }
15
+
5
16
  // ─── Types ──────────────────────────────────────────────
6
17
 
7
18
  interface StorageFile {
@@ -43,6 +54,13 @@ export function createStorage(dir: string = "./uploads"): Storage {
43
54
 
44
55
  return {
45
56
  async store(file: File | Blob, filename?: string): Promise<string> {
57
+ // 크기 제한 검증
58
+ if (file.size > MAX_FILE_SIZE) {
59
+ throw new Error(
60
+ `File too large: ${formatBytes(file.size)} exceeds limit of ${formatBytes(MAX_FILE_SIZE)}`
61
+ );
62
+ }
63
+
46
64
  const id = crypto.randomUUID();
47
65
  const name = filename || (file instanceof File ? file.name : `${id}.bin`);
48
66
  const filePath = path.join(dir, id);
@@ -69,6 +87,13 @@ export function createStorage(dir: string = "./uploads"): Storage {
69
87
  const id = crypto.randomUUID();
70
88
  const filePath = path.join(dir, id);
71
89
 
90
+ // 크기 제한 검증
91
+ if (buffer.length > MAX_FILE_SIZE) {
92
+ throw new Error(
93
+ `File too large: ${formatBytes(buffer.length)} exceeds limit of ${formatBytes(MAX_FILE_SIZE)}`
94
+ );
95
+ }
96
+
72
97
  await fs.writeFile(filePath, buffer);
73
98
 
74
99
  metaStore.set(id, {