tina4-nodejs 3.13.38 → 3.13.40
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/CLAUDE.md +54 -5
- package/README.md +6 -6
- package/package.json +1 -1
- package/packages/core/src/api.ts +64 -1
- package/packages/core/src/auth.ts +4 -1
- package/packages/core/src/devAdmin.ts +91 -21
- package/packages/core/src/index.ts +9 -4
- package/packages/core/src/logger.ts +84 -27
- package/packages/core/src/mcp.ts +105 -12
- package/packages/core/src/metrics.ts +330 -70
- package/packages/core/src/middleware.ts +1 -1
- package/packages/core/src/queueBackends/kafkaBackend.ts +97 -0
- package/packages/core/src/router.ts +54 -6
- package/packages/core/src/server.ts +120 -22
- package/packages/core/src/sessionHandlers/mongoHandler.ts +2 -0
- package/packages/core/src/types.ts +21 -2
- package/packages/core/src/websocket.ts +419 -9
- package/packages/core/src/websocketConnection.ts +6 -0
- package/packages/orm/src/baseModel.ts +167 -22
- package/packages/orm/src/docstore.ts +819 -0
- package/packages/orm/src/index.ts +14 -0
- package/packages/orm/src/migration.ts +149 -22
- package/packages/orm/src/queryBuilder.ts +14 -2
- package/packages/orm/src/types.ts +7 -0
- package/packages/orm/src/validation.ts +14 -0
- package/packages/swagger/src/generator.ts +119 -16
- package/packages/swagger/src/ui.ts +10 -2
package/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.40)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
@@ -117,14 +117,18 @@ The HTTP foundation. Handles request/response lifecycle, route matching, middlew
|
|
|
117
117
|
- `fakeData.ts` — Core fake data generator (names, emails, addresses, UUIDs, etc.)
|
|
118
118
|
- `constants.ts` — HTTP status codes (`HTTP_OK`, `HTTP_NOT_FOUND`, etc.) and content types (`APPLICATION_JSON`, `TEXT_HTML`, etc.)
|
|
119
119
|
- `devAdmin.ts` — Dev toolbar (fixed bottom bar injected into HTML pages) and admin dashboard at `/_dev/`
|
|
120
|
+
- `mcp.ts` - Model Context Protocol server (mounted by `devAdmin.ts` at `/__dev/mcp`) for live AI access to project tools. **MCP environment (read by `mcp.ts` / `devAdmin.ts`):**
|
|
121
|
+
- `TINA4_MCP` / `TINA4_DEBUG` - capability gate (whether MCP is enabled at all). Explicit `TINA4_MCP` true/false wins on any host; else `TINA4_DEBUG=true` enables it.
|
|
122
|
+
- `TINA4_MCP_TOKEN` - bearer token authorising a REMOTE MCP request (fallback `TINA4_API_KEY`). Accepted as `Authorization: Bearer`, `X-MCP-Token`, or `X-Api-Key`. With no token configured a remote caller is always denied. Loopback callers never need it.
|
|
123
|
+
- `TINA4_MCP_REMOTE` - set `true` to allow non-loopback MCP callers at all (still requires a valid token).
|
|
120
124
|
- `auth.ts` — Authentication helpers
|
|
121
125
|
- `cache.ts` — In-memory caching
|
|
122
126
|
- `session.ts` — Session management with pluggable handlers. `TINA4_SESSION_SAMESITE` env var (default: Lax)
|
|
123
|
-
- `websocket.ts` — WebSocket support with backplane for scaling via Redis/NATS pub/sub (`TINA4_WS_BACKPLANE`, `TINA4_WS_BACKPLANE_URL`). The backplane is wired for real: `broadcast`/`broadcastToRoom`/`sendTo` deliver to LOCAL connections first (resiliently — a dead/slow client is pruned, never aborting the loop) then publish an envelope `{src,kind,exclude,room,path,text|b64}` to the shared channel `tina4:ws` (identical wire shape across all 4 frameworks). The subscribe callback relays directly on the event loop with an origin guard (drop our own `src` echo — no double-delivery) and never re-publishes (no cluster loop); binary messages ride as base64. Security/ops knobs (all opt-in, non-breaking): `TINA4_WS_ALLOWED_ORIGINS` (comma-separated origin allow-list enforced on upgrade — empty/unset = allow all), `TINA4_WS_IDLE_TIMEOUT` (seconds; 0/unset disables the idle-connection reaper), `TINA4_WS_MAX_BACKLOG` (bytes; a slow client whose socket write backlog exceeds this is dropped rather than buffered without bound). Rooms API: `wss.joinRoom(clientId, room)`, `wss.leaveRoom(clientId, room)`, `wss.broadcastToRoom(room, msg, excludeIds?)`, `wss.getRoomConnections(room)`, `wss.roomCount(room)`, `wss.getClientRooms(clientId)`. `originAllowed(headers)` is exported for upgrade-path checks.
|
|
127
|
+
- `websocket.ts` — WebSocket support with backplane for scaling via Redis/NATS pub/sub (`TINA4_WS_BACKPLANE`, `TINA4_WS_BACKPLANE_URL`). The backplane is wired for real: `broadcast`/`broadcastToRoom`/`sendTo` deliver to LOCAL connections first (resiliently — a dead/slow client is pruned, never aborting the loop) then publish an envelope `{src,kind,exclude,room,path,text|b64}` to the shared channel `tina4:ws` (identical wire shape across all 4 frameworks). The subscribe callback relays directly on the event loop with an origin guard (drop our own `src` echo — no double-delivery) and never re-publishes (no cluster loop); binary messages ride as base64. Security/ops knobs (all opt-in, non-breaking): `TINA4_WS_ALLOWED_ORIGINS` (comma-separated origin allow-list enforced on upgrade — empty/unset = allow all), `TINA4_WS_IDLE_TIMEOUT` (seconds; 0/unset disables the idle-connection reaper), `TINA4_WS_MAX_BACKLOG` (bytes; a slow client whose socket write backlog exceeds this is dropped rather than buffered without bound). Rooms API: `wss.joinRoom(clientId, room)`, `wss.leaveRoom(clientId, room)`, `wss.broadcastToRoom(room, msg, excludeIds?)`, `wss.getRoomConnections(room)`, `wss.roomCount(room)`, `wss.getClientRooms(clientId)`. `originAllowed(headers)` is exported for upgrade-path checks. **Per-route auth (v3.13.39, Python master b5976d4):** a WS route is PUBLIC by default (mirrors GET). Mark it secured imperatively — `Router.websocket(path, fn, { secured: true })` / chain `.secure()` on the returned `WsRouteRef` / `WebSocketServer.route(path, fn, { secured: true })` — OR decorator-style via a `_secured` flag on the handler (works in either declaration order). A secured route enforces a valid JWT on the upgrade, AFTER the origin allow-list and BEFORE accept: missing/invalid → reject with HTTP 401 (never accept); public routes always pass (non-breaking). Three token transports (`wsToken(headers, query, subprotocol)`): (1) `Authorization: Bearer <jwt>` header (server/CLI/mobile); (2) the `Sec-WebSocket-Protocol` subprotocol `"bearer, <jwt>"` (browsers — `new WebSocket()` can't set headers; the server echoes `bearer` back as the accepted subprotocol); (3) `?token=<jwt>` query param. Validated via the same `validToken` (Auth) the HTTP routes use. The verified payload is exposed as `connection.auth` (`null` on public routes). `wsAuthorized(route, headers, query, subprotocol) -> [payload, ok]` is the gate. **The integrated server (`server.ts`) now dispatches user WS routes:** its `upgrade` handler routes a non-`/__dev_reload` upgrade through `serveWebSocketRoute(req, socket, head)`, which matches the WS route table, enforces per-route auth, then drives the real open/message/close lifecycle on the connection (`wsRouteManager`) — parity with Python/PHP/Ruby (previously only `/__dev_reload` was wired, so user WS routes never reached a live connection).
|
|
124
128
|
- `queue.ts` — Queue system with pluggable backends
|
|
125
129
|
- `graphql.ts` — GraphQL engine. **Hardening:** selection-set nesting is bounded by `TINA4_GRAPHQL_MAX_DEPTH` (default `50`; `<= 0` disables; exposed as the public `gql.maxDepth` field + `graphqlMaxDepth()` helper). Depth increments on every recursive entry — sub-selections, fragment spreads, AND inline fragments — so an over-deep query or a circular fragment fails with `Query exceeds maximum depth of N` instead of overflowing the stack (top-level starts at depth 1). A resolver exception is logged via `Log.error` and the detail is surfaced to the client **only** under `TINA4_DEBUG` (`isDebugMode()`); otherwise it returns a generic `Internal server error` (path preserved) so internal state never leaks.
|
|
126
130
|
- `i18n.ts` — Internationalization / localization
|
|
127
|
-
- `logger.ts` — Structured logging
|
|
131
|
+
- `logger.ts` — Structured logging. Five first-class severity levels: `debug`(0) < `info`(1) < `warning`(2) < `error`(3) < `critical`(4). `critical` is the HIGHEST level, NOT a relabelled `error`, and renders magenta. `Log.critical()` ALWAYS emits like every other level — subject only to `TINA4_LOG_LEVEL` (which it always clears) and teed to the log file whenever a file is being written. There is NO enable toggle: the old `TINA4_LOG_CRITICAL` opt-in was retired in v3.13.39 (the env var is no longer read), so a critical log is never a silent no-op. `Log.isEnabled("critical")` is ordinary threshold logic (`4 >= configured min`). **Dev/prod-aware default file output (v3.13.39, Python master 4c6d881):** stdout is ALWAYS on. When `TINA4_LOG_OUTPUT` is unset (default), the log FILE (`logs/tina4.log`) is written ONLY in development (`TINA4_DEBUG` truthy); in production / containers (`TINA4_DEBUG` falsy) the logger is stdout-only — no file to bloat the writable layer / disk (12-factor: logs on stdout for the platform to capture). Explicit `TINA4_LOG_OUTPUT=file`/`both`, OR an explicit `TINA4_LOG_FILE` path, always forces a file (explicit wins). `readEnv()` resolves all of this into a single `fileEnabled` flag that gates the file writer. Full parity with Python master.
|
|
128
132
|
- `rateLimiter.ts` — Rate limiting middleware
|
|
129
133
|
- `dotenv.ts` — `.env` file loading
|
|
130
134
|
- `health.ts` — Health check endpoint
|
|
@@ -234,6 +238,30 @@ db.getError(): string | null
|
|
|
234
238
|
db.cacheStats(): { enabled, size, ttl }
|
|
235
239
|
```
|
|
236
240
|
|
|
241
|
+
### DocStore — pymongo-style document store (zero-config SQLite fallback)
|
|
242
|
+
|
|
243
|
+
`getCollection(name)` (from `@tina4/orm`) returns a Mongo-style collection. When a Mongo URI is configured it is a real Mongo collection (resolved lazily, returns a Promise); otherwise it is a `SqliteCollection` backed by a local SQLite file (`node:sqlite`, JSON1) and is synchronous. The call sites are identical either way — only the backend differs — so you develop against a zero-dependency local store and switch to MongoDB in production by setting one env var. Because `node:sqlite` is synchronous, `getCollection` is sync in the serverless path and returns a Promise only on the real-Mongo path.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { getCollection, isServerless, ObjectId } from "@tina4/orm";
|
|
247
|
+
|
|
248
|
+
const orders = getCollection("orders") as any; // SqliteCollection in serverless mode
|
|
249
|
+
const res = orders.insertOne({ customer_id: 1, total: 9.99, status: "new" });
|
|
250
|
+
orders.findOne({ _id: res.insertedId });
|
|
251
|
+
orders.updateOne({ _id: res.insertedId }, { $set: { status: "shipped" } });
|
|
252
|
+
for (const doc of orders.find({ total: { $gt: 5 } }).sort("total", -1).limit(10)) {
|
|
253
|
+
// ...
|
|
254
|
+
}
|
|
255
|
+
orders.countDocuments({ status: "shipped" });
|
|
256
|
+
isServerless(); // true when running on the SQLite fallback
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Filter operators: equality, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$exists`, `$regex`, implicit AND, `$or`, `$and`, and dotted nested keys (`addr.city`). Updates: `$set`, `$unset`, `$inc`, replace, upsert. Cursors: `sort`, `limit`, `skip`, projection. Values round-trip (Date to/from ISO-8601, `ObjectId` to/from 24-hex) and stay queryable via `json_extract`. Non-goals: aggregation pipelines, `$elemMatch`, geo queries.
|
|
260
|
+
|
|
261
|
+
Selection and configuration:
|
|
262
|
+
- `TINA4_MONGO_URI` — app-wide Mongo URI. Falls back to `TINA4_SESSION_MONGO_URI`, then the legacy `TINA4_SESSION_MONGO_URL`. When one is set, `getCollection` returns a real Mongo collection.
|
|
263
|
+
- `TINA4_DOC_STORE_PATH` — SQLite file for the fallback store (default `data/tina4_docstore.db`).
|
|
264
|
+
|
|
237
265
|
### Request extras
|
|
238
266
|
|
|
239
267
|
```typescript
|
|
@@ -258,12 +286,20 @@ queue.consume(topic?, id?, pollInterval=1000): AsyncGenerator<QueueJob>
|
|
|
258
286
|
```
|
|
259
287
|
|
|
260
288
|
### @tina4/swagger (`packages/swagger/`)
|
|
261
|
-
Auto-generates OpenAPI 3.0 docs.
|
|
289
|
+
Auto-generates OpenAPI 3.0.3 docs.
|
|
262
290
|
|
|
263
291
|
**Key files:**
|
|
264
292
|
- `generator.ts` — Produces OpenAPI spec from route table + model definitions
|
|
265
293
|
- `ui.ts` — Serves Swagger UI HTML (CDN-based) at `/swagger` and spec at `/swagger/openapi.json`
|
|
266
294
|
|
|
295
|
+
**3.13.40 spec behaviour:** ORM models become reusable `components.schemas` entries referenced by `$ref` (no more inlined duplicate shapes); a secured route emits a `bearerAuth` security requirement; the spec is OpenAPI 3.0.3.
|
|
296
|
+
|
|
297
|
+
**Environment (read by `generator.ts` / `ui.ts`):**
|
|
298
|
+
- `TINA4_SWAGGER_ENABLED` - turns the `/swagger` UI + `/swagger/openapi.json` endpoints on/off (`ui.ts`). Explicit `true`/`false` wins; unset falls back to `TINA4_DEBUG`. Set `false` to DISABLE swagger in ANY environment (including dev); set `true` to expose it in production. This is the documented production on/off switch (wired for real in 3.13.40 - previously ignored). **This is how you disable swagger.**
|
|
299
|
+
- `TINA4_SWAGGER_SERVERS` - comma-separated list of server URLs for the OpenAPI `servers[]` block (multi-server / multi-environment). Falls back to `SWAGGER_DEV_URL`, else the framework default.
|
|
300
|
+
- `TINA4_SWAGGER_UI_CDN` - base URL for the Swagger UI assets (`swagger-ui.css` + `swagger-ui-bundle.js`). Defaults to the public CDN (`https://unpkg.com/swagger-ui-dist@5`); point it at a self-hosted mirror for air-gapped deployments.
|
|
301
|
+
- Info block: `TINA4_SWAGGER_TITLE`, `TINA4_SWAGGER_VERSION`, `TINA4_SWAGGER_DESCRIPTION`, `TINA4_SWAGGER_CONTACT_EMAIL`, `TINA4_SWAGGER_CONTACT_TEAM`, `TINA4_SWAGGER_CONTACT_URL`, `TINA4_SWAGGER_LICENSE`.
|
|
302
|
+
|
|
267
303
|
### @tina4/frond (`packages/frond/`)
|
|
268
304
|
Built-in zero-dependency Twig-compatible template engine (the only template engine; there is no `twig` npm dependency).
|
|
269
305
|
|
|
@@ -775,6 +811,8 @@ const adults = User.query().where("age > ?", [18]).orderBy("name").get();
|
|
|
775
811
|
|
|
776
812
|
SQL-file based migrations under `migrations/`. The framework runs pending migrations on startup; the helpers here are for programmatic control (CLI, scripts, tests).
|
|
777
813
|
|
|
814
|
+
**Auto-run on startup (`TINA4_AUTO_MIGRATE`, default on).** `startServer()` calls `autoMigrateOnStartup()` (in `server.ts`) AFTER `initDatabase()`/model sync and BEFORE the server listens. When a `migrations/` folder with at least one `.sql` file exists, `TINA4_AUTO_MIGRATE` is not falsy (default `"true"`; `false`/`0`/`no`/`off` disable), and a DB adapter is resolvable, it runs the existing `migrate()` runner so the schema is current with no manual `tina4 migrate` step. It is **non-breaking**: a failure is logged via `Log.error` and the service still starts (a bad migration must never take the backend down). The runner is wrapped in try/catch and `autoMigrateOnStartup()` never rejects/throws out of the boot path. Set `TINA4_AUTO_MIGRATE=false` to disable (e.g. multi-instance production that migrates as a separate deploy step — concurrent first-apply can race). The explicit `tina4 migrate` CLI (`packages/cli/src/commands/migrate.ts`) is unaffected and stays **fail-fast** (`process.exit(1)` on a statement error) so CI keeps the non-zero exit code.
|
|
815
|
+
|
|
778
816
|
```typescript
|
|
779
817
|
import {
|
|
780
818
|
migrate, rollback, status, createMigration, syncModels,
|
|
@@ -788,7 +826,16 @@ await createMigration("add users table"); // scaffolds migrations/<ts>_add_use
|
|
|
788
826
|
syncModels(discoveredModels); // auto-create tables / add columns from `static fields`
|
|
789
827
|
```
|
|
790
828
|
|
|
791
|
-
|
|
829
|
+
### How migrations work internally
|
|
830
|
+
|
|
831
|
+
- SQL files live in `migrations/`, named `NNNNNN_description.sql` (sequential) or `YYYYMMDDHHMMSS_description.sql` (timestamp), and are split on the `;` delimiter.
|
|
832
|
+
- Files are applied in **numeric-prefix order** (`9_` before `10_` — a plain lexical sort misorders unpadded prefixes because `"10" < "9"`). A file with no numeric/timestamp prefix sorts **after** the numbered ones (lexically) and logs a `Log.warning` — its order is undefined.
|
|
833
|
+
- State is tracked by **row existence** in `tina4_migration` (id, name, batch, applied_at), auto-created per engine: a migration runs once — if a row with its `name` exists it is skipped. A FAILED migration is **never** recorded (no row is written, nothing is deleted) — fix the bad file and re-run; the `migrate()` summary's `failed[]` carries the failure.
|
|
834
|
+
- **Each migration FILE is wrapped in its own transaction.** On a failure the file rolls back and `migrate()` **STOPS** — later files are never applied on top of a missing earlier one (parity with Python/PHP/Ruby). Already-applied files stay applied. The explicit `tina4 migrate` CLI surfaces a non-empty `failed[]` as a non-zero exit; startup auto-migration logs it and the service still boots (see `TINA4_AUTO_MIGRATE` above).
|
|
835
|
+
- **Atomicity caveat:** per-file transactions are truly atomic only on engines with **transactional DDL (PostgreSQL)**. MySQL, Firebird, and SQLite auto-commit DDL, so a multi-statement migration that fails midway on those engines leaves earlier statements applied — keep one logical change per file. `CREATE TABLE` and `ALTER TABLE ... ADD` are made idempotent on Firebird/MSSQL (existence-checked via `RDB$RELATION_FIELDS` / `tableExists`) so a re-run with a raw `CREATE`/`ADD` does not error "object already exists"; SQLite/MySQL/PostgreSQL support `IF NOT EXISTS` and are left to the engine. Only a genuine already-exists is skipped — every other error still raises.
|
|
836
|
+
- The stored-proc block delimiters (`$$ … $$` / `// … //`) are extracted before splitting, but a `//` preceded by a colon is **not** treated as a delimiter, so a URL (`https://…`) or any `://` literal inside a migration is never swallowed as an opaque block.
|
|
837
|
+
|
|
838
|
+
Schema sync (`syncModels`) runs alongside SQL migrations on boot.
|
|
792
839
|
|
|
793
840
|
## Module: Frond (`packages/frond/src/engine.ts`)
|
|
794
841
|
|
|
@@ -821,6 +868,8 @@ frond.unsandbox();
|
|
|
821
868
|
|
|
822
869
|
Zero-dep HTTP client over `node:http` / `node:https`. Used by integrations, queue producers, health checks, and tests.
|
|
823
870
|
|
|
871
|
+
**Retry/backoff (opt-in, default off):** pass `maxRetries` (default `0`) and `retryBackoff` (default `0.5`s base, exponential) in the options bag — `new Api(url, { bearerToken, maxRetries: 3, retryBackoff: 0.5 })`. Retries a transport error (`http_code` null) or a retryable status (429/500/502/503/504); 4xx is never retried (a retried non-idempotent request may be re-sent, so retries are opt-in). Node's `node:http`/`node:https` doesn't auto-follow redirects, so there's no cross-host Authorization-leak surface (the redirect auth-strip is Python-only).
|
|
872
|
+
|
|
824
873
|
```typescript
|
|
825
874
|
import { Api } from "@tina4/core";
|
|
826
875
|
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# With the Tina4 CLI (recommended
|
|
20
|
+
# With the Tina4 CLI (recommended, enables SCSS + live reload)
|
|
21
21
|
cargo install tina4 # or grab a binary from https://github.com/tina4stack/tina4/releases
|
|
22
22
|
tina4 init nodejs ./my-app
|
|
23
23
|
cd my-app && tina4 serve
|
|
@@ -57,12 +57,12 @@ export default class User {
|
|
|
57
57
|
| Category | Features |
|
|
58
58
|
|----------|----------|
|
|
59
59
|
| **Core HTTP** (7) | Router with path params (`{id:int}`, `{p:path}`), Server, Request/Response, Middleware pipeline, Static file serving, CORS |
|
|
60
|
-
| **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird
|
|
60
|
+
| **Database** (6) | SQLite, PostgreSQL, MySQL, MSSQL, Firebird: unified adapter, connection pooling, query cache, transactions, race-safe ID generation, SQL dialect translation |
|
|
61
61
|
| **ORM** (7) | Active Record with typed fields, relationships (`has_one`/`has_many`/`belongs_to`), soft delete, QueryBuilder + MongoDB support, Auto-CRUD generator, migrations with rollback |
|
|
62
62
|
| **Auth & Security** (5) | JWT (HS256/RS256), password hashing (PBKDF2-SHA256), API key validation, rate limiting, CSRF form tokens |
|
|
63
63
|
| **Templating** (3) | Frond engine (Twig/Jinja2-compatible, pre-compiled 2.8× faster), SCSS auto-compilation, built-in CSS (~24 KB) |
|
|
64
64
|
| **API & Integration** (5) | HTTP client (zero-dep), GraphQL with ORM auto-schema + GraphiQL IDE, WSDL/SOAP with auto WSDL, WebSocket (RFC 6455) + Redis backplane, MCP server (24 dev tools) |
|
|
65
|
-
| **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters
|
|
65
|
+
| **Background** (3) | Job queue (File/RabbitMQ/Kafka/MongoDB) with priority, delay, retry, dead letters; service runner; event system (on/emit/once/off) |
|
|
66
66
|
| **Data & Storage** (4) | Session (File/Redis/Valkey/MongoDB/DB), response cache (LRU, TTL), seeder + 50+ fake data generators, messenger (SMTP/IMAP) |
|
|
67
67
|
| **Developer Tools** (7) | Dev dashboard (11 tabs), dev toolbar, error overlay (Catppuccin Mocha), dev mailbox, hot reload + CSS hot-reload, code metrics (complexity, coupling, maintainability), AI context installer (7 tools) |
|
|
68
68
|
| **Utilities** (7) | DI container (transient + singleton), HtmlElement builder, inline testing (`@tests` decorator), i18n (6 languages), Swagger/OpenAPI auto-generation, CLI scaffolding (`generate model/route/migration/middleware`), structured logging |
|
|
@@ -86,14 +86,14 @@ npx tina4nodejs generate model <name>
|
|
|
86
86
|
|
|
87
87
|
## Performance
|
|
88
88
|
|
|
89
|
-
Benchmarked with `wrk
|
|
89
|
+
Benchmarked with `wrk`: 5,000 requests, 50 concurrent, median of 3 runs:
|
|
90
90
|
|
|
91
91
|
| Framework | JSON req/s | Deps | Features |
|
|
92
92
|
|-----------|-----------|------|----------|
|
|
93
93
|
| Raw `node:http` | 91,110 | 0 | 1 |
|
|
94
94
|
| **Tina4 Node.js** | **84,771** | 0 | 55 |
|
|
95
95
|
|
|
96
|
-
Tina4 Node.js runs at **93% of raw Node.js speed** while providing 55 built-in features
|
|
96
|
+
Tina4 Node.js runs at **93% of raw Node.js speed** while providing 55 built-in features, a zero-overhead architecture.
|
|
97
97
|
|
|
98
98
|
**Across all 4 Tina4 implementations:**
|
|
99
99
|
|
|
@@ -107,7 +107,7 @@ Tina4 Node.js runs at **93% of raw Node.js speed** while providing 55 built-in f
|
|
|
107
107
|
|
|
108
108
|
## Cross-Framework Parity
|
|
109
109
|
|
|
110
|
-
Tina4 ships identical features across four languages
|
|
110
|
+
Tina4 ships identical features across four languages: same architecture, same conventions, same 55 features:
|
|
111
111
|
|
|
112
112
|
| | Python | PHP | Ruby | Node.js |
|
|
113
113
|
|---|--------|-----|------|---------|
|
package/package.json
CHANGED
package/packages/core/src/api.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface ApiResult {
|
|
|
18
18
|
error: string | null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* HTTP statuses that warrant an automatic retry when `maxRetries` > 0:
|
|
23
|
+
* rate-limit (429) plus the transient server-side 5xx family. 4xx client
|
|
24
|
+
* errors (401, 404, …) are NOT retried — a repeat won't succeed.
|
|
25
|
+
*/
|
|
26
|
+
const RETRY_STATUSES: ReadonlySet<number> = new Set([429, 500, 502, 503, 504]);
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
29
|
* Constructor options for {@link Api}. Used as the second argument to
|
|
23
30
|
* `new Api(url, { ... })` — cross-framework parity with Python
|
|
@@ -33,6 +40,16 @@ export interface ApiOptions {
|
|
|
33
40
|
username?: string;
|
|
34
41
|
password?: string;
|
|
35
42
|
headers?: Record<string, string>;
|
|
43
|
+
/**
|
|
44
|
+
* Maximum automatic retries on a transient failure (default 0 = off, so
|
|
45
|
+
* existing callers are unaffected). When > 0, a transport error or a
|
|
46
|
+
* retryable status (429/5xx) is retried up to this many times with
|
|
47
|
+
* exponential backoff. NOTE: a retried non-idempotent request (POST/…)
|
|
48
|
+
* may be re-sent — retries are opt-in for that reason.
|
|
49
|
+
*/
|
|
50
|
+
maxRetries?: number;
|
|
51
|
+
/** Base backoff in seconds, doubling each attempt (default 0.5). */
|
|
52
|
+
retryBackoff?: number;
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
export class Api {
|
|
@@ -41,6 +58,8 @@ export class Api {
|
|
|
41
58
|
private timeout: number;
|
|
42
59
|
private authHeader: string;
|
|
43
60
|
private ignoreSsl: boolean;
|
|
61
|
+
private maxRetries: number;
|
|
62
|
+
private retryBackoff: number;
|
|
44
63
|
|
|
45
64
|
/**
|
|
46
65
|
* Construct an Api client.
|
|
@@ -60,6 +79,14 @@ export class Api {
|
|
|
60
79
|
* Bearer wins over basic-auth when both passed. `verifySsl: false` is
|
|
61
80
|
* the positive form of `ignoreSsl: true`; `ignoreSsl` wins when both
|
|
62
81
|
* supplied for backward compatibility.
|
|
82
|
+
*
|
|
83
|
+
* `maxRetries` (default 0 = off) enables automatic retry with
|
|
84
|
+
* exponential backoff (`retryBackoff` seconds base, doubling each
|
|
85
|
+
* attempt) on a transport error or a retryable status (429/5xx). A
|
|
86
|
+
* retried non-idempotent request (POST/…) may be re-sent — retries are
|
|
87
|
+
* opt-in for that reason.
|
|
88
|
+
*
|
|
89
|
+
* new Api("https://api.example.com", { maxRetries: 3, retryBackoff: 0.5 });
|
|
63
90
|
*/
|
|
64
91
|
constructor(
|
|
65
92
|
baseUrl: string = "",
|
|
@@ -68,6 +95,9 @@ export class Api {
|
|
|
68
95
|
) {
|
|
69
96
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
70
97
|
this.headers = {};
|
|
98
|
+
// Retry defaults: off (0) so existing callers are unaffected.
|
|
99
|
+
this.maxRetries = 0;
|
|
100
|
+
this.retryBackoff = 0.5;
|
|
71
101
|
|
|
72
102
|
// Options-bag form — second arg is an object literal
|
|
73
103
|
if (typeof authHeaderOrOptions === "object" && authHeaderOrOptions !== null) {
|
|
@@ -75,6 +105,8 @@ export class Api {
|
|
|
75
105
|
this.authHeader = opts.authHeader ?? "";
|
|
76
106
|
this.timeout = opts.timeout ?? timeout;
|
|
77
107
|
this.ignoreSsl = (opts.ignoreSsl ?? false) || (opts.verifySsl === false);
|
|
108
|
+
this.maxRetries = Math.max(0, opts.maxRetries ?? 0);
|
|
109
|
+
this.retryBackoff = opts.retryBackoff ?? 0.5;
|
|
78
110
|
|
|
79
111
|
// Bearer wins over basic-auth when both are passed
|
|
80
112
|
if (opts.bearerToken != null) {
|
|
@@ -189,7 +221,38 @@ export class Api {
|
|
|
189
221
|
return `${this.baseUrl}/${path.replace(/^\/+/, "")}`;
|
|
190
222
|
}
|
|
191
223
|
|
|
192
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Execute the request with opt-in retry/backoff.
|
|
226
|
+
*
|
|
227
|
+
* With `maxRetries` > 0, a transport failure (`http_code` null) or a
|
|
228
|
+
* retryable status (429/5xx) is retried up to `maxRetries` times with
|
|
229
|
+
* exponential backoff; any other outcome (2xx, 3xx, other 4xx) returns
|
|
230
|
+
* at once. A retried non-idempotent request may be re-sent — retries
|
|
231
|
+
* are opt-in for that reason.
|
|
232
|
+
*/
|
|
233
|
+
private async execute(
|
|
234
|
+
method: string,
|
|
235
|
+
url: string,
|
|
236
|
+
body?: unknown,
|
|
237
|
+
contentType: string = "application/json",
|
|
238
|
+
): Promise<ApiResult> {
|
|
239
|
+
const attempts = this.maxRetries + 1;
|
|
240
|
+
let result: ApiResult = { http_code: null, body: null, headers: {}, error: null };
|
|
241
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
242
|
+
result = await this.attempt(method, url, body, contentType);
|
|
243
|
+
const code = result.http_code;
|
|
244
|
+
const retryable = code === null || RETRY_STATUSES.has(code);
|
|
245
|
+
if (!retryable || attempt === attempts - 1) {
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
const delayMs = this.retryBackoff * Math.pow(2, attempt) * 1000;
|
|
249
|
+
await new Promise<void>((r) => setTimeout(r, delayMs));
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** A single HTTP attempt — returns the standardized result. */
|
|
255
|
+
private attempt(
|
|
193
256
|
method: string,
|
|
194
257
|
url: string,
|
|
195
258
|
body?: unknown,
|
|
@@ -29,7 +29,10 @@ import { isTruthy } from "./dotenv.js";
|
|
|
29
29
|
/** Actionable blank-secret warning — emitted from both the bootstrap (CI/prod) and the lazy resolvers. */
|
|
30
30
|
const BLANK_SECRET_WARNING =
|
|
31
31
|
"Auth: TINA4_SECRET is not set — JWT signing is insecure. Set TINA4_SECRET to a random " +
|
|
32
|
-
"value (e.g. `openssl rand -hex 32`) in your environment or .env before serving traffic."
|
|
32
|
+
"value (e.g. `openssl rand -hex 32`) in your environment or .env before serving traffic. " +
|
|
33
|
+
"For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated automatically " +
|
|
34
|
+
"into .env.local (gitignored). Seeing this warning means the run was NOT detected as dev — " +
|
|
35
|
+
"typically a container or CI without TINA4_DEBUG set, or TINA4_ENV=production.";
|
|
33
36
|
|
|
34
37
|
/** True when running under CI — the de-facto `CI` env var (set by every major CI). */
|
|
35
38
|
function _isCi(): boolean {
|
|
@@ -14,12 +14,13 @@ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, copyFi
|
|
|
14
14
|
import { join, dirname, resolve, relative } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import type { Router } from "./router.js";
|
|
17
|
-
import type { RouteHandler } from "./types.js";
|
|
17
|
+
import type { RouteHandler, Tina4Request } from "./types.js";
|
|
18
18
|
import { DevMailbox } from "./devMailbox.js";
|
|
19
19
|
import { isTruthy } from "./dotenv.js";
|
|
20
20
|
import { quickMetrics, fullAnalysis, fileDetail } from "./metrics.js";
|
|
21
21
|
import { registerFeedbackRoutes } from "./feedback.js";
|
|
22
|
-
import { getDefaultDevServer } from "./mcp.js";
|
|
22
|
+
import { getDefaultDevServer, mcpEnabled, isRequestAllowed } from "./mcp.js";
|
|
23
|
+
import { timingSafeEqual } from "node:crypto";
|
|
23
24
|
|
|
24
25
|
const cpuCount = osCpus().length;
|
|
25
26
|
|
|
@@ -561,18 +562,6 @@ export class DevAdmin {
|
|
|
561
562
|
{ method: "POST", pattern: "/__dev/api/deps/install", handler: handleDepsInstall },
|
|
562
563
|
// Git status
|
|
563
564
|
{ method: "GET", pattern: "/__dev/api/git/status", handler: handleGitStatus },
|
|
564
|
-
// MCP tool introspection over the built-in MCP server (browser dev-admin REST shim)
|
|
565
|
-
{ method: "GET", pattern: "/__dev/api/mcp/tools", handler: handleMcpTools },
|
|
566
|
-
{ method: "POST", pattern: "/__dev/api/mcp/call", handler: handleMcpCall },
|
|
567
|
-
// MCP JSON-RPC + SSE endpoints that REAL MCP clients (Claude Code/Desktop)
|
|
568
|
-
// speak. POST /__dev/mcp[/message] -> JSON-RPC handleMessage; GET
|
|
569
|
-
// /__dev/mcp/sse -> SSE handshake announcing the message endpoint. Mounted
|
|
570
|
-
// through the same dispatch as the REST shim above and gated by the same
|
|
571
|
-
// /__dev public-route rule. Mirrors the Python v3 fix (POST /__dev/mcp +
|
|
572
|
-
// /__dev/mcp/message, GET /__dev/mcp/sse).
|
|
573
|
-
{ method: "POST", pattern: "/__dev/mcp", handler: handleMcpMessage },
|
|
574
|
-
{ method: "POST", pattern: "/__dev/mcp/message", handler: handleMcpMessage },
|
|
575
|
-
{ method: "GET", pattern: "/__dev/mcp/sse", handler: handleMcpSse },
|
|
576
565
|
// Scaffolding
|
|
577
566
|
{ method: "GET", pattern: "/__dev/api/scaffold", handler: handleScaffoldList },
|
|
578
567
|
{ method: "POST", pattern: "/__dev/api/scaffold/run", handler: handleScaffoldRun },
|
|
@@ -610,12 +599,41 @@ export class DevAdmin {
|
|
|
610
599
|
});
|
|
611
600
|
}
|
|
612
601
|
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
|
|
602
|
+
// MCP exposure is gated SEPARATELY from the rest of the dev dashboard.
|
|
603
|
+
// The MCP dev tools expose powerful operations (DB query, file read/WRITE,
|
|
604
|
+
// route listing), so they must NOT auto-expose on a non-localhost
|
|
605
|
+
// TINA4_DEBUG=true deployment. mcpEnabled() honours an explicit TINA4_MCP on
|
|
606
|
+
// any host, else requires TINA4_DEBUG AND (localhost OR TINA4_MCP_REMOTE) —
|
|
607
|
+
// full parity with Python master tina4_python.mcp.is_enabled(). When the
|
|
608
|
+
// gate is closed, neither the REST shim, the JSON-RPC/SSE endpoints, nor the
|
|
609
|
+
// default dev MCP server (with its dev tools) are registered.
|
|
610
|
+
if (mcpEnabled()) {
|
|
611
|
+
const mcpRoutes: Array<{ method: string; pattern: string; handler: RouteHandler }> = [
|
|
612
|
+
// MCP tool introspection over the built-in MCP server (browser dev-admin REST shim)
|
|
613
|
+
{ method: "GET", pattern: "/__dev/api/mcp/tools", handler: handleMcpTools },
|
|
614
|
+
{ method: "POST", pattern: "/__dev/api/mcp/call", handler: handleMcpCall },
|
|
615
|
+
// MCP JSON-RPC + SSE endpoints that REAL MCP clients (Claude Code/Desktop)
|
|
616
|
+
// speak. POST /__dev/mcp[/message] -> JSON-RPC handleMessage; GET
|
|
617
|
+
// /__dev/mcp/sse -> SSE handshake announcing the message endpoint. Mirrors
|
|
618
|
+
// the Python v3 fix (POST /__dev/mcp + /__dev/mcp/message, GET /__dev/mcp/sse).
|
|
619
|
+
{ method: "POST", pattern: "/__dev/mcp", handler: handleMcpMessage },
|
|
620
|
+
{ method: "POST", pattern: "/__dev/mcp/message", handler: handleMcpMessage },
|
|
621
|
+
{ method: "GET", pattern: "/__dev/mcp/sse", handler: handleMcpSse },
|
|
622
|
+
];
|
|
623
|
+
for (const route of mcpRoutes) {
|
|
624
|
+
router.addRoute({
|
|
625
|
+
method: route.method,
|
|
626
|
+
pattern: route.pattern,
|
|
627
|
+
handler: route.handler,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Ensure the default /__dev/mcp MCP server exists with its dev tools
|
|
632
|
+
// registered. This is the single shared instance behind both the REST shim
|
|
633
|
+
// and the JSON-RPC + SSE endpoints registered above, so tools/list and the
|
|
634
|
+
// REST shim return tools immediately, before any first call.
|
|
635
|
+
getDefaultDevServer();
|
|
636
|
+
}
|
|
619
637
|
}
|
|
620
638
|
|
|
621
639
|
/**
|
|
@@ -2134,7 +2152,47 @@ const handleGitStatus: RouteHandler = async (_req, res) => {
|
|
|
2134
2152
|
}
|
|
2135
2153
|
};
|
|
2136
2154
|
|
|
2137
|
-
|
|
2155
|
+
/** Constant-time string compare (length-guarded so timingSafeEqual never throws). */
|
|
2156
|
+
function mcpSecureEqual(expected: string, provided: string): boolean {
|
|
2157
|
+
const a = Buffer.from(expected);
|
|
2158
|
+
const b = Buffer.from(provided);
|
|
2159
|
+
if (a.length !== b.length) return false;
|
|
2160
|
+
return timingSafeEqual(a, b);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Whether the request carried a token matching TINA4_MCP_TOKEN (fallback
|
|
2165
|
+
* TINA4_API_KEY). Transports: Authorization Bearer / X-MCP-Token / X-Api-Key.
|
|
2166
|
+
* No configured token ⇒ a remote caller can never present a valid one.
|
|
2167
|
+
*/
|
|
2168
|
+
function mcpTokenOk(req: Tina4Request): boolean {
|
|
2169
|
+
let expected = process.env.TINA4_MCP_TOKEN;
|
|
2170
|
+
if (!expected) expected = process.env.TINA4_API_KEY;
|
|
2171
|
+
if (!expected) return false;
|
|
2172
|
+
let provided = "";
|
|
2173
|
+
const auth = req.header("authorization") ?? "";
|
|
2174
|
+
if (auth.toLowerCase().startsWith("bearer ")) provided = auth.slice(7).trim();
|
|
2175
|
+
if (!provided) provided = req.header("x-mcp-token") ?? "";
|
|
2176
|
+
if (!provided) provided = req.header("x-api-key") ?? "";
|
|
2177
|
+
if (!provided) return false;
|
|
2178
|
+
return mcpSecureEqual(expected, provided);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Per-request MCP authorisation using the RAW socket peer (never X-Forwarded-For,
|
|
2183
|
+
* which is spoofable). Loopback is always allowed; a remote caller needs
|
|
2184
|
+
* TINA4_MCP_REMOTE=true plus a valid token. Mirrors the Python/PHP/Ruby gate.
|
|
2185
|
+
*/
|
|
2186
|
+
function mcpRequestAllowed(req: Tina4Request): boolean {
|
|
2187
|
+
const peer = (req as unknown as { socket?: { remoteAddress?: string } }).socket?.remoteAddress ?? "";
|
|
2188
|
+
return isRequestAllowed(peer, mcpTokenOk(req));
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
const handleMcpTools: RouteHandler = async (req, res) => {
|
|
2192
|
+
if (!mcpRequestAllowed(req)) {
|
|
2193
|
+
res.json({ tools: [], error: "MCP forbidden" }, 404);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2138
2196
|
try {
|
|
2139
2197
|
// Ensure the default /__dev/mcp server exists with its dev tools registered,
|
|
2140
2198
|
// then enumerate every registered MCP server instance (app-defined servers
|
|
@@ -2155,6 +2213,10 @@ const handleMcpTools: RouteHandler = async (_req, res) => {
|
|
|
2155
2213
|
};
|
|
2156
2214
|
|
|
2157
2215
|
const handleMcpCall: RouteHandler = async (req, res) => {
|
|
2216
|
+
if (!mcpRequestAllowed(req)) {
|
|
2217
|
+
res.json({ error: "MCP forbidden" }, 404);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2158
2220
|
const body = (req.body as Record<string, unknown>) || {};
|
|
2159
2221
|
const name = (body.name as string) || "";
|
|
2160
2222
|
const args = (body.arguments as Record<string, unknown>) || {};
|
|
@@ -2186,6 +2248,10 @@ const handleMcpCall: RouteHandler = async (req, res) => {
|
|
|
2186
2248
|
* clients connect without a token.
|
|
2187
2249
|
*/
|
|
2188
2250
|
const handleMcpMessage: RouteHandler = async (req, res) => {
|
|
2251
|
+
if (!mcpRequestAllowed(req)) {
|
|
2252
|
+
res.json({ error: "MCP forbidden" }, 404);
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2189
2255
|
try {
|
|
2190
2256
|
const { getDefaultDevServer } = await import("./mcp.js");
|
|
2191
2257
|
const server = getDefaultDevServer();
|
|
@@ -2216,6 +2282,10 @@ const handleMcpMessage: RouteHandler = async (req, res) => {
|
|
|
2216
2282
|
* the Python v3 fix. Content-Type text/event-stream, status 200.
|
|
2217
2283
|
*/
|
|
2218
2284
|
const handleMcpSse: RouteHandler = async (req, res) => {
|
|
2285
|
+
if (!mcpRequestAllowed(req)) {
|
|
2286
|
+
res.json({ error: "MCP forbidden" }, 404);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2219
2289
|
// req.path is the path only (no query); turn /__dev/mcp/sse into the message
|
|
2220
2290
|
// endpoint /__dev/mcp/message that the client should POST to.
|
|
2221
2291
|
const reqPath = req.path || "/__dev/mcp/sse";
|
|
@@ -15,7 +15,7 @@ export type {
|
|
|
15
15
|
|
|
16
16
|
export { startServer, resolvePortAndHost, handle, start, stop, httpReason, resolveTemplate, resetTemplateCache, templateAutoRoutingEnabled, isBannerSuppressed } from "./server.js";
|
|
17
17
|
export { background, stopAllBackgroundTasks, backgroundTaskCount } from "./background.js";
|
|
18
|
-
export { Router, RouteGroup, RouteRef, defaultRouter, runRouteMiddlewares, resolveStringMiddleware, isTrailingSlashRedirectEnabled } from "./router.js";
|
|
18
|
+
export { Router, RouteGroup, RouteRef, WsRouteRef, defaultRouter, runRouteMiddlewares, resolveStringMiddleware, isTrailingSlashRedirectEnabled } from "./router.js";
|
|
19
19
|
export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
|
|
20
20
|
export type { RouteInfo } from "./router.js";
|
|
21
21
|
export { discoverRoutes } from "./routeDiscovery.js";
|
|
@@ -64,6 +64,7 @@ export {
|
|
|
64
64
|
WebSocketServer,
|
|
65
65
|
devReloadWs,
|
|
66
66
|
computeAcceptKey, parseUpgradeHeaders, buildFrame, parseFrame, originAllowed,
|
|
67
|
+
wsToken, wsAuthorized, offeredBearerSubprotocol, serveWebSocketRoute, wsRouteManager,
|
|
67
68
|
OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG,
|
|
68
69
|
CLOSE_NORMAL, CLOSE_GOING_AWAY, CLOSE_PROTOCOL_ERROR, CLOSE_POLICY_VIOLATION,
|
|
69
70
|
} from "./websocket.js";
|
|
@@ -100,8 +101,12 @@ export type { ImapMessage, ImapFullMessage } from "./messenger.js";
|
|
|
100
101
|
export { LiteBackend } from "./queueBackends/liteBackend.js";
|
|
101
102
|
export { RabbitMQBackend, parseAmqpUrl } from "./queueBackends/rabbitmqBackend.js";
|
|
102
103
|
export type { RabbitMQConfig } from "./queueBackends/rabbitmqBackend.js";
|
|
103
|
-
export { KafkaBackend } from "./queueBackends/kafkaBackend.js";
|
|
104
|
-
export type {
|
|
104
|
+
export { KafkaBackend, kafkaSecurityConfig } from "./queueBackends/kafkaBackend.js";
|
|
105
|
+
export type {
|
|
106
|
+
KafkaConfig,
|
|
107
|
+
KafkaSecurityConfig,
|
|
108
|
+
KafkaClientConfig,
|
|
109
|
+
} from "./queueBackends/kafkaBackend.js";
|
|
105
110
|
export { MongoBackend } from "./queueBackends/mongoBackend.js";
|
|
106
111
|
export type { MongoConfig as MongoQueueConfig } from "./queueBackends/mongoBackend.js";
|
|
107
112
|
export { DatabaseSessionHandler } from "./sessionHandlers/databaseHandler.js";
|
|
@@ -130,7 +135,7 @@ export type {
|
|
|
130
135
|
export {
|
|
131
136
|
McpServer, mcpTool, mcpResource, registerDevTools, getDefaultDevServer,
|
|
132
137
|
encodeResponse, encodeError, encodeNotification, decodeRequest,
|
|
133
|
-
schemaFromParams, isLocalhost, mcpEnabled, mcpPort,
|
|
138
|
+
schemaFromParams, isLocalhost, isLoopback, mcpEnabled, isRequestAllowed, mcpPort,
|
|
134
139
|
PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND, INVALID_PARAMS, INTERNAL_ERROR,
|
|
135
140
|
} from "./mcp.js";
|
|
136
141
|
export type { JsonRpcMessage, McpToolDefinition, McpResourceDefinition, JsonSchema, McpToolParam } from "./mcp.js";
|