Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6ed20e7
test(core): scope no_lenient_decrypt_on_secrets to bare encrypt/decry…
Arcoders Jul 7, 2026
951709e
feat(core): plugin platform Lote A — definePlugin facade + request-pa…
Arcoders Jul 7, 2026
07e218a
refactor(reporting,template): adopt definePlugin and widen the ABI-bo…
Arcoders Jul 7, 2026
83eebc9
test(core): E4/E6 plugin fail-closed matrix + hostile-identifier fuzz…
Arcoders Jul 7, 2026
2ca4b26
feat(core): E8 typed section builders for the plugin surface
Arcoders Jul 7, 2026
1edf8b7
test(core): E7 per-entry api-extractor goldens + /plugin surface hygi…
Arcoders Jul 7, 2026
7faa625
test(crypto): rename shadowed local in worm_ledger spec (no-shadow)
Arcoders Jul 7, 2026
4ed15d8
feat(core): S-0 plugin execution scope primitive (Lote S foundation)
Arcoders Jul 7, 2026
057a581
feat(core): S-1a plugin permission model (declare + consent foundation)
Arcoders Jul 7, 2026
474680f
feat(core): S-1b permission coherence guard + fail-closed install con…
Arcoders Jul 7, 2026
c970ebb
docs(core): S-1c plugin permissions + install consent (closes S-1)
Arcoders Jul 7, 2026
4b2b19b
feat(core): S-2 supply-chain — native-addon install gate + lasagna:he…
Arcoders Jul 7, 2026
4b7c988
feat(core): S-3a read-only routing for untrusted plugins (adapter + c…
Arcoders Jul 7, 2026
8109d66
test(core): S-3b plugin_ro role + read-only firewall red-team (CI-pro…
Arcoders Jul 7, 2026
22b7501
feat(core): S-4 worker sandbox machinery + native-addon boot guard
Arcoders Jul 8, 2026
42450a0
feat(core): S-5 in-process friction — core-access proxies + capabilit…
Arcoders Jul 8, 2026
1dd983d
feat(core): S-6 plugin:doctor + plugin_extension_identifier guard + t…
Arcoders Jul 8, 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
6 changes: 6 additions & 0 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ Reports against the core isolation guarantees (cross-tenant read or write
leakage, resolver hijack, fail-open under dependency outage) are highest
priority. See the [stability matrix](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/reference/stability)
for the per-feature labels.

An installed plugin/satellite runs in-process with full privilege by design, so an
**in-process plugin sandbox escape is not a goal** — the in-process trust controls are
friction, and the hard boundary for an untrusted plugin's write is the read-only
Postgres role (S3). The [plugin platform trust boundary](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/guides/security#plugin-platform-trust-boundary)
in the security guide states the five-layer model and what each layer does and does not contain.
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,11 @@ jobs:
# fixture's `rls_probe` connection picks these up.
RLS_DB_USER: rls_ci
RLS_DB_PASSWORD: rls_ci
# Least-privilege SELECT-only role for the S3 plugin read-only proof. The
# fixture's `plugin_ro` connection picks these up; the role is created below
# with `default_transaction_read_only = on` so a write is denied by Postgres.
PLUGIN_RO_DB_USER: plugin_ro
PLUGIN_RO_DB_PASSWORD: plugin_ro

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
Expand Down Expand Up @@ -564,6 +569,27 @@ jobs:
GRANT USAGE ON SCHEMA public TO rls_ci;
SQL

# Least-privilege SELECT-only role for the S3 read-only firewall proof. It
# is NOSUPERUSER NOBYPASSRLS and — the point — `default_transaction_read_only
# = on`, so a write is denied by Postgres even when the role is granted
# INSERT. The fixture's `plugin_ro` connection authenticates as it (via
# PLUGIN_RO_DB_USER/PLUGIN_RO_DB_PASSWORD); the proof self-skips when the
# connection resolves to a writable role (the local default).
- name: Create least-privilege plugin read-only role
env:
PGPASSWORD: postgres
run: |
psql -h 127.0.0.1 -U postgres -d multitenancy_test -v ON_ERROR_STOP=1 <<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'plugin_ro') THEN
CREATE ROLE plugin_ro LOGIN PASSWORD 'plugin_ro' NOSUPERUSER NOBYPASSRLS;
END IF;
END $$;
GRANT CONNECT ON DATABASE multitenancy_test TO plugin_ro;
GRANT USAGE ON SCHEMA public TO plugin_ro;
ALTER ROLE plugin_ro SET default_transaction_read_only = on;
SQL

- name: Test (integration) + coverage
run: npm run test:integration:coverage

Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const sidebar = [
{ text: 'Read replicas', link: '/guides/read-replicas' },
{ text: 'Contextual logging', link: '/guides/contextual-logging' },
{ text: 'Testing', link: '/guides/testing' },
{ text: 'Building a plugin', link: '/guides/plugins' },
{
text: 'Recipes',
link: '/guides/cookbook/',
Expand Down
46 changes: 29 additions & 17 deletions docs/guides/cookbook/creating-a-satellite.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,38 +126,50 @@ surface, and versioned independently of core's published version.
- you **omit** it: it warns that compatibility is unverified.

Set it to the value of `SATELLITE_API_VERSION` you developed against (today, `1`).
You can also assert it at runtime in your provider's `boot()` via
`checkSatelliteApiCompat(...)` from `/sdk`.
The `definePlugin` facade asserts it at runtime for you from the `satelliteApi`
field; a hand-written provider calls `assertSatelliteApiCompatAtBoot(...)` from
`/sdk` itself in `boot()`.

## The provider

Author the provider with the [`definePlugin` facade](/guides/plugins): declare what
the satellite adds and the facade wires the ABI backstops and the request-path
seams for you. This is the shape the reference template ships.

```ts
// providers/my_provider.ts
import type { ApplicationService } from '@adonisjs/core/types'
import { definePlugin, LASAGNA_PLUGIN_API_VERSION } from '@adonisjs-lasagna/saas-tenancy/plugin'
import { HookRegistry } from '@adonisjs-lasagna/saas-tenancy/services'
import type { SatelliteProviderContract } from '@adonisjs-lasagna/saas-tenancy/sdk'
import MyService from '../src/my_service.js'

export default class MyProvider implements SatelliteProviderContract {
constructor(protected app: ApplicationService) {}
export default definePlugin({
name: 'my-feature',
satelliteApi: 1,
pluginApiVersion: LASAGNA_PLUGIN_API_VERSION,

register() {
this.app.container.singleton(MyService, () => new MyService())
}
bind(app) {
app.container.singleton(MyService, () => new MyService())
},

async start() {
const hooks = await this.app.container.make(HookRegistry)
async start(app) {
const hooks = await app.container.make(HookRegistry)
hooks.before('destroy', async (ctx) => {
const service = await this.app.container.make(MyService)
const service = await app.container.make(MyService)
await service.deleteForTenant(ctx.tenant.id)
})
}
}
},
})
```

Every lifecycle method (`register`, `boot`, `start`, `ready`, `shutdown`) is
optional. Resolve core services with `app.container.make(...)`; never `new` a
core service yourself.
Every hook (`bind`, `boot`, `start`, `ready`, `shutdown`) is optional. Resolve core
services with `app.container.make(...)`; never `new` a core service yourself. The
facade runs the `satelliteApi` boot-time assert for you (the ABI declared above),
so a satellite gets the runtime compatibility backstop with no boilerplate.

Reach for a raw `implements SatelliteProviderContract` class only when you need
lifecycle control the spec does not model. See
[Building a plugin](/guides/plugins) for the full facade reference, including the
authorizer, middleware, request-macro, and capability seams.

## Migrations

Expand Down
8 changes: 8 additions & 0 deletions docs/guides/extensibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ driver, an audit sink, a feature-flag strategy. This page is the one contract
they all follow, so once you have written one extension you know how to write
any of them.

If you are packaging a whole satellite, the [`definePlugin` facade](/guides/plugins)
wires several of these seams (tenant-access authorizers, route middleware, request
macros, capabilities) into one declarative spec. This page is the contract those
seams sit on.

## The two version numbers

Lasagna has two independent version integers. Keep them straight:
Expand Down Expand Up @@ -73,6 +78,9 @@ because that is what it is.
| [webhooks](/guides/satellites/webhooks) | payload transformers | `WebhookTransformerRegistry` | `WEBHOOKS_CONTRACT_VERSION` |
| [isolation](/guides/cookbook/custom-isolation-driver) | custom isolation drivers | `IsolationDriverRegistry` | `ISOLATION_CONTRACT_VERSION` |
| resolution | custom tenant resolvers | `TenantResolverRegistry` | `RESOLVER_CONTRACT_VERSION` |
| [plugin](/guides/plugins) | tenant-access authorizers | `AuthorizerRegistry` | `AUTHORIZER_CONTRACT_VERSION` |
| [plugin](/guides/plugins) | route middleware | `TenantMiddlewareRegistry` | `TENANT_MIDDLEWARE_CONTRACT_VERSION` |
| [plugin](/guides/plugins) | cross-plugin capabilities | `CapabilityRegistry` | `CAPABILITY_CONTRACT_VERSION` |

`reporting`, `audit`, `feature-flags`, and `webhooks` registries are container
singletons (resolve via `container.make`). `admin` and `sso` ship no provider, so
Expand Down
Loading
Loading