swallowkit 1.0.0-beta.2 → 1.0.0-beta.21

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.
Files changed (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.ja.md +312 -215
  3. package/README.md +369 -216
  4. package/dist/__tests__/fixtures.d.ts +22 -0
  5. package/dist/__tests__/fixtures.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures.js +146 -0
  7. package/dist/__tests__/fixtures.js.map +1 -0
  8. package/dist/cli/commands/add-auth.d.ts +10 -0
  9. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  10. package/dist/cli/commands/add-auth.js +444 -0
  11. package/dist/cli/commands/add-auth.js.map +1 -0
  12. package/dist/cli/commands/add-connector.d.ts +20 -0
  13. package/dist/cli/commands/add-connector.d.ts.map +1 -0
  14. package/dist/cli/commands/add-connector.js +163 -0
  15. package/dist/cli/commands/add-connector.js.map +1 -0
  16. package/dist/cli/commands/create-model.d.ts +1 -4
  17. package/dist/cli/commands/create-model.d.ts.map +1 -1
  18. package/dist/cli/commands/create-model.js +21 -82
  19. package/dist/cli/commands/create-model.js.map +1 -1
  20. package/dist/cli/commands/dev-seeds.d.ts +35 -0
  21. package/dist/cli/commands/dev-seeds.d.ts.map +1 -0
  22. package/dist/cli/commands/dev-seeds.js +292 -0
  23. package/dist/cli/commands/dev-seeds.js.map +1 -0
  24. package/dist/cli/commands/dev.d.ts +19 -0
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +476 -117
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/index.d.ts +1 -0
  29. package/dist/cli/commands/index.d.ts.map +1 -1
  30. package/dist/cli/commands/index.js +3 -1
  31. package/dist/cli/commands/index.js.map +1 -1
  32. package/dist/cli/commands/init.d.ts +13 -0
  33. package/dist/cli/commands/init.d.ts.map +1 -1
  34. package/dist/cli/commands/init.js +2627 -1708
  35. package/dist/cli/commands/init.js.map +1 -1
  36. package/dist/cli/commands/scaffold.d.ts +3 -0
  37. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  38. package/dist/cli/commands/scaffold.js +617 -129
  39. package/dist/cli/commands/scaffold.js.map +1 -1
  40. package/dist/cli/index.d.ts +5 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +164 -42
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/core/config.d.ts +8 -2
  45. package/dist/core/config.d.ts.map +1 -1
  46. package/dist/core/config.js +90 -4
  47. package/dist/core/config.js.map +1 -1
  48. package/dist/core/mock/connector-mock-server.d.ts +101 -0
  49. package/dist/core/mock/connector-mock-server.d.ts.map +1 -0
  50. package/dist/core/mock/connector-mock-server.js +480 -0
  51. package/dist/core/mock/connector-mock-server.js.map +1 -0
  52. package/dist/core/mock/zod-mock-generator.d.ts +14 -0
  53. package/dist/core/mock/zod-mock-generator.d.ts.map +1 -0
  54. package/dist/core/mock/zod-mock-generator.js +163 -0
  55. package/dist/core/mock/zod-mock-generator.js.map +1 -0
  56. package/dist/core/operations/create-model.d.ts +15 -0
  57. package/dist/core/operations/create-model.d.ts.map +1 -0
  58. package/dist/core/operations/create-model.js +171 -0
  59. package/dist/core/operations/create-model.js.map +1 -0
  60. package/dist/core/operations/runtime.d.ts +32 -0
  61. package/dist/core/operations/runtime.d.ts.map +1 -0
  62. package/dist/core/operations/runtime.js +225 -0
  63. package/dist/core/operations/runtime.js.map +1 -0
  64. package/dist/core/operations/scaffold-machine.d.ts +16 -0
  65. package/dist/core/operations/scaffold-machine.d.ts.map +1 -0
  66. package/dist/core/operations/scaffold-machine.js +63 -0
  67. package/dist/core/operations/scaffold-machine.js.map +1 -0
  68. package/dist/core/project/manifest.d.ts +92 -0
  69. package/dist/core/project/manifest.d.ts.map +1 -0
  70. package/dist/core/project/manifest.js +321 -0
  71. package/dist/core/project/manifest.js.map +1 -0
  72. package/dist/core/project/validation.d.ts +20 -0
  73. package/dist/core/project/validation.d.ts.map +1 -0
  74. package/dist/core/project/validation.js +204 -0
  75. package/dist/core/project/validation.js.map +1 -0
  76. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  77. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  78. package/dist/core/scaffold/auth-generator.js +1244 -0
  79. package/dist/core/scaffold/auth-generator.js.map +1 -0
  80. package/dist/core/scaffold/connector-functions-generator.d.ts +41 -0
  81. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -0
  82. package/dist/core/scaffold/connector-functions-generator.js +1027 -0
  83. package/dist/core/scaffold/connector-functions-generator.js.map +1 -0
  84. package/dist/core/scaffold/functions-generator.d.ts +7 -1
  85. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  86. package/dist/core/scaffold/functions-generator.js +920 -213
  87. package/dist/core/scaffold/functions-generator.js.map +1 -1
  88. package/dist/core/scaffold/model-parser.d.ts +20 -1
  89. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  90. package/dist/core/scaffold/model-parser.js +329 -135
  91. package/dist/core/scaffold/model-parser.js.map +1 -1
  92. package/dist/core/scaffold/nextjs-generator.d.ts +8 -0
  93. package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
  94. package/dist/core/scaffold/nextjs-generator.js +314 -182
  95. package/dist/core/scaffold/nextjs-generator.js.map +1 -1
  96. package/dist/core/scaffold/openapi-generator.d.ts +3 -0
  97. package/dist/core/scaffold/openapi-generator.d.ts.map +1 -0
  98. package/dist/core/scaffold/openapi-generator.js +190 -0
  99. package/dist/core/scaffold/openapi-generator.js.map +1 -0
  100. package/dist/core/scaffold/ui-generator.d.ts +10 -4
  101. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  102. package/dist/core/scaffold/ui-generator.js +768 -663
  103. package/dist/core/scaffold/ui-generator.js.map +1 -1
  104. package/dist/database/base-model.d.ts +3 -3
  105. package/dist/database/base-model.js +3 -3
  106. package/dist/index.d.ts +2 -2
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +2 -1
  109. package/dist/index.js.map +1 -1
  110. package/dist/machine/contracts.d.ts +16 -0
  111. package/dist/machine/contracts.d.ts.map +1 -0
  112. package/dist/machine/contracts.js +3 -0
  113. package/dist/machine/contracts.js.map +1 -0
  114. package/dist/machine/errors.d.ts +11 -0
  115. package/dist/machine/errors.d.ts.map +1 -0
  116. package/dist/machine/errors.js +34 -0
  117. package/dist/machine/errors.js.map +1 -0
  118. package/dist/machine/index.d.ts +3 -0
  119. package/dist/machine/index.d.ts.map +1 -0
  120. package/dist/machine/index.js +156 -0
  121. package/dist/machine/index.js.map +1 -0
  122. package/dist/mcp/index.d.ts +25 -0
  123. package/dist/mcp/index.d.ts.map +1 -0
  124. package/dist/mcp/index.js +184 -0
  125. package/dist/mcp/index.js.map +1 -0
  126. package/dist/types/index.d.ts +65 -0
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/utils/package-manager.d.ts +109 -0
  129. package/dist/utils/package-manager.d.ts.map +1 -0
  130. package/dist/utils/package-manager.js +215 -0
  131. package/dist/utils/package-manager.js.map +1 -0
  132. package/package.json +85 -73
  133. package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +1139 -0
  134. package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +194 -0
  135. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +532 -0
  136. package/src/__tests__/auth.test.ts +654 -0
  137. package/src/__tests__/config.test.ts +263 -0
  138. package/src/__tests__/connector-functions-generator.test.ts +288 -0
  139. package/src/__tests__/connector-mock-server.test.ts +439 -0
  140. package/src/__tests__/connector-model-bff.test.ts +162 -0
  141. package/src/__tests__/dev-seeds.test.ts +112 -0
  142. package/src/__tests__/dev.test.ts +154 -0
  143. package/src/__tests__/fixtures.ts +144 -0
  144. package/src/__tests__/functions-generator.test.ts +237 -0
  145. package/src/__tests__/init.test.ts +80 -0
  146. package/src/__tests__/machine.test.ts +212 -0
  147. package/src/__tests__/mcp.test.ts +56 -0
  148. package/src/__tests__/model-parser.test.ts +72 -0
  149. package/src/__tests__/nextjs-generator.test.ts +97 -0
  150. package/src/__tests__/openapi-generator.test.ts +43 -0
  151. package/src/__tests__/package-manager.test.ts +189 -0
  152. package/src/__tests__/scaffold.test.ts +39 -0
  153. package/src/__tests__/string-utils.test.ts +75 -0
  154. package/src/__tests__/ui-generator.test.ts +144 -0
  155. package/src/__tests__/zod-mock-generator.test.ts +132 -0
  156. package/src/cli/commands/add-auth.ts +500 -0
  157. package/src/cli/commands/add-connector.ts +158 -0
  158. package/src/cli/commands/create-model.ts +62 -0
  159. package/src/cli/commands/dev-seeds.ts +358 -0
  160. package/src/cli/commands/dev.ts +962 -0
  161. package/src/cli/commands/index.ts +9 -0
  162. package/src/cli/commands/init.ts +3371 -0
  163. package/src/cli/commands/provision.ts +193 -0
  164. package/src/cli/commands/scaffold.ts +1211 -0
  165. package/src/cli/index.ts +193 -0
  166. package/src/core/config.ts +308 -0
  167. package/src/core/mock/connector-mock-server.ts +555 -0
  168. package/src/core/mock/zod-mock-generator.ts +205 -0
  169. package/src/core/operations/create-model.ts +174 -0
  170. package/src/core/operations/runtime.ts +235 -0
  171. package/src/core/operations/scaffold-machine.ts +91 -0
  172. package/src/core/project/manifest.ts +402 -0
  173. package/src/core/project/validation.ts +221 -0
  174. package/src/core/scaffold/auth-generator.ts +1284 -0
  175. package/src/core/scaffold/connector-functions-generator.ts +1128 -0
  176. package/src/core/scaffold/functions-generator.ts +970 -0
  177. package/src/core/scaffold/model-parser.ts +841 -0
  178. package/src/core/scaffold/nextjs-generator.ts +370 -0
  179. package/src/core/scaffold/openapi-generator.ts +212 -0
  180. package/src/core/scaffold/ui-generator.ts +1061 -0
  181. package/src/database/base-model.ts +184 -0
  182. package/src/database/client.ts +140 -0
  183. package/src/database/repository.ts +104 -0
  184. package/src/database/runtime-check.ts +25 -0
  185. package/src/index.ts +27 -0
  186. package/src/machine/contracts.ts +17 -0
  187. package/src/machine/errors.ts +34 -0
  188. package/src/machine/index.ts +173 -0
  189. package/src/mcp/index.ts +185 -0
  190. package/src/types/index.ts +134 -0
  191. package/src/utils/package-manager.ts +229 -0
@@ -0,0 +1,555 @@
1
+ /**
2
+ * コネクタモデル用モックサーバー
3
+ * - コネクタモデルへのリクエスト → Zodベースのインメモリ CRUD で応答
4
+ * - その他のリクエスト → 実Azure Functions へプロキシ
5
+ */
6
+
7
+ import * as http from "http";
8
+ import jwt, { SignOptions } from "jsonwebtoken";
9
+ import { ModelInfo, toCamelCase } from "../scaffold/model-parser";
10
+ import { generateMockDocuments } from "./zod-mock-generator";
11
+ import { loadDevSeedFiles } from "../../cli/commands/dev-seeds";
12
+
13
+ export interface ConnectorMockServerOptions {
14
+ /** モックサーバーのリッスンポート */
15
+ port: number;
16
+ /** Azure Functions の転送先ホスト:ポート(例: "localhost:7071") */
17
+ functionsTarget: string;
18
+ /** コネクタモデル一覧 */
19
+ connectorModels: ModelInfo[];
20
+ /** 全モデル一覧(dev-seeds 読み込み用) */
21
+ allModels?: ModelInfo[];
22
+ /** dev-seeds 環境名(指定時はシードデータを初期データとして読み込む) */
23
+ seedEnv?: string;
24
+ /** dev-seeds ディレクトリ */
25
+ seedsDir?: string;
26
+ /** 各モデルの初期生成レコード数(デフォルト: 5) */
27
+ mockCount?: number;
28
+ /** ホスト名 */
29
+ host?: string;
30
+ /** Auth config — auth functions use RDB connector, mocked the same way */
31
+ authConfig?: {
32
+ /** JWT secret for mock token generation/verification */
33
+ jwtSecret: string;
34
+ /** Token expiry (e.g., '24h') */
35
+ tokenExpiry?: string;
36
+ /** Custom JWT config from swallowkit.config.js */
37
+ customJwt?: {
38
+ /** RDB table name that holds user records (e.g., "users") */
39
+ userTable: string;
40
+ loginIdColumn: string;
41
+ passwordHashColumn: string;
42
+ rolesColumn: string;
43
+ };
44
+ /** Default auth policy: 'authenticated' = all models need auth, 'anonymous' = only models with authPolicy */
45
+ defaultPolicy?: "authenticated" | "anonymous";
46
+ };
47
+ }
48
+
49
+ type MockDocument = Record<string, unknown>;
50
+
51
+ /**
52
+ * コネクタモデル用モックサーバー
53
+ */
54
+ export class ConnectorMockServer {
55
+ private server: http.Server | null = null;
56
+ private stores = new Map<string, MockDocument[]>();
57
+ private routeMap = new Map<string, ModelInfo>(); // route → model
58
+ private options: ConnectorMockServerOptions;
59
+
60
+ constructor(options: ConnectorMockServerOptions) {
61
+ this.options = options;
62
+
63
+ // ルートマップを構築: モデル名(camelCase) → ModelInfo
64
+ for (const model of options.connectorModels) {
65
+ const route = toCamelCase(model.name);
66
+ this.routeMap.set(route, model);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * モックデータを初期化し、サーバーを起動
72
+ */
73
+ async start(): Promise<void> {
74
+ await this.initializeStores();
75
+
76
+ return new Promise((resolve, reject) => {
77
+ this.server = http.createServer((req, res) => {
78
+ this.handleRequest(req, res);
79
+ });
80
+
81
+ this.server.on("error", reject);
82
+ this.server.listen(this.options.port, this.options.host || "localhost", () => {
83
+ resolve();
84
+ });
85
+ });
86
+ }
87
+
88
+ /**
89
+ * サーバーを停止
90
+ */
91
+ stop(): Promise<void> {
92
+ return new Promise((resolve) => {
93
+ if (this.server) {
94
+ // Force close keep-alive connections (Node.js 18.2+)
95
+ if (typeof (this.server as any).closeAllConnections === "function") {
96
+ (this.server as any).closeAllConnections();
97
+ }
98
+ this.server.close(() => resolve());
99
+ } else {
100
+ resolve();
101
+ }
102
+ });
103
+ }
104
+
105
+ /**
106
+ * 特定モデルの現在のストアデータを取得(テスト用)
107
+ */
108
+ getStore(modelName: string): MockDocument[] {
109
+ const route = toCamelCase(modelName);
110
+ return this.stores.get(route) || [];
111
+ }
112
+
113
+ // ─── Internal ─────────────────────────────────────────────
114
+
115
+ private async initializeStores() {
116
+ const allModels = this.options.allModels || this.options.connectorModels;
117
+ const mockCount = this.options.mockCount ?? 5;
118
+
119
+ // まず Zod ベースのモックデータで初期化
120
+ for (const model of this.options.connectorModels) {
121
+ const route = toCamelCase(model.name);
122
+ this.stores.set(route, generateMockDocuments(model, mockCount, allModels));
123
+ }
124
+
125
+ // dev-seeds があれば上書き
126
+ if (this.options.seedEnv) {
127
+ try {
128
+ const seedFiles = await loadDevSeedFiles(
129
+ this.options.seedEnv,
130
+ this.options.connectorModels,
131
+ this.options.seedsDir
132
+ );
133
+ for (const seedFile of seedFiles) {
134
+ const route = toCamelCase(seedFile.model.name);
135
+ if (this.stores.has(route)) {
136
+ this.stores.set(route, seedFile.documents as MockDocument[]);
137
+ console.log(` 📂 Loaded ${seedFile.documents.length} seed doc(s) for ${seedFile.model.name}`);
138
+ }
139
+ }
140
+ } catch (err) {
141
+ console.warn(`⚠️ Failed to load dev-seeds for connectors: ${(err as Error).message}`);
142
+ }
143
+ }
144
+ }
145
+
146
+ private handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
147
+ const { route, id } = this.parseRoute(req.url || "");
148
+
149
+ // Auth endpoints (when authConfig is set)
150
+ if (this.options.authConfig && route === "auth") {
151
+ this.handleAuthRequest(req, res, id);
152
+ return;
153
+ }
154
+
155
+ // コネクタモデルのルートか判定
156
+ if (route && this.routeMap.has(route)) {
157
+ this.handleMockCrud(req, res, route, id);
158
+ } else {
159
+ this.proxyToFunctions(req, res);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * URL パス解析: /api/backlogIssue/123 → { route: "backlogIssue", id: "123" }
165
+ */
166
+ private parseRoute(url: string): { route: string | null; id: string | null } {
167
+ const parsed = new URL(url, "http://localhost");
168
+ const segments = parsed.pathname.split("/").filter(Boolean);
169
+
170
+ // /api/<route> or /api/<route>/<id>
171
+ if (segments.length >= 2 && segments[0] === "api") {
172
+ return {
173
+ route: segments[1],
174
+ id: segments.length >= 3 ? segments[2] : null,
175
+ };
176
+ }
177
+
178
+ return { route: null, id: null };
179
+ }
180
+
181
+ private handleMockCrud(
182
+ req: http.IncomingMessage,
183
+ res: http.ServerResponse,
184
+ route: string,
185
+ id: string | null
186
+ ) {
187
+ const method = (req.method || "GET").toUpperCase();
188
+ const store = this.stores.get(route) || [];
189
+ const model = this.routeMap.get(route)!;
190
+ const ops = model.connectorConfig?.operations || [];
191
+
192
+ const isWrite = method === "POST" || method === "PUT" || method === "DELETE";
193
+ const requiredRoles = this.resolveRequiredRoles(model, isWrite);
194
+ const authResult = this.checkAuth(req, res, model, requiredRoles);
195
+ if (authResult === "error") return; // 401/403 already sent
196
+
197
+ switch (method) {
198
+ case "GET":
199
+ req.resume();
200
+ if (id) {
201
+ if (!ops.includes("getById")) {
202
+ return this.sendJson(res, 405, { error: "getById not supported" });
203
+ }
204
+ const item = store.find((doc) => doc.id === id);
205
+ return item
206
+ ? this.sendJson(res, 200, item)
207
+ : this.sendJson(res, 404, { error: "Item not found" });
208
+ }
209
+ if (!ops.includes("getAll")) {
210
+ return this.sendJson(res, 405, { error: "getAll not supported" });
211
+ }
212
+ return this.sendJson(res, 200, store);
213
+
214
+ case "POST":
215
+ if (!ops.includes("create")) {
216
+ return this.drainAndRespond(req, res, 405, { error: "create not supported" });
217
+ }
218
+ this.readBody(req, (body) => {
219
+ if (!body.id) {
220
+ body.id = `${route}-${Date.now()}`;
221
+ }
222
+ body.createdAt = body.createdAt || new Date().toISOString();
223
+ body.updatedAt = body.updatedAt || new Date().toISOString();
224
+ store.push(body);
225
+ this.sendJson(res, 201, body);
226
+ });
227
+ return;
228
+
229
+ case "PUT":
230
+ if (!ops.includes("update")) {
231
+ return this.drainAndRespond(req, res, 405, { error: "update not supported" });
232
+ }
233
+ if (!id) return this.drainAndRespond(req, res, 400, { error: "id required" });
234
+ this.readBody(req, (body) => {
235
+ const idx = store.findIndex((doc) => doc.id === id);
236
+ if (idx === -1) return this.sendJson(res, 404, { error: "Item not found" });
237
+ body.updatedAt = new Date().toISOString();
238
+ store[idx] = { ...store[idx], ...body, id };
239
+ this.sendJson(res, 200, store[idx]);
240
+ });
241
+ return;
242
+
243
+ case "DELETE":
244
+ {
245
+ req.resume();
246
+ if (!ops.includes("delete")) {
247
+ return this.sendJson(res, 405, { error: "delete not supported" });
248
+ }
249
+ if (!id) return this.sendJson(res, 400, { error: "id required" });
250
+ const deleteIdx = store.findIndex((doc) => doc.id === id);
251
+ if (deleteIdx === -1) return this.sendJson(res, 404, { error: "Item not found" });
252
+ store.splice(deleteIdx, 1);
253
+ return this.sendJson(res, 204, null);
254
+ }
255
+
256
+ default:
257
+ req.resume();
258
+ return this.sendJson(res, 405, { error: `Method ${method} not allowed` });
259
+ }
260
+ }
261
+
262
+ /**
263
+ * コネクタモデル以外のリクエストをAzure Functionsへプロキシ
264
+ */
265
+ private proxyToFunctions(req: http.IncomingMessage, res: http.ServerResponse) {
266
+ const [targetHost, targetPort] = this.options.functionsTarget.split(":");
267
+
268
+ const proxyOpts: http.RequestOptions = {
269
+ hostname: targetHost,
270
+ port: parseInt(targetPort, 10),
271
+ path: req.url,
272
+ method: req.method,
273
+ headers: { ...req.headers, host: this.options.functionsTarget },
274
+ };
275
+
276
+ const proxyReq = http.request(proxyOpts, (proxyRes) => {
277
+ res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
278
+ proxyRes.pipe(res, { end: true });
279
+ });
280
+
281
+ proxyReq.on("error", (err) => {
282
+ // Functions がまだ起動していない場合
283
+ this.sendJson(res, 502, {
284
+ error: "Azure Functions not available",
285
+ detail: err.message,
286
+ });
287
+ });
288
+
289
+ req.pipe(proxyReq, { end: true });
290
+ }
291
+
292
+ // ─── Auth Routes (RDB user queries + JWT generation) ───
293
+
294
+ private handleAuthRequest(
295
+ req: http.IncomingMessage,
296
+ res: http.ServerResponse,
297
+ endpoint: string | null
298
+ ) {
299
+ const method = (req.method || "GET").toUpperCase();
300
+
301
+ // Handle CORS preflight
302
+ if (method === "OPTIONS") {
303
+ req.resume();
304
+ res.writeHead(200, {
305
+ "Access-Control-Allow-Origin": "*",
306
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
307
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
308
+ });
309
+ res.end();
310
+ return;
311
+ }
312
+
313
+ if (endpoint === "login" && method === "POST") {
314
+ this.handleLogin(req, res);
315
+ } else if (endpoint === "me" && method === "GET") {
316
+ this.handleMe(req, res);
317
+ } else if (endpoint === "logout" && method === "POST") {
318
+ req.resume();
319
+ this.sendJson(res, 200, { message: "Logged out" });
320
+ } else {
321
+ req.resume();
322
+ this.sendJson(res, 404, { error: "Auth endpoint not found" });
323
+ }
324
+ }
325
+
326
+ private handleLogin(req: http.IncomingMessage, res: http.ServerResponse) {
327
+ this.readBody(req, (body) => {
328
+ const loginId = body.loginId as string;
329
+ const password = body.password as string;
330
+
331
+ if (!loginId || !password) {
332
+ return this.sendJson(res, 400, { error: "loginId and password are required" });
333
+ }
334
+
335
+ const users = this.resolveUserStore();
336
+ if (!users) {
337
+ return this.sendJson(res, 500, {
338
+ error: "No user model found — ensure a connector model with the configured userTable exists",
339
+ });
340
+ }
341
+
342
+ const loginField = this.options.authConfig?.customJwt?.loginIdColumn || "loginId";
343
+ const passwordField = this.options.authConfig?.customJwt?.passwordHashColumn || "password";
344
+ const rolesField = this.options.authConfig?.customJwt?.rolesColumn || "roles";
345
+
346
+ const user = users.find(
347
+ (u) => (u[loginField] || u.loginId) === loginId
348
+ );
349
+
350
+ if (!user) {
351
+ return this.sendJson(res, 401, { error: "Invalid credentials" });
352
+ }
353
+
354
+ // Plaintext password comparison (mock mode only)
355
+ const storedPassword = (user[passwordField] || user.password) as string;
356
+ if (storedPassword !== password) {
357
+ return this.sendJson(res, 401, { error: "Invalid credentials" });
358
+ }
359
+
360
+ // Parse roles
361
+ let roles: string[] = [];
362
+ const rolesValue = user[rolesField] || user.roles;
363
+ if (Array.isArray(rolesValue)) {
364
+ roles = rolesValue as string[];
365
+ } else if (typeof rolesValue === "string") {
366
+ try {
367
+ roles = JSON.parse(rolesValue);
368
+ } catch {
369
+ roles = (rolesValue as string).split(",").map((r: string) => r.trim());
370
+ }
371
+ }
372
+
373
+ const authUser = {
374
+ id: String(user.id),
375
+ loginId: String(user[loginField] || user.loginId),
376
+ name: String(user.name || user[loginField] || user.loginId),
377
+ email: String(user.email || ""),
378
+ roles,
379
+ };
380
+
381
+ // Generate JWT
382
+ const secret = this.options.authConfig!.jwtSecret;
383
+ const expiry = (this.options.authConfig!.tokenExpiry || "24h") as SignOptions["expiresIn"];
384
+
385
+ const token = jwt.sign(
386
+ {
387
+ sub: authUser.id,
388
+ loginId: authUser.loginId,
389
+ name: authUser.name,
390
+ email: authUser.email,
391
+ roles: authUser.roles,
392
+ },
393
+ secret,
394
+ { expiresIn: expiry }
395
+ );
396
+
397
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
398
+
399
+ this.sendJson(res, 200, { user: authUser, token, expiresAt });
400
+ });
401
+ }
402
+
403
+ private handleMe(req: http.IncomingMessage, res: http.ServerResponse) {
404
+ req.resume();
405
+
406
+ const authHeader = req.headers["authorization"];
407
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
408
+ return this.sendJson(res, 401, { error: "Missing or invalid Authorization header" });
409
+ }
410
+
411
+ const token = authHeader.slice(7);
412
+ try {
413
+ const secret = this.options.authConfig!.jwtSecret;
414
+ const payload = jwt.verify(token, secret) as Record<string, unknown>;
415
+ this.sendJson(res, 200, {
416
+ sub: payload.sub,
417
+ loginId: payload.loginId,
418
+ name: payload.name,
419
+ email: payload.email,
420
+ roles: payload.roles,
421
+ });
422
+ } catch {
423
+ this.sendJson(res, 401, { error: "Invalid or expired token" });
424
+ }
425
+ }
426
+
427
+ // ─── Utilities ────────────────────────────────────────────
428
+
429
+ /**
430
+ * authConfig.customJwt.userTable に対応するモデルのストアを返す。
431
+ * ユーザーテーブルが見つからない場合は null を返す。
432
+ */
433
+ private resolveUserStore(): MockDocument[] | null {
434
+ const userTable = this.options.authConfig?.customJwt?.userTable;
435
+ if (!userTable) return null;
436
+
437
+ for (const model of this.options.connectorModels) {
438
+ const cfg = model.connectorConfig;
439
+ if (cfg && "table" in cfg && cfg.table === userTable) {
440
+ return this.stores.get(toCamelCase(model.name)) || null;
441
+ }
442
+ }
443
+ return null;
444
+ }
445
+
446
+ /**
447
+ * JWT を検証し、ペイロード(roles 含む)を返す。
448
+ * 認証不要な場合は null を返す。401/403 の場合はレスポンスを送信して 'error' を返す。
449
+ */
450
+ private checkAuth(
451
+ req: http.IncomingMessage,
452
+ res: http.ServerResponse,
453
+ model: ModelInfo,
454
+ requiredRoles: string[] | undefined
455
+ ): Record<string, unknown> | null | "error" {
456
+ const authCfg = this.options.authConfig;
457
+ if (!authCfg) return null; // auth 未設定 → 全スルー
458
+
459
+ const policy = model.authPolicy;
460
+ const defaultPolicy = authCfg.defaultPolicy || "anonymous";
461
+
462
+ // モデルに authPolicy がなく defaultPolicy が anonymous → 認証不要
463
+ if (!policy && defaultPolicy === "anonymous") return null;
464
+
465
+ // JWT 検証
466
+ const authHeader = req.headers["authorization"];
467
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
468
+ this.sendJson(res, 401, { error: "Missing or invalid Authorization header" });
469
+ return "error";
470
+ }
471
+
472
+ const token = authHeader.slice(7);
473
+ try {
474
+ const payload = jwt.verify(token, authCfg.jwtSecret) as Record<string, unknown>;
475
+
476
+ // ロールチェック
477
+ if (requiredRoles && requiredRoles.length > 0) {
478
+ const userRoles = Array.isArray(payload.roles) ? (payload.roles as string[]) : [];
479
+ const hasRole = requiredRoles.some((r) => userRoles.includes(r));
480
+ if (!hasRole) {
481
+ this.sendJson(res, 403, {
482
+ error: `Requires one of roles: ${requiredRoles.join(", ")}`,
483
+ });
484
+ return "error";
485
+ }
486
+ }
487
+
488
+ return payload;
489
+ } catch {
490
+ this.sendJson(res, 401, { error: "Invalid or expired token" });
491
+ return "error";
492
+ }
493
+ }
494
+
495
+ /**
496
+ * モデルの authPolicy から read/write に必要なロールを解決
497
+ */
498
+ private resolveRequiredRoles(
499
+ model: ModelInfo,
500
+ isWrite: boolean
501
+ ): string[] | undefined {
502
+ const policy = model.authPolicy;
503
+ if (!policy) return undefined; // defaultPolicy: authenticated → 認証のみ、ロールチェック不要
504
+
505
+ if (isWrite) {
506
+ return policy.write || policy.roles;
507
+ }
508
+ return policy.read || policy.roles;
509
+ }
510
+
511
+ private readBody(req: http.IncomingMessage, callback: (body: MockDocument) => void) {
512
+ let data = "";
513
+ req.on("data", (chunk) => (data += chunk));
514
+ req.on("end", () => {
515
+ try {
516
+ callback(JSON.parse(data));
517
+ } catch {
518
+ callback({});
519
+ }
520
+ });
521
+ }
522
+
523
+ private sendJson(res: http.ServerResponse, status: number, body: unknown) {
524
+ res.writeHead(status, {
525
+ "Content-Type": "application/json",
526
+ "Access-Control-Allow-Origin": "*",
527
+ "Connection": "close",
528
+ });
529
+ if (body !== null && body !== undefined) {
530
+ res.end(JSON.stringify(body));
531
+ } else {
532
+ res.end();
533
+ }
534
+ }
535
+
536
+ /**
537
+ * リクエストボディを完全に読み捨ててからレスポンスを送信
538
+ * POST/PUT の 405 等で未読ボディがある場合に Linux で RST を防ぐ
539
+ */
540
+ private drainAndRespond(
541
+ req: http.IncomingMessage,
542
+ res: http.ServerResponse,
543
+ status: number,
544
+ body: unknown
545
+ ) {
546
+ if (req.readableEnded) {
547
+ this.sendJson(res, status, body);
548
+ return;
549
+ }
550
+ req.resume();
551
+ req.on("end", () => {
552
+ this.sendJson(res, status, body);
553
+ });
554
+ }
555
+ }