Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7c178da
feat(ai): embedding provider contract + MockEmbeddingProvider (WS-AI-…
Arcoders Jul 2, 2026
a816b3f
feat(ai): OpenAI-compatible embedding provider + wire parser (WS-AI-3…
Arcoders Jul 2, 2026
2b6b0ba
feat(ai): per-tenant embeddings migration + build wiring + drift guar…
Arcoders Jul 2, 2026
5ce244c
feat(ai): VectorStoreService — driver-aware, sealed, rowscope-refusin…
Arcoders Jul 2, 2026
5cbad75
feat(ai): EmbeddingIngestionService — fail-closed reserve/embed/store…
Arcoders Jul 2, 2026
08f5261
feat(ai): the /ai/embed ingest choke point + embed audit event (WS-AI…
Arcoders Jul 2, 2026
480f061
feat(ai): wire the vector store + config validation + pgvector provis…
Arcoders Jul 2, 2026
7b34e53
test(ai): the I1 vector-placement guard + an AI no_unsafe_raw_sql gua…
Arcoders Jul 2, 2026
ddd4641
docs(ai): the vector store guide + CHANGELOG (WS-AI-3, C9)
Arcoders Jul 2, 2026
867b993
style(ai): prettier-format the WS-AI-3 embedding specs
Arcoders Jul 2, 2026
0428426
fix(ai): harden the vector store per the WS-AI-3 adversarial review
Arcoders Jul 2, 2026
061c102
feat(ai): the retrievalFilter gate + retrieval contracts (WS-AI-5, C1)
Arcoders Jul 2, 2026
3f9b2bb
feat(ai): the vector-store scope filter + RetrievalService + retrieva…
Arcoders Jul 2, 2026
f10e775
feat(ai): the retrieved-context builder (#10) + the I4/I8 invariant g…
Arcoders Jul 2, 2026
e46395f
feat(ai): the POST /ai/retrieve route + controller + audit event (WS-…
Arcoders Jul 2, 2026
04cc218
feat(ai): wire opt-in RAG into /ai/chat (WS-AI-5, C5)
Arcoders Jul 2, 2026
18ae432
test(ai): the real-pgvector RAG retrieval two-tenant + filter isolati…
Arcoders Jul 2, 2026
d0d35cc
docs(ai): the Retrieval (RAG) guide section + CHANGELOG (WS-AI-5, C7)
Arcoders Jul 2, 2026
e107b8f
style(ai): prettier-format the WS-AI-5 changes + fix a routes shadow …
Arcoders Jul 2, 2026
c573bbe
feat(ai): make the default retrieval ACL fail-closed (WS-AI-5, C9)
Arcoders Jul 2, 2026
afb9dce
fix(ai): resolve the RAG-chat document ACL before the rate limiter (W…
Arcoders Jul 2, 2026
ccd6128
feat(ai): the append-only AI audit table + I5 guard (WS-AI-7, C1)
Arcoders Jul 3, 2026
96b2552
feat(ai): the fail-closed hash-chained AI audit writer + guard (WS-AI…
Arcoders Jul 3, 2026
c00c035
feat(ai): the three PG audit sinks + persisted-row non-PII proof (WS-…
Arcoders Jul 3, 2026
323cc4d
feat(ai): wire the audit sinks fail-closed + config + ai_audit doctor…
Arcoders Jul 3, 2026
e63f38f
feat(ai): external audit anchoring via the kernel destination registr…
Arcoders Jul 3, 2026
f343498
feat(ai): the tenant:ai:audit:verify chain-verification command (WS-A…
Arcoders Jul 3, 2026
43244dc
test(ai): real-PG immutability/tamper/isolation/resilience + bounded-…
Arcoders Jul 3, 2026
11ed239
docs(ai): the Audit guide section + CHANGELOG (WS-AI-7, C8)
Arcoders Jul 3, 2026
0930f69
style(ai): prettier-format the WS-AI-7 changes + fix a verify no-shad…
Arcoders Jul 3, 2026
54d9c56
feat(ai): the config.ai.memory block + aiConversationMemory secret cl…
Arcoders Jul 3, 2026
b04a449
feat(ai): ConversationMemoryService — Redis-LIST store, HMAC sessions…
Arcoders Jul 3, 2026
4d1b4fa
feat(ai): wire memory into /ai/chat — session mint/validate, replay, …
Arcoders Jul 3, 2026
157c52b
feat(ai): the check-ai-invariant-2 structural guard for I2 (WS-AI-4, C4)
Arcoders Jul 3, 2026
0a2f778
test(ai): memory service/context/doctor/flow units + real-Redis isola…
Arcoders Jul 3, 2026
081107a
docs(ai): the Conversation memory guide section + CHANGELOG (WS-AI-4,…
Arcoders Jul 3, 2026
8f1261e
feat(ai): WS-AI-9 compliance — GDPR purge, data residency, no-train g…
Arcoders Jul 3, 2026
b1b5ffd
feat(ai): WS-AI-8 chaos/E2E/enterprise resilience tier + two additive…
Arcoders Jul 3, 2026
5086883
docs(ai): WS-AI-10 AI security & threat-model page + guide lead corre…
Arcoders Jul 3, 2026
0c3e8f4
feat(ai): optional redactOutput seam + OWASP LLM 2025 mapping (WS-AI-…
Arcoders Jul 3, 2026
c3ded21
ci: TEMP run full CI on the WS-AI 1.0 branch (revert before merge)
Arcoders Jul 3, 2026
eaf9af8
fix(core,ai): install pgvector into a dedicated extensions schema so …
Arcoders Jul 3, 2026
11788c0
fix(core,ai): rollback folds satellite migrations; cap-test isolation…
Arcoders Jul 3, 2026
5c7e107
test(ai): surface the /ai/embed response body when the two-tenant e2e…
Arcoders Jul 3, 2026
76b7abf
fix(ai,demo): provision the demo AI audit table + wrap the chat rate-…
Arcoders Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ name: CI

on:
push:
branches: [master, main]
# TEMP (WS-AI 1.0 branch review): run the full CI matrix on this feature
# branch so the whole suite is exercised on the branch before the single 1.0
# PR is opened. REVERT this extra branch entry before merging to master.
branches: [master, main, LASAGNA-020726/ws-ai-3-vector-store]
pull_request:
branches: [master, main]

Expand Down Expand Up @@ -706,7 +709,10 @@ jobs:

services:
postgres:
image: postgres:16-alpine
# pgvector image (drop-in superset of postgres:16) so the AI e2e that need
# the `vector` extension run here; the non-AI demo e2e are unaffected, and
# the AI e2e self-skip on any plain-postgres runner (WS-AI-8).
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const sidebar = [
{ text: 'Metrics', link: '/guides/satellites/metrics' },
{ text: 'Reporting', link: '/guides/satellites/reporting' },
{ text: 'AI', link: '/guides/satellites/ai' },
{ text: 'AI security', link: '/guides/satellites/ai-security' },
{ text: 'Quotas', link: '/guides/satellites/quotas' },
{ text: 'Billing', link: '/guides/satellites/billing' },
{ text: 'Backup', link: '/guides/satellites/backup' },
Expand Down
310 changes: 310 additions & 0 deletions docs/guides/satellites/ai-security.md

Large diffs are not rendered by default.

330 changes: 318 additions & 12 deletions docs/guides/satellites/ai.md

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions docs/guides/satellites/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,16 @@ To build your own, see [Creating a satellite](/guides/cookbook/creating-a-satell

<Callout type="note" title="Also documented in this section">
[Backup](/guides/satellites/backup), [Admin](/guides/satellites/admin),
[Reporting](/guides/satellites/reporting) and [AI](/guides/satellites/ai) appear in
[Reporting](/guides/satellites/reporting), [AI](/guides/satellites/ai) and
[AI security](/guides/satellites/ai-security) appear in
this section's sidebar but aren't tenant-attached feature satellites like the ten
above. Backup is an operational concern (`pg_dump` with retention tiers, shipped as
`@adonisjs-lasagna/backup`); Admin is the shared REST surface the satellites expose,
not a feature of its own; Reporting is the cross-tenant analytics layer that
aggregates what the metrics pipeline writes into the backoffice schema; AI is the
multi-provider streaming spine (`@adonisjs-lasagna/ai`) and ships no tenant table in
this release. They live here because that's where you'll look for them.
this release; AI security is that satellite's consolidated threat model and hardening
checklist. They live here because that's where you'll look for them.
</Callout>

## Cross-satellite invariants
Expand Down
1 change: 1 addition & 0 deletions docs/guides/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ guarantee table above is a good reference). We will acknowledge within
## Related

- [Compliance (SOC2 & GDPR)](/guides/compliance); how these guarantees map to SOC2/GDPR/ISO/HIPAA controls, plus audit export, anonymization, and the posture report.
- [AI security & threat model](/guides/satellites/ai-security); the AI satellite's 18 threat vectors, invariants, fail-closed postures, and hardening checklist.
- [Concepts](/start/concepts); connection routing, schema model, the boundary the guarantees sit on.
- [Audit logs](/guides/satellites/audit); append-only enforcement details.
- [Quotas](/guides/satellites/quotas); atomic enforcement details.
Expand Down
25 changes: 21 additions & 4 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,22 @@ For hosts using the vector store (embeddings), the PostgreSQL `vector`
extension must exist before any migration declares a `vector(N)` column.
`CREATE EXTENSION` needs a privileged role, so it runs under a separate
provisioning connection (`isolation.provisionConnectionName`), never the app's
request-serving role.
request-serving role. The extension is installed into a dedicated `extensions`
schema, which `schema-pg` tenant connections append to their `search_path` after
the tenant's own schema — so a bare `vector(N)` column and its operators resolve
while `public` (which holds central-connection data) stays off the tenant path,
keeping physical tenant isolation (I1) intact. A hand-rolled tenant model that
registers its own connection must append that schema too (see the demo's
`app/models/backoffice/tenant.ts`).

| Command | What it does |
|---|---|
| `tenant:vector:provision` | Install the pgvector extension idempotently under the privileged provisioning connection. Dispatched by driver: once on the shared database for `schema-pg`/`rowscope-pg`, per tenant database for `database-pg` (honouring `--tenant`). Run it before any embeddings migration; it doubles as the backfill for existing databases. `--dry-run` previews. |
| `tenant:vector:provision` | Install the pgvector extension idempotently, into the dedicated `extensions` schema, under the privileged provisioning connection. Dispatched by driver: once on the shared database for `schema-pg`/`rowscope-pg`, per tenant database for `database-pg` (honouring `--tenant`). Run it before any embeddings migration; it doubles as the backfill for existing databases. `--dry-run` previews. |

The opt-in `pgvector_extension` doctor check verifies the app role is not a
superuser and that the extension is present where embeddings live. Register it
with `doctorService.register(pgvectorExtensionCheck)`, then it runs under
superuser and that the extension is present in the `extensions` schema where
embeddings resolve it. Register it with
`doctorService.register(pgvectorExtensionCheck)`, then it runs under
`tenant:doctor --check=pgvector_extension`.

The requirement is **ordering, not locking**: provision before you migrate a
Expand Down Expand Up @@ -193,6 +200,16 @@ Available when `--with=reporting` is configured. Full reference in the
|---|---|
| `tenant:report:generate` | Generate and print a cross-tenant usage report. Flags: `--period=day\|week\|month`, `--since=<iso>`, `--until=<iso>`, `--top=<n>`, `--format=table\|json\|csv`, `--out=<file>`, `--extension=<name>` (run a registered report extension instead of the built-in). |

## AI

Available when `--with=ai` is configured. Full reference in the
[AI satellite](/guides/satellites/ai#audit).

| Command | What it does |
|---|---|
| `tenant:ai:audit:verify` | Re-walk the append-only AI audit hash chain and report the first tamper (a broken checksum, a `seq` gap, or a broken prev-link) that got past the DB triggers. Exit 1 on the first break, so it gates a cron or a post-incident check. Flags: `--tenant=<id>` (omit for all), `--json`. |
| `tenant:ai:purge` | Erase a tenant's AI data for GDPR: conversation memory, the response-cache epoch, and embeddings. Scopes: `--tenant=<uuid> --force` (all), `--tenant=<uuid> --principal=<id>` (one user, Art.17), `--tenant=<uuid> --source=<key>` (one document). `--dry-run` previews the counts and writes nothing; `--verify-chain` also re-walks the audit chain; `--actor=<id>` sets the audited operator. The immutable, non-PII audit chain intentionally survives. |

## REPL

```bash
Expand Down
2 changes: 2 additions & 0 deletions examples/api/adonisrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineConfig({
() => import('@adonisjs-lasagna/backup/commands'),
() => import('@adonisjs-lasagna/billing/commands'),
() => import('@adonisjs-lasagna/reporting/commands'),
() => import('@adonisjs-lasagna/ai/commands'),
],

providers: [
Expand All @@ -28,6 +29,7 @@ export default defineConfig({
() => import('@adonisjs-lasagna/billing/provider'),
() => import('@adonisjs-lasagna/websockets/provider'),
() => import('@adonisjs-lasagna/reporting/provider'),
() => import('@adonisjs-lasagna/ai/provider'),
() => import('#app/providers/app_provider'),
],

Expand Down
11 changes: 9 additions & 2 deletions examples/api/app/models/backoffice/tenant.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { BackofficeBaseModel } from '@adonisjs-lasagna/saas-tenancy/base-models'
import { ReadReplicaService } from '@adonisjs-lasagna/saas-tenancy/services'
import {
ReadReplicaService,
PGVECTOR_EXTENSION_SCHEMA,
} from '@adonisjs-lasagna/saas-tenancy/services'
import { column, scope } from '@adonisjs/lucid/orm'
import db from '@adonisjs/lucid/services/db'
import app from '@adonisjs/core/services/app'
Expand Down Expand Up @@ -153,7 +156,11 @@ export default class Tenant extends BackofficeBaseModel {

db.manager.add(this.connectionName, {
...config,
searchPath: [this.schemaName],
// The tenant schema stays FIRST (all tenant objects resolve there); the shared
// pgvector `extensions` schema is appended so the `ai_embeddings vector(N)`
// column + operators resolve, exactly like core's SchemaPgDriver. `public`
// (central data) is deliberately kept off the tenant path.
searchPath: [this.schemaName, PGVECTOR_EXTENSION_SCHEMA],
} as PostgreConfig)

connectionLru.set(this.connectionName, Date.now())
Expand Down
33 changes: 33 additions & 0 deletions examples/api/app/providers/app_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
} from '@adonisjs-lasagna/reporting'
import type { ReportExtensionFilters } from '@adonisjs-lasagna/reporting'
import { adminActionRegistry, ADMIN_CONTRACT_VERSION } from '@adonisjs-lasagna/admin'
import { AIProviderRegistry, EmbeddingProviderRegistry } from '@adonisjs-lasagna/ai'
import { MockAIProvider, MockEmbeddingProvider } from '@adonisjs-lasagna/ai/testing'
import TenantRepository from '#app/repositories/tenant_repository'

export default class AppProvider {
Expand All @@ -24,6 +26,37 @@ export default class AppProvider {
this.bindContainerServices()
await this.registerReportExtensions()
this.registerAdminActions()
await this.registerAiMockProviders()
}

/**
* Register the offline mock AI providers so the demo (and the AI e2e suite) run
* with no network. `AiProvider.register()` runs before this provider's `boot()`
* (it is listed earlier in adonisrc), so both registries are bound and makeable
* here. The chat mock activates as the `mock` provider named in `config.ai`; the
* embedding mock overrides the configured default via the WS-AI-8 registry so
* `/ai/embed` and `/ai/retrieve` never dial a real embeddings endpoint.
*/
private async registerAiMockProviders() {
const chat = await this.app.container.make(AIProviderRegistry)
if (!chat.has('mock'))
chat.register(
// Emit a fake PII token in the streamed output so the demo
// `config.ai.redactOutput` DLP hook has something to strip end to end
// (proven by the ai_output_redaction e2e). Normal prose is preserved; only
// the SSN-shaped token is redacted.
new MockAIProvider({
name: 'mock',
fragments: [
{ data: 'The record is ', tokens: 1 },
{ data: 'SSN-000-00-0000', tokens: 1 },
],
}),
{ activate: true }
)

const embedding = await this.app.container.make(EmbeddingProviderRegistry)
if (!embedding.has()) embedding.register(new MockEmbeddingProvider({ dimension: 8 }))
}

/**
Expand Down
33 changes: 33 additions & 0 deletions examples/api/config/multitenancy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,37 @@ export default {
rollups: { enabled: true },
cache: { invalidateOnFlush: true },
},

// ─── AI satellite (@adonisjs-lasagna/ai) ─────────────────────────
// Fully offline in the demo: AppProvider registers a MockAIProvider (chat) and a
// MockEmbeddingProvider (embed/retrieve) so the AI e2e run with no network. The
// `dimension: 8` here is baked into the per-tenant `ai_embeddings vector(8)` column
// by the satellite migration and matches the mock's vectors. `retrievalFilter`
// returns the tenant-wide scope (isolation stays structural, per-tenant schema);
// `authorizeAIAccess` is open for the demo; `resolvePrincipal` reads `x-ai-user`.
// The AI routes are mounted under TenantGuard in start/routes.ts. `baseUrl` is an
// unreachable https placeholder: the mock override means it is never dialed. The
// AI e2e self-skip unless Postgres has the pgvector extension.
ai: {
allowedProviders: ['mock'],
defaultProvider: 'mock',
authorizeAIAccess: () => true,
resolvePrincipal: (ctx: any) => ctx.request.header('x-ai-user') ?? null,
rateLimit: { limit: 5, windowSeconds: 60 },
audit: { enabled: true },
embedding: {
provider: 'mock-embedding',
apiKey: 'demo-embeddings-key',
baseUrl: 'https://embeddings.invalid',
dimension: 8,
authorizeIngestion: () => true,
},
retrieval: { retrievalFilter: () => ({ kind: 'all' as const }) },
// A demo output-redaction (DLP) hook: strip anything shaped like an SSN from
// the model's streamed output. Host-owned defense-in-depth, NEVER the isolation
// control (I4/I8 remain the guarantee); the mandatory output bound still runs.
// Proven end to end by the ai_output_redaction e2e.
redactOutput: (_ctx: any, _tenant: any, chunk: string) =>
chunk.replace(/SSN-\d{3}-\d{2}-\d{4}/g, '[redacted]'),
},
} as const
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { BaseSchema } from '@adonisjs/lucid/schema'

/**
* The dedicated AI audit table (WS-AI-7). The demo enables `config.ai.audit`, so
* the AI gateway writes a non-PII, append-only, hash-chained row per chat /
* embedding / retrieval action; `/ai/embed` and `/ai/retrieve` fail CLOSED when
* this write cannot land, so the table must exist. This mirrors the satellite's
* `create_ai_audit_logs_table.stub` (the source of truth) that `configure` copies
* into a host app; the demo carries it as a real backoffice migration so the AI
* e2e run against a provisioned audit chain.
*/
export default class extends BaseSchema {
protected tableName = 'ai_audit_logs'

async up() {
this.schema.withSchema('backoffice').createTable(this.tableName, (table) => {
table.uuid('id').primary().defaultTo(this.db.rawQuery('gen_random_uuid()').knexQuery)
table.uuid('tenant_id').notNullable()

// Per-tenant monotonic sequence + hash chain. `checksum` is computed in the
// writer as sha256(canonical(row, seq) + '\n' + prev_checksum), so a
// deletion, reorder, or in-place rewrite that slips past the triggers breaks
// the chain and `tenant:ai:audit:verify` reports it. `UNIQUE(tenant_id, seq)`
// backs the advisory-locked writer.
table.bigInteger('seq').notNullable()
table.specificType('checksum', 'char(64)').notNullable()
table.specificType('prev_checksum', 'char(64)').nullable()

// Non-PII attribution only (I5): principal and source are one-way SHA-256
// hashes; no prompt, response, query, or document text is ever stored. `op`
// discriminates the three choke points (chat / embedding / retrieval) whose
// frozen events map onto this shared row.
table.string('op').notNullable()
table.string('outcome').notNullable()
table.string('reason').nullable()
table.specificType('principal_hash', 'char(64)').nullable()
table.specificType('source_hash', 'char(64)').nullable()
table.string('provider').nullable()
table.string('model').nullable()
table.integer('tokens').notNullable().defaultTo(0)
table.integer('fragments').notNullable().defaultTo(0)
table.integer('embeddings_count').notNullable().defaultTo(0)
table.integer('dimension').notNullable().defaultTo(0)
table.integer('match_count').notNullable().defaultTo(0)
table.boolean('idempotent_replay').notNullable().defaultTo(false)

table.timestamp('occurred_at', { useTz: true }).notNullable()
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(this.now())

table.unique(['tenant_id', 'seq'], 'ai_audit_logs_tenant_seq_uq')
table.index(['tenant_id', 'created_at'], 'ai_audit_logs_tenant_created_idx')
})

// Append-only, enforced at the database level so a compromised tenant role or a
// buggy controller cannot rewrite or erase evidence. The triggers fire on every
// UPDATE/DELETE regardless of role; TRUNCATE needs its own statement-level guard.
this.defer(async (db) => {
await db.rawQuery(`
CREATE OR REPLACE FUNCTION backoffice.ai_audit_logs_no_mutate()
RETURNS TRIGGER AS $$
BEGIN
RAISE EXCEPTION 'ai_audit_logs is append-only; UPDATE/DELETE is forbidden'
USING ERRCODE = 'insufficient_privilege';
END;
$$ LANGUAGE plpgsql;
`)
await db.rawQuery(`
DROP TRIGGER IF EXISTS ai_audit_logs_no_update ON backoffice.ai_audit_logs;
CREATE TRIGGER ai_audit_logs_no_update
BEFORE UPDATE ON backoffice.ai_audit_logs
FOR EACH ROW EXECUTE FUNCTION backoffice.ai_audit_logs_no_mutate();
`)
await db.rawQuery(`
DROP TRIGGER IF EXISTS ai_audit_logs_no_delete ON backoffice.ai_audit_logs;
CREATE TRIGGER ai_audit_logs_no_delete
BEFORE DELETE ON backoffice.ai_audit_logs
FOR EACH ROW EXECUTE FUNCTION backoffice.ai_audit_logs_no_mutate();
`)
await db.rawQuery(`
DROP TRIGGER IF EXISTS ai_audit_logs_no_truncate ON backoffice.ai_audit_logs;
CREATE TRIGGER ai_audit_logs_no_truncate
BEFORE TRUNCATE ON backoffice.ai_audit_logs
FOR EACH STATEMENT EXECUTE FUNCTION backoffice.ai_audit_logs_no_mutate();
`)
})
}

async down() {
this.defer(async (db) => {
await db.rawQuery(
'DROP TRIGGER IF EXISTS ai_audit_logs_no_update ON backoffice.ai_audit_logs'
)
await db.rawQuery(
'DROP TRIGGER IF EXISTS ai_audit_logs_no_delete ON backoffice.ai_audit_logs'
)
await db.rawQuery(
'DROP TRIGGER IF EXISTS ai_audit_logs_no_truncate ON backoffice.ai_audit_logs'
)
await db.rawQuery('DROP FUNCTION IF EXISTS backoffice.ai_audit_logs_no_mutate()')
})
this.schema.withSchema('backoffice').dropTable(this.tableName)
}
}
1 change: 1 addition & 0 deletions examples/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"@adonisjs-lasagna/saas-tenancy": "*",
"@adonisjs-lasagna/admin": "*",
"@adonisjs-lasagna/ai": "*",
"@adonisjs-lasagna/billing": "*",
"@adonisjs-lasagna/sso": "*",
"@adonisjs-lasagna/backup": "*",
Expand Down
7 changes: 7 additions & 0 deletions examples/api/start/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { multitenancyRoutes } from '@adonisjs-lasagna/saas-tenancy/health'
import { multitenancyAdminRoutes } from '@adonisjs-lasagna/admin'
import { multitenancyBillingRoutes } from '@adonisjs-lasagna/billing'
import { multitenancyReportingRoutes } from '@adonisjs-lasagna/reporting'
import { multitenancyAiRoutes } from '@adonisjs-lasagna/ai/routes'

/**
* Lazy controller imports — keeps the route file small and lets the
Expand Down Expand Up @@ -59,6 +60,12 @@ multitenancyReportingRoutes({
cacheTtlMs: 60_000,
})

/* ─── AI gateway (@adonisjs-lasagna/ai) — tenant-scoped, fail-closed mount ── */
// POST /ai/chat (SSE), /ai/embed, /ai/retrieve. TenantGuard FIRST per the mount
// contract; config.ai.authorizeAIAccess is the per-request membership gate. The
// demo runs fully offline through the mock providers registered in AppProvider.
multitenancyAiRoutes({ middleware: [middleware.tenantGuard()] })

/* ─── Impersonation verify probe ─────────────────────────────────────────── */
// Echoes the verified impersonation context attached by ImpersonationMiddleware,
// so a request carrying a token minted via the admin API can prove it resolves
Expand Down
Loading
Loading