tina4-nodejs 3.10.90 → 3.10.92
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/package.json +1 -1
- package/packages/core/src/ai.ts +1 -1
- package/packages/core/src/api.ts +6 -6
- package/packages/core/src/auth.ts +28 -15
- package/packages/core/src/cache.ts +9 -0
- package/packages/core/src/devAdmin.ts +85 -7
- package/packages/core/src/devMailbox.ts +21 -21
- package/packages/core/src/fakeData.ts +24 -14
- package/packages/core/src/graphql.ts +37 -1
- package/packages/core/src/i18n.ts +1 -1
- package/packages/core/src/index.ts +6 -6
- package/packages/core/src/mcp.ts +3 -0
- package/packages/core/src/messenger.ts +52 -4
- package/packages/core/src/middleware.ts +61 -0
- package/packages/core/src/queue.ts +103 -30
- package/packages/core/src/queueBackends/liteBackend.ts +43 -0
- package/packages/core/src/rateLimiter.ts +88 -1
- package/packages/core/src/request.ts +24 -1
- package/packages/core/src/response.ts +54 -10
- package/packages/core/src/router.ts +32 -14
- package/packages/core/src/scss.ts +44 -2
- package/packages/core/src/server.ts +26 -4
- package/packages/core/src/service.ts +7 -0
- package/packages/core/src/session.ts +4 -4
- package/packages/core/src/testClient.ts +2 -2
- package/packages/core/src/testing.ts +6 -6
- package/packages/core/src/types.ts +8 -1
- package/packages/core/src/watcher.ts +66 -0
- package/packages/core/src/websocket.ts +24 -3
- package/packages/core/src/websocketConnection.ts +4 -0
- package/packages/core/src/wsdl.ts +12 -12
- package/packages/frond/src/engine.ts +6 -0
- package/packages/orm/src/adapters/firebird.ts +2 -2
- package/packages/orm/src/adapters/mssql.ts +2 -2
- package/packages/orm/src/adapters/mysql.ts +2 -2
- package/packages/orm/src/adapters/postgres.ts +2 -2
- package/packages/orm/src/adapters/sqlite.ts +3 -3
- package/packages/orm/src/autoCrud.ts +117 -74
- package/packages/orm/src/baseModel.ts +44 -7
- package/packages/orm/src/database.ts +58 -15
- package/packages/orm/src/databaseResult.ts +5 -0
- package/packages/orm/src/fakeData.ts +1 -11
- package/packages/orm/src/index.ts +1 -1
- package/packages/orm/src/migration.ts +78 -5
- package/packages/orm/src/queryBuilder.ts +2 -2
- package/packages/orm/src/sqlTranslation.ts +20 -3
- package/packages/orm/src/types.ts +2 -2
- package/packages/swagger/src/generator.ts +2 -2
- package/packages/swagger/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.92",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
package/packages/core/src/ai.ts
CHANGED
|
@@ -608,7 +608,7 @@ npx tina4nodejs test # Run all tests
|
|
|
608
608
|
|
|
609
609
|
Add test files in \`test/\` directory. Use built-in inline testing:
|
|
610
610
|
\`\`\`typescript
|
|
611
|
-
import { tests, assertEqual,
|
|
611
|
+
import { tests, assertEqual, runAll } from "@tina4/core";
|
|
612
612
|
\`\`\`
|
|
613
613
|
|
|
614
614
|
## Important
|
package/packages/core/src/api.ts
CHANGED
|
@@ -36,7 +36,7 @@ export class Api {
|
|
|
36
36
|
/**
|
|
37
37
|
* Add custom headers to all subsequent requests.
|
|
38
38
|
*/
|
|
39
|
-
|
|
39
|
+
addHeaders(headers: Record<string, string>): void {
|
|
40
40
|
Object.assign(this.headers, headers);
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -78,36 +78,36 @@ export class Api {
|
|
|
78
78
|
* HTTP POST request.
|
|
79
79
|
*/
|
|
80
80
|
async post(path: string, body?: unknown, contentType: string = "application/json"): Promise<ApiResult> {
|
|
81
|
-
return this.sendRequest(
|
|
81
|
+
return this.sendRequest("POST", path, body, contentType);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* HTTP PUT request.
|
|
86
86
|
*/
|
|
87
87
|
async put(path: string, body?: unknown, contentType: string = "application/json"): Promise<ApiResult> {
|
|
88
|
-
return this.sendRequest(
|
|
88
|
+
return this.sendRequest("PUT", path, body, contentType);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* HTTP PATCH request.
|
|
93
93
|
*/
|
|
94
94
|
async patch(path: string, body?: unknown, contentType: string = "application/json"): Promise<ApiResult> {
|
|
95
|
-
return this.sendRequest(
|
|
95
|
+
return this.sendRequest("PATCH", path, body, contentType);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* HTTP DELETE request.
|
|
100
100
|
*/
|
|
101
101
|
async delete(path: string, body?: unknown): Promise<ApiResult> {
|
|
102
|
-
return this.sendRequest(
|
|
102
|
+
return this.sendRequest("DELETE", path, body);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Generic request method — public entry point for any HTTP method.
|
|
107
107
|
*/
|
|
108
108
|
async sendRequest(
|
|
109
|
-
path: string,
|
|
110
109
|
method: string,
|
|
110
|
+
path: string,
|
|
111
111
|
body?: unknown,
|
|
112
112
|
contentType: string = "application/json",
|
|
113
113
|
): Promise<ApiResult> {
|
|
@@ -35,32 +35,45 @@ function base64urlDecode(str: string): Buffer {
|
|
|
35
35
|
* Secret is always read from `process.env.SECRET`.
|
|
36
36
|
* Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256").
|
|
37
37
|
*
|
|
38
|
-
* @param payload
|
|
39
|
-
* @param
|
|
38
|
+
* @param payload - Claims to encode (e.g. `{ userId: 1, role: "admin" }`)
|
|
39
|
+
* @param secretOrExpiresIn - Signing secret string, OR expiresIn number (back-compat with old 2-arg form)
|
|
40
|
+
* @param expiresIn - Lifetime in seconds (default 3600). Only used when secret is a string.
|
|
40
41
|
* @returns Signed JWT string: header.payload.signature
|
|
41
42
|
*/
|
|
42
43
|
export function getToken(
|
|
43
44
|
payload: Record<string, unknown>,
|
|
45
|
+
secretOrExpiresIn?: string | number,
|
|
44
46
|
expiresIn: number = 3600,
|
|
47
|
+
algorithm?: string,
|
|
45
48
|
): string {
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
// Back-compat: if second arg is a number, treat it as expiresIn (old 2-arg form)
|
|
50
|
+
let resolvedSecret: string;
|
|
51
|
+
let resolvedExpiresIn: number;
|
|
52
|
+
if (typeof secretOrExpiresIn === "number") {
|
|
53
|
+
resolvedSecret = process.env.SECRET ?? "";
|
|
54
|
+
resolvedExpiresIn = secretOrExpiresIn;
|
|
55
|
+
} else {
|
|
56
|
+
resolvedSecret = secretOrExpiresIn ?? process.env.SECRET ?? "";
|
|
57
|
+
resolvedExpiresIn = expiresIn;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!resolvedSecret) {
|
|
48
61
|
console.warn("Auth: SECRET not set in .env — using blank secret (insecure)");
|
|
49
62
|
}
|
|
50
|
-
const
|
|
63
|
+
const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
|
|
51
64
|
|
|
52
|
-
const header = { alg:
|
|
65
|
+
const header = { alg: resolvedAlgorithm, typ: "JWT" };
|
|
53
66
|
const now = Math.floor(Date.now() / 1000);
|
|
54
67
|
|
|
55
68
|
const claims: Record<string, unknown> = { ...payload, iat: now };
|
|
56
|
-
if (
|
|
57
|
-
claims.exp = now +
|
|
69
|
+
if (resolvedExpiresIn !== 0) {
|
|
70
|
+
claims.exp = now + resolvedExpiresIn;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
const h = base64urlEncode(Buffer.from(JSON.stringify(header)));
|
|
61
74
|
const p = base64urlEncode(Buffer.from(JSON.stringify(claims)));
|
|
62
75
|
const signingInput = `${h}.${p}`;
|
|
63
|
-
const signature = sign(signingInput,
|
|
76
|
+
const signature = sign(signingInput, resolvedSecret, resolvedAlgorithm);
|
|
64
77
|
|
|
65
78
|
return `${h}.${p}.${signature}`;
|
|
66
79
|
}
|
|
@@ -71,12 +84,12 @@ export function getToken(
|
|
|
71
84
|
* Secret is always read from `process.env.SECRET`.
|
|
72
85
|
* Algorithm is read from `process.env.TINA4_JWT_ALGORITHM` (default "HS256").
|
|
73
86
|
*/
|
|
74
|
-
export function validToken(token: string): boolean {
|
|
75
|
-
const
|
|
76
|
-
if (!
|
|
87
|
+
export function validToken(token: string, secret?: string, algorithm?: string): boolean {
|
|
88
|
+
const resolvedSecret = secret ?? process.env.SECRET ?? "";
|
|
89
|
+
if (!resolvedSecret) {
|
|
77
90
|
console.warn("Auth: SECRET not set in .env — using blank secret (insecure)");
|
|
78
91
|
}
|
|
79
|
-
const
|
|
92
|
+
const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
|
|
80
93
|
try {
|
|
81
94
|
const parts = token.split(".");
|
|
82
95
|
if (parts.length !== 3) return false;
|
|
@@ -84,7 +97,7 @@ export function validToken(token: string): boolean {
|
|
|
84
97
|
const [h, p, sig] = parts;
|
|
85
98
|
const signingInput = `${h}.${p}`;
|
|
86
99
|
|
|
87
|
-
if (!verifySignature(signingInput, sig,
|
|
100
|
+
if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
|
|
88
101
|
return false;
|
|
89
102
|
}
|
|
90
103
|
|
|
@@ -211,7 +224,7 @@ export function authMiddleware(secret?: string, algorithm: string = "HS256"): Mi
|
|
|
211
224
|
}
|
|
212
225
|
|
|
213
226
|
const token = authHeader.slice(7);
|
|
214
|
-
if (!validToken(token)) {
|
|
227
|
+
if (!validToken(token, secret, algorithm)) {
|
|
215
228
|
res({ error: "Unauthorized" }, 401);
|
|
216
229
|
return;
|
|
217
230
|
}
|
|
@@ -501,6 +501,15 @@ export function cacheClear(): void {
|
|
|
501
501
|
_getBackend().clear();
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
/** Remove expired entries from the cache. Returns count removed. */
|
|
505
|
+
export function sweep(): number {
|
|
506
|
+
const backend = _getBackend();
|
|
507
|
+
if (typeof (backend as any).sweep === "function") {
|
|
508
|
+
return (backend as any).sweep();
|
|
509
|
+
}
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
504
513
|
/** Return cache statistics from the active backend. */
|
|
505
514
|
export function cacheBackendStats(): { hits: number; misses: number; size: number; backend: string } {
|
|
506
515
|
return _getBackend().stats();
|
|
@@ -191,15 +191,40 @@ export class RequestInspector {
|
|
|
191
191
|
|
|
192
192
|
export class ErrorTracker {
|
|
193
193
|
private static errors: ErrorEntry[] = [];
|
|
194
|
+
private static maxErrors = 200;
|
|
195
|
+
private static registered = false;
|
|
194
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Capture an error with dedup (matches PHP/Ruby/Python signature).
|
|
199
|
+
* Duplicate errors (same message) increment count and update last_seen.
|
|
200
|
+
*/
|
|
201
|
+
static capture(errorType: string, message: string, traceback = "", file = "", line = 0): void {
|
|
202
|
+
const fingerprint = `${errorType}|${message}|${file}|${line}`;
|
|
203
|
+
const existing = this.errors.find((e) => (e as any).fingerprint === fingerprint);
|
|
204
|
+
const now = new Date().toISOString();
|
|
205
|
+
|
|
206
|
+
if (existing) {
|
|
207
|
+
(existing as any).count = ((existing as any).count || 1) + 1;
|
|
208
|
+
(existing as any).last_seen = now;
|
|
209
|
+
existing.resolved = false; // re-open resolved duplicates
|
|
210
|
+
} else {
|
|
211
|
+
this.errors.push({
|
|
212
|
+
id: `err_${Date.now()}_${this.errors.length}`,
|
|
213
|
+
timestamp: now,
|
|
214
|
+
message,
|
|
215
|
+
stack: traceback || undefined,
|
|
216
|
+
resolved: false,
|
|
217
|
+
...({ fingerprint, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now } as any),
|
|
218
|
+
});
|
|
219
|
+
if (this.errors.length > this.maxErrors) {
|
|
220
|
+
this.errors = this.errors.slice(-this.maxErrors);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Legacy alias for capture (backward compatibility). */
|
|
195
226
|
static track(message: string, stack?: string): void {
|
|
196
|
-
this.
|
|
197
|
-
id: `err_${Date.now()}_${this.errors.length}`,
|
|
198
|
-
timestamp: new Date().toISOString(),
|
|
199
|
-
message,
|
|
200
|
-
stack,
|
|
201
|
-
resolved: false,
|
|
202
|
-
});
|
|
227
|
+
this.capture("Error", message, stack || "");
|
|
203
228
|
}
|
|
204
229
|
|
|
205
230
|
static get(): ErrorEntry[] {
|
|
@@ -218,6 +243,56 @@ export class ErrorTracker {
|
|
|
218
243
|
static clearResolved(): void {
|
|
219
244
|
this.errors = this.errors.filter((e) => !e.resolved);
|
|
220
245
|
}
|
|
246
|
+
|
|
247
|
+
/** Remove ALL tracked errors. */
|
|
248
|
+
static clearAll(): void {
|
|
249
|
+
this.errors = [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Health summary — are there unresolved errors? */
|
|
253
|
+
static health(): { healthy: boolean; total: number; unresolved: number; resolved: number } {
|
|
254
|
+
const total = this.errors.length;
|
|
255
|
+
const resolved = this.errors.filter((e) => e.resolved).length;
|
|
256
|
+
const unresolved = total - resolved;
|
|
257
|
+
return { healthy: unresolved === 0, total, unresolved, resolved };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Count of unresolved errors. */
|
|
261
|
+
static unresolvedCount(): number {
|
|
262
|
+
return this.errors.filter((e) => !e.resolved).length;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Reset all state (for testing). */
|
|
266
|
+
static reset(): void {
|
|
267
|
+
this.errors = [];
|
|
268
|
+
this.registered = false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Register global error handlers to feed the tracker.
|
|
273
|
+
* Safe to call multiple times — only registers once.
|
|
274
|
+
*/
|
|
275
|
+
static register(): void {
|
|
276
|
+
if (this.registered) return;
|
|
277
|
+
this.registered = true;
|
|
278
|
+
|
|
279
|
+
process.on("uncaughtException", (err: Error) => {
|
|
280
|
+
this.capture(
|
|
281
|
+
err.constructor.name,
|
|
282
|
+
err.message,
|
|
283
|
+
err.stack || "",
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
process.on("unhandledRejection", (reason: unknown) => {
|
|
288
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
289
|
+
this.capture(
|
|
290
|
+
err.constructor.name,
|
|
291
|
+
err.message,
|
|
292
|
+
err.stack || "",
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
221
296
|
}
|
|
222
297
|
|
|
223
298
|
// ---------------------------------------------------------------------------
|
|
@@ -355,6 +430,9 @@ export class DevAdmin {
|
|
|
355
430
|
* Register all /__dev routes on the given router.
|
|
356
431
|
*/
|
|
357
432
|
static register(router: Router): void {
|
|
433
|
+
// Register error handlers to feed the ErrorTracker
|
|
434
|
+
ErrorTracker.register();
|
|
435
|
+
|
|
358
436
|
const routes: Array<{ method: string; pattern: string; handler: RouteHandler }> = [
|
|
359
437
|
// Dashboard
|
|
360
438
|
{ method: "GET", pattern: "/__dev", handler: handleDashboard },
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* import { DevMailbox, createMessenger } from "@tina4/core";
|
|
8
8
|
*
|
|
9
9
|
* const mailbox = new DevMailbox();
|
|
10
|
-
* mailbox.capture(
|
|
10
|
+
* mailbox.capture("alice@test.com", "Hello", "Hi!");
|
|
11
11
|
* const messages = mailbox.inbox();
|
|
12
12
|
*/
|
|
13
13
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
@@ -39,33 +39,33 @@ export class DevMailbox {
|
|
|
39
39
|
/**
|
|
40
40
|
* Capture an email to the dev mailbox instead of sending it.
|
|
41
41
|
*/
|
|
42
|
-
capture(
|
|
43
|
-
to: string | string[]
|
|
44
|
-
subject: string
|
|
45
|
-
body: string
|
|
46
|
-
html
|
|
47
|
-
cc
|
|
48
|
-
bcc
|
|
49
|
-
replyTo?: string
|
|
50
|
-
attachments
|
|
51
|
-
from?: string
|
|
52
|
-
|
|
42
|
+
capture(
|
|
43
|
+
to: string | string[],
|
|
44
|
+
subject: string,
|
|
45
|
+
body: string,
|
|
46
|
+
html: boolean = false,
|
|
47
|
+
cc: string[] = [],
|
|
48
|
+
bcc: string[] = [],
|
|
49
|
+
replyTo?: string,
|
|
50
|
+
attachments: string[] = [],
|
|
51
|
+
from?: string,
|
|
52
|
+
): SendResult {
|
|
53
53
|
const id = randomUUID();
|
|
54
|
-
const toList = Array.isArray(
|
|
54
|
+
const toList = Array.isArray(to) ? to : [to];
|
|
55
55
|
const now = new Date().toISOString();
|
|
56
56
|
|
|
57
57
|
const message: EmailMessage = {
|
|
58
58
|
id,
|
|
59
59
|
type: "outbox",
|
|
60
|
-
from:
|
|
60
|
+
from: from ?? process.env.SMTP_FROM ?? "dev@localhost",
|
|
61
61
|
to: toList,
|
|
62
|
-
cc
|
|
63
|
-
bcc
|
|
64
|
-
reply_to:
|
|
65
|
-
subject
|
|
66
|
-
body
|
|
67
|
-
html
|
|
68
|
-
attachments
|
|
62
|
+
cc,
|
|
63
|
+
bcc,
|
|
64
|
+
reply_to: replyTo,
|
|
65
|
+
subject,
|
|
66
|
+
body,
|
|
67
|
+
html,
|
|
68
|
+
attachments,
|
|
69
69
|
date: now,
|
|
70
70
|
read: false,
|
|
71
71
|
};
|
|
@@ -68,12 +68,6 @@ const JOB_TITLES = [
|
|
|
68
68
|
"Systems Administrator",
|
|
69
69
|
];
|
|
70
70
|
|
|
71
|
-
const COLORS = [
|
|
72
|
-
"red", "blue", "green", "yellow", "purple", "orange", "pink",
|
|
73
|
-
"cyan", "magenta", "teal", "indigo", "violet", "coral", "salmon",
|
|
74
|
-
"turquoise", "maroon", "navy", "olive", "silver", "gold",
|
|
75
|
-
];
|
|
76
|
-
|
|
77
71
|
const CURRENCIES = [
|
|
78
72
|
"USD", "EUR", "GBP", "JPY", "AUD", "CAD", "CHF", "CNY",
|
|
79
73
|
"SEK", "NZD", "MXN", "SGD", "HKD", "NOK", "ZAR", "INR",
|
|
@@ -137,7 +131,7 @@ export class FakeData {
|
|
|
137
131
|
return this.pick(LAST_NAMES);
|
|
138
132
|
}
|
|
139
133
|
|
|
140
|
-
|
|
134
|
+
name(): string {
|
|
141
135
|
return `${this.firstName()} ${this.lastName()}`;
|
|
142
136
|
}
|
|
143
137
|
|
|
@@ -209,7 +203,7 @@ export class FakeData {
|
|
|
209
203
|
return this.randInt(min, max + 1);
|
|
210
204
|
}
|
|
211
205
|
|
|
212
|
-
|
|
206
|
+
numeric(min = 0, max = 1000, decimals = 2): number {
|
|
213
207
|
const raw = min + this.rng() * (max - min);
|
|
214
208
|
return Number(raw.toFixed(decimals));
|
|
215
209
|
}
|
|
@@ -251,11 +245,7 @@ export class FakeData {
|
|
|
251
245
|
return `${this.randInt(1, 256)}.${this.randInt(0, 256)}.${this.randInt(0, 256)}.${this.randInt(1, 256)}`;
|
|
252
246
|
}
|
|
253
247
|
|
|
254
|
-
|
|
255
|
-
return this.pick(COLORS);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
hexColor(): string {
|
|
248
|
+
colorHex(): string {
|
|
259
249
|
const hex = this.randInt(0, 0x1000000).toString(16).padStart(6, "0");
|
|
260
250
|
return `#${hex}`;
|
|
261
251
|
}
|
|
@@ -270,11 +260,31 @@ export class FakeData {
|
|
|
270
260
|
return this.pick(CURRENCIES);
|
|
271
261
|
}
|
|
272
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Returns multi-paragraph text.
|
|
265
|
+
* Matches Python's text() method.
|
|
266
|
+
*/
|
|
267
|
+
text(paragraphs = 3): string {
|
|
268
|
+
const parts: string[] = [];
|
|
269
|
+
for (let i = 0; i < paragraphs; i++) {
|
|
270
|
+
parts.push(this.paragraph(4));
|
|
271
|
+
}
|
|
272
|
+
return parts.join("\n\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Returns a random element from the given array.
|
|
277
|
+
* Matches Python's choice() method.
|
|
278
|
+
*/
|
|
279
|
+
choice<T>(items: T[]): T {
|
|
280
|
+
return items[this.randInt(0, items.length)];
|
|
281
|
+
}
|
|
282
|
+
|
|
273
283
|
/**
|
|
274
284
|
* Run seed files from a directory. Each file should export a default async function.
|
|
275
285
|
* Returns an array of executed file paths.
|
|
276
286
|
*/
|
|
277
|
-
async
|
|
287
|
+
async seedDir(seedDir?: string): Promise<string[]> {
|
|
278
288
|
const dir = resolve(seedDir ?? "src/seeds");
|
|
279
289
|
if (!existsSync(dir)) return [];
|
|
280
290
|
const files = readdirSync(dir)
|
|
@@ -69,7 +69,7 @@ const TOKEN_PATTERNS: Array<[string, RegExp]> = [
|
|
|
69
69
|
["COMMENT", /#[^\n]*/y],
|
|
70
70
|
];
|
|
71
71
|
|
|
72
|
-
function tokenize(source: string): Token[] {
|
|
72
|
+
export function tokenize(source: string): Token[] {
|
|
73
73
|
const tokens: Token[] = [];
|
|
74
74
|
let pos = 0;
|
|
75
75
|
|
|
@@ -932,3 +932,39 @@ export class GraphQL {
|
|
|
932
932
|
return resolved;
|
|
933
933
|
}
|
|
934
934
|
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Lightweight GraphQL type wrapper matching Ruby's GraphQLType.
|
|
938
|
+
*/
|
|
939
|
+
export class GraphQLType {
|
|
940
|
+
static readonly SCALARS = ["String", "Int", "Float", "Boolean", "ID"];
|
|
941
|
+
|
|
942
|
+
name: string;
|
|
943
|
+
kind: string;
|
|
944
|
+
ofType: GraphQLType | null;
|
|
945
|
+
|
|
946
|
+
constructor(name: string, kind: string = "object", ofType: GraphQLType | null = null) {
|
|
947
|
+
this.name = name;
|
|
948
|
+
this.kind = kind;
|
|
949
|
+
this.ofType = ofType;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Parse a GraphQL type string like "String", "String!", "[Int!]!".
|
|
954
|
+
*/
|
|
955
|
+
static parse(typeStr: string): GraphQLType {
|
|
956
|
+
const s = String(typeStr).trim();
|
|
957
|
+
if (s.endsWith("!")) {
|
|
958
|
+
const inner = GraphQLType.parse(s.slice(0, -1));
|
|
959
|
+
return new GraphQLType(s, "non_null", inner);
|
|
960
|
+
}
|
|
961
|
+
if (s.startsWith("[") && s.endsWith("]")) {
|
|
962
|
+
const inner = GraphQLType.parse(s.slice(1, -1));
|
|
963
|
+
return new GraphQLType(s, "list", inner);
|
|
964
|
+
}
|
|
965
|
+
if (GraphQLType.SCALARS.includes(s)) {
|
|
966
|
+
return new GraphQLType(s, "scalar");
|
|
967
|
+
}
|
|
968
|
+
return new GraphQLType(s, "object");
|
|
969
|
+
}
|
|
970
|
+
}
|
|
@@ -12,15 +12,15 @@ export type {
|
|
|
12
12
|
WebSocketRouteDefinition,
|
|
13
13
|
} from "./types.js";
|
|
14
14
|
|
|
15
|
-
export { startServer, resolvePortAndHost, handle } from "./server.js";
|
|
15
|
+
export { startServer, resolvePortAndHost, handle, start, stop } from "./server.js";
|
|
16
16
|
export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares } from "./router.js";
|
|
17
17
|
export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
|
|
18
18
|
export type { RouteInfo } from "./router.js";
|
|
19
19
|
export { discoverRoutes } from "./routeDiscovery.js";
|
|
20
20
|
export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware, CsrfMiddleware } from "./middleware.js";
|
|
21
21
|
export type { CorsConfig } from "./middleware.js";
|
|
22
|
-
export { createRequest
|
|
23
|
-
export { createResponse, errorResponse, setDefaultTemplatesDir } from "./response.js";
|
|
22
|
+
export { createRequest } from "./request.js";
|
|
23
|
+
export { createResponse, errorResponse, setDefaultTemplatesDir, getFrond, setFrond, getFrameworkFrond } from "./response.js";
|
|
24
24
|
export { tryServeStatic } from "./static.js";
|
|
25
25
|
export { loadEnv, getEnv, requireEnv, hasEnv, allEnv, resetEnv, isTruthy } from "./dotenv.js";
|
|
26
26
|
export { Log } from "./logger.js";
|
|
@@ -74,8 +74,8 @@ export { DevAdmin, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore,
|
|
|
74
74
|
export { Messenger } from "./messenger.js";
|
|
75
75
|
export type { SendResult, EmailMessage } from "./messenger.js";
|
|
76
76
|
export { DevMailbox, createMessenger } from "./devMailbox.js";
|
|
77
|
-
export { WSDLService,
|
|
78
|
-
export type {
|
|
77
|
+
export { WSDLService, WSDLOperation } from "./wsdl.js";
|
|
78
|
+
export type { WSDLOperationMeta } from "./wsdl.js";
|
|
79
79
|
export { HtmlElement, htmlElement, addHtmlHelpers } from "./htmlElement.js";
|
|
80
80
|
export { renderErrorOverlay, renderProductionError, isDebugMode } from "./errorOverlay.js";
|
|
81
81
|
export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateContext } from "./ai.js";
|
|
@@ -96,7 +96,7 @@ export { ValkeySessionHandler } from "./sessionHandlers/valkeyHandler.js";
|
|
|
96
96
|
export type { ValkeySessionConfig } from "./sessionHandlers/valkeyHandler.js";
|
|
97
97
|
export { RedisNpmSessionHandler } from "./sessionHandlers/redisHandler.js";
|
|
98
98
|
export type { RedisNpmSessionConfig } from "./sessionHandlers/redisHandler.js";
|
|
99
|
-
export { tests, assertEqual,
|
|
99
|
+
export { tests, assertEqual, assertRaises, assertTrue, assertFalse, runAll, reset } from "./testing.js";
|
|
100
100
|
export { TestClient, TestResponse } from "./testClient.js";
|
|
101
101
|
export { Container, container } from "./container.js";
|
|
102
102
|
export { Validator } from "./validator.js";
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*
|
|
22
22
|
* const mail = new Messenger(); // reads from .env
|
|
23
23
|
* const mail = new Messenger({ host: "smtp.office365.com", port: 587 }); // override
|
|
24
|
-
* await mail.send(
|
|
24
|
+
* await mail.send("user@test.com", "Welcome", "<h1>Hello!</h1>", true, "Hello!");
|
|
25
25
|
*/
|
|
26
26
|
import net from "node:net";
|
|
27
27
|
import tls from "node:tls";
|
|
@@ -355,7 +355,19 @@ export class Messenger {
|
|
|
355
355
|
/**
|
|
356
356
|
* Send an email via SMTP.
|
|
357
357
|
*/
|
|
358
|
-
async send(
|
|
358
|
+
async send(
|
|
359
|
+
to: string | string[],
|
|
360
|
+
subject: string,
|
|
361
|
+
body: string,
|
|
362
|
+
html: boolean = false,
|
|
363
|
+
text?: string,
|
|
364
|
+
cc?: string | string[],
|
|
365
|
+
bcc?: string | string[],
|
|
366
|
+
replyTo?: string,
|
|
367
|
+
attachments?: string[],
|
|
368
|
+
headers?: Record<string, string>,
|
|
369
|
+
): Promise<SendResult> {
|
|
370
|
+
const options: SendOptions = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
|
|
359
371
|
const toList = Array.isArray(options.to) ? options.to : [options.to];
|
|
360
372
|
const ccList = Array.isArray(options.cc) ? options.cc : (options.cc ? [options.cc] : []);
|
|
361
373
|
const bccList = Array.isArray(options.bcc) ? options.bcc : (options.bcc ? [options.bcc] : []);
|
|
@@ -655,7 +667,24 @@ export class Messenger {
|
|
|
655
667
|
/**
|
|
656
668
|
* Search messages using IMAP search criteria.
|
|
657
669
|
*/
|
|
658
|
-
async search(
|
|
670
|
+
async search(
|
|
671
|
+
folder: string = "INBOX",
|
|
672
|
+
subject?: string,
|
|
673
|
+
sender?: string,
|
|
674
|
+
since?: string,
|
|
675
|
+
before?: string,
|
|
676
|
+
unseenOnly: boolean = false,
|
|
677
|
+
limit: number = 50,
|
|
678
|
+
): Promise<ImapMessage[]> {
|
|
679
|
+
// Build IMAP SEARCH criteria from structured params
|
|
680
|
+
const criteria: string[] = ["ALL"];
|
|
681
|
+
if (subject) criteria.push(`SUBJECT "${subject}"`);
|
|
682
|
+
if (sender) criteria.push(`FROM "${sender}"`);
|
|
683
|
+
if (since) criteria.push(`SINCE ${since}`);
|
|
684
|
+
if (before) criteria.push(`BEFORE ${before}`);
|
|
685
|
+
if (unseenOnly) criteria.push("UNSEEN");
|
|
686
|
+
|
|
687
|
+
const query = criteria.join(" ");
|
|
659
688
|
const socket = await this.imapConnect();
|
|
660
689
|
try {
|
|
661
690
|
await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
|
|
@@ -665,7 +694,7 @@ export class Messenger {
|
|
|
665
694
|
|
|
666
695
|
uids.reverse();
|
|
667
696
|
const messages: ImapMessage[] = [];
|
|
668
|
-
for (const uid of uids.slice(0,
|
|
697
|
+
for (const uid of uids.slice(0, limit)) {
|
|
669
698
|
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
|
|
670
699
|
messages.push(parseHeaderResponse(uid, fetchResp));
|
|
671
700
|
}
|
|
@@ -716,6 +745,25 @@ export class Messenger {
|
|
|
716
745
|
}
|
|
717
746
|
}
|
|
718
747
|
|
|
748
|
+
/**
|
|
749
|
+
* List available IMAP folders/mailboxes.
|
|
750
|
+
*/
|
|
751
|
+
async folders(): Promise<string[]> {
|
|
752
|
+
const socket = await this.imapConnect();
|
|
753
|
+
try {
|
|
754
|
+
const resp = await imapCommand(socket, 'LIST "" "*"');
|
|
755
|
+
const result: string[] = [];
|
|
756
|
+
for (const line of resp.split("\r\n")) {
|
|
757
|
+
// Parse LIST response: * LIST (\flags) "/" "FolderName"
|
|
758
|
+
const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
|
|
759
|
+
if (m) result.push(m[1]);
|
|
760
|
+
}
|
|
761
|
+
return result;
|
|
762
|
+
} finally {
|
|
763
|
+
await this.imapDisconnect(socket);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
719
767
|
/**
|
|
720
768
|
* Test IMAP connectivity without reading.
|
|
721
769
|
*/
|