@browserlessapi/inferlo 0.7.0 → 0.8.0
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/README.md +3 -2
- package/dist/app.d.ts +24 -0
- package/dist/app.js +336 -13
- package/dist/app.js.map +1 -1
- package/dist/authjs.d.ts +70 -0
- package/dist/authjs.js +94 -0
- package/dist/authjs.js.map +1 -0
- package/dist/better-auth.d.ts +89 -0
- package/dist/better-auth.js +75 -0
- package/dist/better-auth.js.map +1 -0
- package/dist/cli/main.js +1 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/client-ip.d.ts +5 -0
- package/dist/client-ip.js +5 -0
- package/dist/client-ip.js.map +1 -1
- package/dist/cross-site.d.ts +14 -0
- package/dist/cross-site.js +56 -0
- package/dist/cross-site.js.map +1 -0
- package/dist/db/postgres.js +19 -5
- package/dist/db/postgres.js.map +1 -1
- package/dist/describe.d.ts +9 -2
- package/dist/describe.js +11 -1
- package/dist/describe.js.map +1 -1
- package/dist/disk-storage.d.ts +6 -0
- package/dist/disk-storage.js +96 -0
- package/dist/disk-storage.js.map +1 -0
- package/dist/files.d.ts +23 -0
- package/dist/files.js +168 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +10 -4
- package/dist/jobs.js +7 -2
- package/dist/jobs.js.map +1 -1
- package/dist/jwt.d.ts +83 -0
- package/dist/jwt.js +312 -0
- package/dist/jwt.js.map +1 -0
- package/dist/model.js +6 -1
- package/dist/model.js.map +1 -1
- package/dist/oidc.d.ts +70 -0
- package/dist/oidc.js +379 -0
- package/dist/oidc.js.map +1 -0
- package/dist/openapi.js +17 -2
- package/dist/openapi.js.map +1 -1
- package/dist/passport.d.ts +54 -0
- package/dist/passport.js +68 -0
- package/dist/passport.js.map +1 -0
- package/dist/postgres-js.d.ts +39 -0
- package/dist/postgres-js.js +71 -0
- package/dist/postgres-js.js.map +1 -0
- package/dist/redis-sockets.d.ts +24 -0
- package/dist/redis-sockets.js +75 -0
- package/dist/redis-sockets.js.map +1 -0
- package/dist/route.d.ts +103 -2
- package/dist/route.js +27 -0
- package/dist/route.js.map +1 -1
- package/dist/schema.d.ts +21 -1
- package/dist/schema.js +70 -0
- package/dist/schema.js.map +1 -1
- package/dist/serve.d.ts +18 -0
- package/dist/serve.js +137 -2
- package/dist/serve.js.map +1 -1
- package/dist/sessions.js +35 -4
- package/dist/sessions.js.map +1 -1
- package/dist/sockets.d.ts +58 -0
- package/dist/sockets.js +111 -0
- package/dist/sockets.js.map +1 -0
- package/dist/storage.d.ts +47 -0
- package/dist/storage.js +123 -0
- package/dist/storage.js.map +1 -0
- package/dist/websocket.d.ts +81 -0
- package/dist/websocket.js +251 -0
- package/dist/websocket.js.map +1 -0
- package/llms.txt +1 -0
- package/package.json +43 -3
- package/templates/app/AGENTS.md +5 -3
- package/templates/app/test/notes.test.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserlessapi/inferlo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The enterprise-ready TypeScript backend framework, with AI models built in: routing, validation, PostgreSQL and migrations, auth, rate limiting, jobs, OpenAPI and MCP, plus Anthropic, OpenAI, Gemini, Ollama and OpenAI-compatible models.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,10 +47,18 @@
|
|
|
47
47
|
"types": "./dist/astro.d.ts",
|
|
48
48
|
"default": "./dist/astro.js"
|
|
49
49
|
},
|
|
50
|
+
"./authjs": {
|
|
51
|
+
"types": "./dist/authjs.d.ts",
|
|
52
|
+
"default": "./dist/authjs.js"
|
|
53
|
+
},
|
|
50
54
|
"./azure": {
|
|
51
55
|
"types": "./dist/azure.d.ts",
|
|
52
56
|
"default": "./dist/azure.js"
|
|
53
57
|
},
|
|
58
|
+
"./better-auth": {
|
|
59
|
+
"types": "./dist/better-auth.d.ts",
|
|
60
|
+
"default": "./dist/better-auth.js"
|
|
61
|
+
},
|
|
54
62
|
"./elysia": {
|
|
55
63
|
"types": "./dist/elysia.d.ts",
|
|
56
64
|
"default": "./dist/elysia.js"
|
|
@@ -87,6 +95,10 @@
|
|
|
87
95
|
"types": "./dist/nextjs.d.ts",
|
|
88
96
|
"default": "./dist/nextjs.js"
|
|
89
97
|
},
|
|
98
|
+
"./passport": {
|
|
99
|
+
"types": "./dist/passport.d.ts",
|
|
100
|
+
"default": "./dist/passport.js"
|
|
101
|
+
},
|
|
90
102
|
"./nestjs": {
|
|
91
103
|
"types": "./dist/nestjs.d.ts",
|
|
92
104
|
"default": "./dist/nestjs.js"
|
|
@@ -111,6 +123,10 @@
|
|
|
111
123
|
"types": "./dist/db/postgres.d.ts",
|
|
112
124
|
"default": "./dist/db/postgres.js"
|
|
113
125
|
},
|
|
126
|
+
"./postgres-js": {
|
|
127
|
+
"types": "./dist/postgres-js.d.ts",
|
|
128
|
+
"default": "./dist/postgres-js.js"
|
|
129
|
+
},
|
|
114
130
|
"./testing": {
|
|
115
131
|
"types": "./dist/testing.d.ts",
|
|
116
132
|
"default": "./dist/testing.js"
|
|
@@ -133,7 +149,7 @@
|
|
|
133
149
|
"scripts": {
|
|
134
150
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli/main.js",
|
|
135
151
|
"prepare": "npm run build",
|
|
136
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p examples/helpdesk && tsc --noEmit -p examples/with-hono && tsc --noEmit -p examples/with-express && tsc --noEmit -p examples/with-fastify && tsc --noEmit -p examples/with-elysia && tsc --noEmit -p examples/with-nestjs && tsc --noEmit -p examples/with-trpc && tsc --noEmit -p examples/with-adonisjs && tsc --noEmit -p examples/with-tsed && tsc --noEmit -p examples/with-encore && tsc --noEmit -p examples/with-koa && tsc --noEmit -p examples/with-nextjs && tsc --noEmit -p examples/with-nitro && tsc --noEmit -p examples/with-sveltekit && tsc --noEmit -p examples/with-react-router && tsc --noEmit -p examples/with-lambda && tsc --noEmit -p examples/with-hapi && tsc --noEmit -p examples/with-azure && tsc --noEmit -p examples/with-cloud-run-functions && tsc --noEmit -p examples/with-cloudflare-workers && tsc --noEmit -p examples/with-vercel && tsc --noEmit -p examples/with-netlify && tsc --noEmit -p examples/with-astro && tsc --noEmit -p examples/with-drizzle",
|
|
152
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p examples/helpdesk && tsc --noEmit -p examples/with-hono && tsc --noEmit -p examples/with-express && tsc --noEmit -p examples/with-fastify && tsc --noEmit -p examples/with-elysia && tsc --noEmit -p examples/with-nestjs && tsc --noEmit -p examples/with-trpc && tsc --noEmit -p examples/with-adonisjs && tsc --noEmit -p examples/with-tsed && tsc --noEmit -p examples/with-encore && tsc --noEmit -p examples/with-koa && tsc --noEmit -p examples/with-nextjs && tsc --noEmit -p examples/with-nitro && tsc --noEmit -p examples/with-sveltekit && tsc --noEmit -p examples/with-react-router && tsc --noEmit -p examples/with-lambda && tsc --noEmit -p examples/with-hapi && tsc --noEmit -p examples/with-azure && tsc --noEmit -p examples/with-cloud-run-functions && tsc --noEmit -p examples/with-cloudflare-workers && tsc --noEmit -p examples/with-vercel && tsc --noEmit -p examples/with-netlify && tsc --noEmit -p examples/with-astro && tsc --noEmit -p examples/with-drizzle && tsc --noEmit -p examples/with-typeorm && tsc --noEmit -p examples/with-kysely && tsc --noEmit -p examples/with-sequelize && tsc --noEmit -p examples/with-knex && tsc --noEmit -p examples/with-postgres-js && tsc --noEmit -p examples/with-mikro-orm && tsc --noEmit -p examples/with-better-auth && node examples/with-prisma/generate.ts && tsc --noEmit -p examples/with-prisma",
|
|
137
153
|
"test": "node --test --test-reporter=spec \"test/**/*.test.ts\" \"examples/*/test/*.test.ts\"",
|
|
138
154
|
"test:bun": "bun test test/",
|
|
139
155
|
"test:deno": "deno test -A --no-check --ignore=test/tsed.test.ts test/ && deno test -A --no-check --ignore=examples/with-tsed examples/ && deno test -A --no-check --unsafe-proto test/tsed.test.ts examples/with-tsed/",
|
|
@@ -146,7 +162,8 @@
|
|
|
146
162
|
"peerDependencies": {
|
|
147
163
|
"@langchain/core": ">=0.3.0",
|
|
148
164
|
"@opentelemetry/api": ">=1.3.0",
|
|
149
|
-
"pg": ">=8"
|
|
165
|
+
"pg": ">=8",
|
|
166
|
+
"postgres": ">=3.4.0"
|
|
150
167
|
},
|
|
151
168
|
"peerDependenciesMeta": {
|
|
152
169
|
"@langchain/core": {
|
|
@@ -157,20 +174,28 @@
|
|
|
157
174
|
},
|
|
158
175
|
"pg": {
|
|
159
176
|
"optional": true
|
|
177
|
+
},
|
|
178
|
+
"postgres": {
|
|
179
|
+
"optional": true
|
|
160
180
|
}
|
|
161
181
|
},
|
|
162
182
|
"devDependencies": {
|
|
163
183
|
"@adonisjs/core": "^6.21.0",
|
|
164
184
|
"@ai-sdk/openai-compatible": "^3.0.85",
|
|
185
|
+
"@auth/core": "^0.41.3",
|
|
165
186
|
"@azure/functions": "^4.17.0",
|
|
166
187
|
"@google-cloud/functions-framework": "^5.0.5",
|
|
167
188
|
"@hapi/hapi": "^21.4.13",
|
|
168
189
|
"@hono/node-server": "^2.1.6",
|
|
169
190
|
"@langchain/core": "^1.3.5",
|
|
191
|
+
"@mikro-orm/core": "^7.2.8",
|
|
192
|
+
"@mikro-orm/postgresql": "^7.2.8",
|
|
170
193
|
"@nestjs/common": "^12.3.3",
|
|
171
194
|
"@nestjs/core": "^12.3.3",
|
|
172
195
|
"@nestjs/platform-express": "^12.3.3",
|
|
173
196
|
"@opentelemetry/api": "^1.10.0",
|
|
197
|
+
"@prisma/adapter-pg": "^7.10.1",
|
|
198
|
+
"@prisma/client": "^7.10.1",
|
|
174
199
|
"@sveltejs/kit": "^2.70.3",
|
|
175
200
|
"@trpc/server": "^11.24.0",
|
|
176
201
|
"@tsed/core": "^8.45.0",
|
|
@@ -181,28 +206,43 @@
|
|
|
181
206
|
"@tsed/schema": "^8.45.0",
|
|
182
207
|
"@types/autocannon": "^7.12.7",
|
|
183
208
|
"@types/express": "^5.0.6",
|
|
209
|
+
"@types/express-session": "^1.19.0",
|
|
184
210
|
"@types/koa": "^3.0.3",
|
|
185
211
|
"@types/node": "^22.18.0",
|
|
212
|
+
"@types/oidc-provider": "^9.13.1",
|
|
213
|
+
"@types/passport": "^1.0.17",
|
|
214
|
+
"@types/passport-local": "^1.0.38",
|
|
186
215
|
"@types/pg": "^8.15.0",
|
|
187
216
|
"ai": "^7.0.163",
|
|
188
217
|
"astro": "^7.4.7",
|
|
189
218
|
"autocannon": "^8.0.0",
|
|
219
|
+
"better-auth": "^1.7.18",
|
|
190
220
|
"drizzle-orm": "^0.45.4",
|
|
191
221
|
"elysia": "^1.4.30",
|
|
192
222
|
"encore.dev": "^1.58.8",
|
|
193
223
|
"express": "^5.3.0",
|
|
224
|
+
"express-session": "^1.19.0",
|
|
194
225
|
"fastify": "^5.12.8",
|
|
195
226
|
"h3": "^2.0.8",
|
|
196
227
|
"hono": "^4.13.16",
|
|
228
|
+
"knex": "^3.3.0",
|
|
197
229
|
"koa": "^3.3.0",
|
|
230
|
+
"kysely": "^0.29.8",
|
|
198
231
|
"miniflare": "^4.20260730.0",
|
|
199
232
|
"next": "^16.4.5",
|
|
233
|
+
"oidc-provider": "^9.13.3",
|
|
234
|
+
"passport": "^0.7.0",
|
|
235
|
+
"passport-local": "^1.0.0",
|
|
200
236
|
"pg": "^8.16.0",
|
|
237
|
+
"postgres": "^4.1.0",
|
|
238
|
+
"prisma": "^7.10.1",
|
|
201
239
|
"react": "^19.3.1",
|
|
202
240
|
"react-router": "^8.5.1",
|
|
203
241
|
"redis": "^6.3.1",
|
|
204
242
|
"reflect-metadata": "^0.2.2",
|
|
205
243
|
"rxjs": "^7.8.2",
|
|
244
|
+
"sequelize": "^6.37.8",
|
|
245
|
+
"typeorm": "^1.1.2",
|
|
206
246
|
"typescript": "^5.9.3",
|
|
207
247
|
"zod": "^4.6.7"
|
|
208
248
|
},
|
package/templates/app/AGENTS.md
CHANGED
|
@@ -30,11 +30,11 @@ Do not finish until `inferlo check` passes.
|
|
|
30
30
|
## Rules
|
|
31
31
|
|
|
32
32
|
1. **One way to do each thing.** Routes: `route.get/post/put/patch/delete(path, options).handle(fn)`. Validation: `t.*` schemas. Database: `db.<table>.<method>`. AI: `ai.generate` / `ai.stream` / `ai.embed`.
|
|
33
|
-
2. **Routes are private by default.** A route without `auth` needs a signed-in user. Use `auth: "public"` only for routes anyone may call, and `auth: "<role>"` for role checks. Browser sign-in uses cookie sessions: `export const session = sessions({ secret: settings.SESSION_SECRET, user: ({ userId }, { db }) => ... })`, `createApp({ auth: session })`, `await session.signIn(ctx, user.id)` in the login route and `session.signOut(ctx)` in logout; browsers send the CSRF token in `X-CSRF-Token` or a `_csrf` form field on POST, PUT, PATCH and DELETE (403 IL3005 without it).
|
|
34
|
-
3. **Validate every input with a schema.** `body`, `query` and `params` options. Reuse model schemas: `Ticket.pick("title", "body")`, `Ticket.omit("ownerId")`, `.partial()` for PATCH.
|
|
33
|
+
2. **Routes are private by default.** A route without `auth` needs a signed-in user. Use `auth: "public"` only for routes anyone may call, and `auth: "<role>"` for role checks. Browser sign-in uses cookie sessions: `export const session = sessions({ secret: settings.SESSION_SECRET, user: ({ userId }, { db }) => ... })`, `createApp({ auth: session })`, `await session.signIn(ctx, user.id)` in the login route and `session.signOut(ctx)` in logout; browsers send the CSRF token in `X-CSRF-Token` or a `_csrf` form field on POST, PUT, PATCH and DELETE (403 IL3005 without it). Company single sign-on (Okta, Entra ID, Google Workspace, Keycloak) for browser apps is `export const sso = oidc({ issuer, clientId, clientSecret, session, userId: (claims, { db }) => ... })` with `routes: [..., sso]`, ending in that session (sign out with a POST form to `sso.logoutPath` with the `_csrf` field, which also signs out at the provider; Google's `hd` and Entra ID's `domain_hint` go in `authorizationParams`); return `null` from `userId` for unknown users and trust `claims.email` only when `claims.email_verified` is true. APIs whose users sign in with an identity provider (Auth0, Okta, Entra ID, Keycloak, Clerk) check its access tokens with `auth: jwt({ issuer, audience, user: (claims) => ({ id: claims.sub, roles: [] }) })`; never decode or verify JWTs by hand. Apps that sign in with Better Auth keep it and use `const session = betterAuthSession(auth, { user: ({ user }) => ({ id: user.id, email: user.email, roles: [] }) })` from `@browserlessapi/inferlo/better-auth` with `createApp({ auth: session, middleware: [session.middleware] })`; never write a second sign-in system next to it. Apps on Auth.js do the same with `authJsSession(Auth, authConfig)` from `@browserlessapi/inferlo/authjs` (add `id: token.sub` to the session in `callbacks.session`). Express apps on Passport.js mount the app with `toNodeHandler(app)` after `passport.session()` and use `auth: passportSession({ user: (u) => ({ id: u.id, email: u.email, roles: [] }) })` from `@browserlessapi/inferlo/passport`.
|
|
34
|
+
3. **Validate every input with a schema.** `body`, `query` and `params` options. Reuse model schemas: `Ticket.pick("title", "body")`, `Ticket.omit("ownerId")`, `.partial()` for PATCH. File uploads: `body: t.object({ avatar: t.file({ maxBytes: 2_000_000, types: ["image/png", "image/jpeg"] }) })` gives the handler a `File` (clients send `FormData`); several files are `t.array(t.file()).max(10)`. Never put `t.file()` in a model: keep the file with `ctx.storage` (`` await storage.put(`avatars/${user.id}`, body.avatar) ``, needs `createApp({ storage: diskStorage({ dir: "./uploads" }) })`, `memoryStorage()` in tests) and its key in a `t.string()`; build keys from ids, never from the client's file name.
|
|
35
35
|
4. **Check ownership.** Fetch with `db.x.get(id)` (throws 404) and compare `ownerId` to `user.id`; throw `notFound()` when it does not match, so other users' ids are not revealed. When more than one rule decides access (owners, admins, teams), define named permissions once in `src/permissions.ts` with `export const access = permissions({ "notes.update": ({ user, note }) => note.ownerId === user.id || (user.roles ?? []).includes("admin") })`, pass `permissions: access` to `createApp`, and call `await access.authorize("notes.update", { user, note })` in routes and jobs (403 IL3004 when denied). Permissions that need only the user go on the route: `{ permission: "reports.view" }`; add `permissions: typeof access` to `Register` (with `import type`) so permission names are type-checked. When changes must be on record (who changed what), add `AuditLog` to the models and call `await audit("notes.update", { model: "notes", before: note, after: updated })` after the change, in routes and jobs (jobs pass `userId`).
|
|
36
36
|
5. **Related rows:** a `t.ref` field named `<name>Id` is a relation: `db.notes.findMany({ include: { owner: true } })` adds `note.owner` with one extra query, and `db.users.findMany({ include: { notes: true } })` adds `user.notes` (the rows that point to each user). Never load related rows in a loop. For lists clients page through, use `db.notes.paginate({ where, orderBy: { createdAt: "desc" }, cursor: query.cursor, limit: query.limit })`, which returns `{ items, nextCursor }`.
|
|
37
|
-
6. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders. Code that takes a pg client (Drizzle, plain `pg`) gets it from `db.$pgClient()`, and inside `db.$transaction(async (tx) => ...)` from `tx.$pgClient()`, so it runs in the same transaction.
|
|
37
|
+
6. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders. Code that takes a pg client (Drizzle, Kysely, plain `pg`, and Knex queries with `.connection(client)`) gets it from `db.$pgClient()`, and inside `db.$transaction(async (tx) => ...)` from `tx.$pgClient()`, so it runs in the same transaction. Apps on postgres.js run Inferlo on their `sql` instance with `postgres({ pool: postgresJsPool(sql) })` from `@browserlessapi/inferlo/postgres-js` (never a second pool), and inside a transaction use `const { sql } = await tx.$pgClient<PostgresJsClient<ReservedSql>>()`. Prisma, TypeORM, Sequelize and MikroORM cannot join an Inferlo transaction: for a write across a Prisma, TypeORM, Sequelize or MikroORM table and an Inferlo model, use `db.$transaction` and write the other table with `tx.$sql`. Apps on MikroORM must set `schemaGenerator: { skipTables: [...models.map((m) => m.name), /^inferlo_/, "audit_log"] }`, or MikroORM's schema update drops Inferlo's tables.
|
|
38
38
|
7. **Model ids, timestamps:** every model already has `id`, `createdAt`, `updatedAt`. Never define them or send them as input.
|
|
39
39
|
8. **New field?** Add it to the model in `src/models.ts`, then run `npx inferlo db push` in development, or `npx inferlo db migrate <name>` to write a migration file for production (applied with `npx inferlo db migrate --apply`). Use `.optional()` or `.default()` for new fields on existing tables. Never edit `migrations/snapshot.json`, and never edit a migration file that has been applied (`--apply` warns with IL4018 and does not run the change): put changes in a new migration. Removing a model or field needs `npx inferlo db migrate <name> --allow-drop` (it deletes that data); never use it for a rename. Changing a field's type or default, making it required or optional, or changing an enum's values needs `--allow-change`; give a newly required field a `.default()` so existing NULL rows get a value, and before removing an enum value add an `UPDATE` in the new migration file (before its `ADD CONSTRAINT`) that moves rows off it (IL4021).
|
|
40
40
|
9. **New resource?** `npx inferlo add resource <plural_name>` generates the model and CRUD routes. Then edit them.
|
|
@@ -50,6 +50,8 @@ Do not finish until `inferlo check` passes.
|
|
|
50
50
|
19. **Caching:** for results that are slow or expensive to compute, use the built-in cache instead of a module-level `Map`: `` return cache.getOrSet(`notes:stats:${user.id}`, { ttlMs: 60_000, tags: ["notes"] }, () => computeStats(db, user.id)) `` in routes and jobs (`ctx.cache`), and `await cache.invalidate("notes")` after notes change. `ttlMs` is required; put the user id in the key of anything that differs per user; values come back as JSON (dates as strings). To cache a whole GET route instead, add the route option `cache: { ttlMs: 60_000, tags: ["notes"] }` (one response per user by default; `by: "public"` only for data that is the same for everyone) and invalidate the tag where the data changes. With several instances, use `createApp({ cache: { store: "database" } })`.
|
|
51
51
|
20. **Calling another Inferlo service:** `const tickets = serviceClient({ baseUrl: settings.TICKETS_URL, token: settings.TICKETS_TOKEN })`, then in a handler `await tickets.call(getTicket, { params: { id }, ctx })` with the other service's route imported, so the call is typed. Catch `ServiceCallError` and check `error.remoteStatus` for answers you expect (404). Never build these URLs with `fetch` by hand. When the app uses `tracing: { api: otel }`, pass the same `tracing` to `serviceClient` so calls join the trace.
|
|
52
52
|
21. **Data of many organisations (multi-tenant):** give the signed-in user a `tenantId` (the organisation's id) and make tables whose rows belong to one organisation tenant-scoped: `model("projects", { ... }, { tenant: true })`. `ctx.db` then filters every query by and stamps every insert with the user's tenant, so never add `tenantId` to a `where`, a body schema or an insert. Jobs queued (or events published) by a request run for the request's tenant: their `ctx.db` is scoped the same way, so do not pass the tenant id in the job input. In scripts use `app.db.$tenant(tenantId).projects`, and `app.jobs.enqueue(job, input, { tenantId })`. For limits and caches shared by an organisation use `rateLimit: { max, per, by: "tenant" }` and `cache: { ttlMs, by: "tenant" }`; `ctx.audit` records the tenant itself.
|
|
53
|
+
22. **Live updates over WebSockets:** `export const chat = route.ws("/chat", { message: t.object({ text: t.string().max(2000) }), rateLimit: { max: 60, per: "minute" } }).handle(async ({ message, user, db, socket }) => ...)` in a routes file, registered like other routes. Return a value to reply with JSON, or call `socket.send(data)` for several messages; stream a model's answer with `await socket.stream(ai.stream({ model: "fast", prompt, for: user.id }))`. Auth works as for HTTP routes (browsers authenticate with cookie sessions); never add a second WebSocket server or library. For live updates, ``socket.join(`project:${id}`)`` in the WebSocket handler (after checking the user may see it) and ``await sockets.broadcast(`project:${id}`, data)`` from any route, WebSocket handler or job; rooms are per tenant, and with more than one instance add `sockets: { transport: "redis", subscriber: await redis.duplicate().connect() }` next to `redis` in createApp. Test with `await app.listen(0)` and `new WebSocket(url)`.
|
|
54
|
+
23. **Response schemas:** give routes whose answer clients rely on a `response` schema (`route.get("/me", { response: t.object({ id: t.uuid(), email: t.email(), name: t.string() }) })`) and return exactly those fields, never a whole row. Outside production Inferlo checks answers against it and logs IL2016 for a missing, wrong or extra field; tests use `createApp({ validateResponses: "error" })` so a mismatch fails.
|
|
53
55
|
|
|
54
56
|
## Testing
|
|
55
57
|
|
|
@@ -11,6 +11,8 @@ function makeApp() {
|
|
|
11
11
|
routes: [notes],
|
|
12
12
|
db: memory(),
|
|
13
13
|
auth: { resolve: () => null },
|
|
14
|
+
// An answer that does not match its route's response schema fails the test (500 IL2016).
|
|
15
|
+
validateResponses: "error",
|
|
14
16
|
ai: { providers: { mock: mockProvider({ respond: () => '{"summary":"A short note."}' }) }, models: { fast: "mock:test" } },
|
|
15
17
|
});
|
|
16
18
|
}
|