Skip to content

Commit efc3d9d

Browse files
authored
Merge pull request #20 from Arcoders/LASAGNA-090706/soft
Lasagna 090706/soft
2 parents 7252b7d + 1e6f79b commit efc3d9d

1,045 files changed

Lines changed: 14609 additions & 25388 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 84 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ name: CI
22

33
on:
44
push:
5-
# TEMP (plugin-platform A→S→B→C branch review): run the full CI matrix on the
6-
# crypto-satellite branch so the whole plugin-platform stack is exercised on the
7-
# branch before merging. REVERT this extra branch entry (crypto-satellite) before
8-
# merging to master. (The ws-ai-3 entry is a stale leftover from an earlier review.)
9-
branches: [master, main, LASAGNA-020726/ws-ai-3-vector-store, LASAGNA-040726/crypto-satellite]
5+
# TEMP: run CI on this branch while it bakes. Revert before merging.
6+
branches: [master, main, LASAGNA-090706/soft]
107
pull_request:
118
branches: [master, main]
129

@@ -56,8 +53,8 @@ jobs:
5653
- name: Build (core)
5754
run: npm run build
5855

59-
- name: Build satellite packages (sso, billing, admin, backup, websockets, reporting, ai, crypto)
60-
run: npm run build:sso && npm run build:billing && npm run build:admin && npm run build:backup && npm run build:websockets && npm run build:reporting && npm run build:ai && npm run build:crypto
56+
- name: Build satellite packages (sso, billing, admin, backup, websockets, reporting, ai)
57+
run: npm run build:sso && npm run build:billing && npm run build:admin && npm run build:backup && npm run build:websockets && npm run build:reporting && npm run build:ai
6158

6259
# The dev-only satellite-test-kit is imported by core's bin/test.integration.ts
6360
# (and each satellite's), which core's tsconfig typechecks. Build it before the
@@ -204,7 +201,6 @@ jobs:
204201
npm run test:coverage --workspace @adonisjs-lasagna/websockets
205202
npm run test:coverage --workspace @adonisjs-lasagna/reporting
206203
npm run test:coverage --workspace @adonisjs-lasagna/ai
207-
npm run test:coverage --workspace @adonisjs-lasagna/crypto
208204
209205
# Satellite ABI compatibility (B5): the reference third-party satellite
210206
# is built + tested against the freshly-built core above. Its typecheck
@@ -216,6 +212,16 @@ jobs:
216212
npm run build:template
217213
npm run test --workspace @adonisjs-lasagna/satellite-template
218214
215+
# The starter scaffolder encodes the golden install path as data. Its unit
216+
# suite pins that plan (peers configured before core, config/database.ts
217+
# written after) and the two install defects the generated config closes.
218+
# The suite never spawns a process, so it needs no database and no network;
219+
# the win32 npm invocation is covered by injecting the platform instead.
220+
- name: Build + test the starter scaffolder
221+
run: |
222+
npm run build:starter
223+
npm run test --workspace create-lasagna-saas
224+
219225
# The shared integration harness underwrites core + every satellite, so it
220226
# carries its own unit suite (the pure exit-code/glob/spec-import logic +
221227
# the boot-safety metatest) behind a self-contained coverage floor
@@ -296,14 +302,6 @@ jobs:
296302
path: coverage/.v8/ai-unit
297303
if-no-files-found: warn
298304

299-
- name: Upload raw satellite unit coverage (V8) — crypto
300-
if: always()
301-
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
302-
with:
303-
name: c8-raw-crypto-unit
304-
path: coverage/.v8/crypto-unit
305-
if-no-files-found: warn
306-
307305
# Full report (unused exports / orphaned files / deps) stays informational.
308306
- name: Knip (unused-code report)
309307
run: npm run knip
@@ -332,7 +330,7 @@ jobs:
332330
- name: Package contracts (publint + types resolution)
333331
run: |
334332
set -e
335-
for pkg in core sso billing admin backup websockets reporting ai crypto; do
333+
for pkg in core sso billing admin backup websockets reporting ai; do
336334
echo "== @adonisjs-lasagna/$pkg =="
337335
( cd "packages/$pkg" \
338336
&& npx -y publint@0.3.21 \
@@ -477,11 +475,11 @@ jobs:
477475
# with `default_transaction_read_only = on` so a write is denied by Postgres.
478476
PLUGIN_RO_DB_USER: plugin_ro
479477
PLUGIN_RO_DB_PASSWORD: plugin_ro
480-
# This job HAS a real Postgres service, so the crypto/AI real-PG proofs are
478+
# This job HAS a real Postgres service, so the AI real-PG proofs are
481479
# mandatory here: they must EXECUTE, never self-skip. The satellite real-PG
482480
# helpers read this flag and turn a would-be self-skip (PG unreachable, role
483481
# lacks CREATEDB) into a hard failure — the fail-loud twin of RLS_DB_USER,
484-
# so a broken/hardened runner can't ship the crypto crown-jewel proofs green.
482+
# so a broken/hardened runner can't ship those proofs green.
485483
REQUIRE_REAL_PG: '1'
486484

487485
steps:
@@ -655,14 +653,6 @@ jobs:
655653
- name: Test (satellite integration) + coverage — ai
656654
run: npm run test:integration:coverage --workspace @adonisjs-lasagna/ai
657655

658-
# crypto's integration tier: the wrapped-DEK store + shred + WORM ledger + the
659-
# @encrypted/@searchable decorators against real Postgres via the kit, across
660-
# every placement (schema-pg, a real second database for database-pg, and the
661-
# shared rowscope table with its RLS stub). The database-pg + rowscope specs
662-
# self-skip if the CI role cannot CREATEDB / set the RLS GUC.
663-
- name: Test (satellite integration) + coverage — crypto
664-
run: npm run test:integration:coverage --workspace @adonisjs-lasagna/crypto
665-
666656
# Consumer canary: boot the shared harness from a fresh satellite (the
667657
# reference template) against core's fixture. A kit change that breaks a
668658
# real consumer fails here on an isolated, fast signal instead of buried in
@@ -753,18 +743,6 @@ jobs:
753743
path: coverage/.v8/ai-integration
754744
if-no-files-found: warn
755745

756-
# crypto integration V8 (written by test:integration:coverage to
757-
# coverage/.v8/crypto-integration in the "— crypto" step above). The
758-
# coverage-report job merges this with crypto's unit V8 so
759-
# check-satellite-coverage.mjs gates a real per-satellite MERGED number.
760-
- name: Upload raw satellite integration coverage (V8) — crypto
761-
if: always()
762-
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
763-
with:
764-
name: c8-raw-crypto-integration
765-
path: coverage/.v8/crypto-integration
766-
if-no-files-found: warn
767-
768746
test-e2e-demo:
769747
name: E2E (demo app)
770748
runs-on: ubuntu-latest
@@ -830,7 +808,10 @@ jobs:
830808
# exempt loopback from the SSRF guard for the demo run. Off in production,
831809
# and even when on it only allows loopback (private/metadata stay blocked).
832810
WEBHOOKS_ALLOW_LOOPBACK_TARGETS: 'true'
833-
DEMO_ADMIN_TOKEN: demo-admin-token-change-me
811+
# The auth_realms e2e logs in as the hook-seeded per-tenant demo user,
812+
# so tenant migrations must seed it here. Default-off everywhere else;
813+
# the deploy smoke stack deliberately leaves it unset to prove that.
814+
DEMO_SEED_TENANT_USERS: 'true'
834815
MAILCATCHER_HOST: 127.0.0.1
835816
MAILCATCHER_PORT: 1025
836817
MAIL_FROM_ADDRESS: demo@example.test
@@ -961,7 +942,6 @@ jobs:
961942
CACHE_REDIS_PORT: 56379
962943
CACHE_REDIS_DB: 2
963944
BACKUP_STORAGE_PATH: ./storage/backups
964-
DEMO_ADMIN_TOKEN: demo-admin-token-change-me
965945
MAILCATCHER_HOST: 127.0.0.1
966946
MAILCATCHER_PORT: 1025
967947
MAIL_FROM_ADDRESS: demo@example.test
@@ -1024,6 +1004,69 @@ jobs:
10241004
path: coverage/.v8/websockets-integration
10251005
if-no-files-found: warn
10261006

1007+
# The only job that installs this package the way a stranger does: from the
1008+
# packed tarball, into an app scaffolded by `npm init adonisjs`. Every other
1009+
# gate resolves @adonisjs-lasagna/saas-tenancy through the workspace symlink,
1010+
# which hides a whole class of defect — a configure hook the root entry never
1011+
# exports, a stub that ships but cannot render, a stub calling an Application
1012+
# method that does not exist, a missing migration. All four shipped at once,
1013+
# with the other 46 gates green.
1014+
clean-install:
1015+
name: Clean install (packed tarball into a fresh app)
1016+
runs-on: ubuntu-latest
1017+
timeout-minutes: 25
1018+
1019+
services:
1020+
postgres:
1021+
image: postgres:16-alpine
1022+
env:
1023+
POSTGRES_USER: postgres
1024+
POSTGRES_PASSWORD: postgres
1025+
POSTGRES_DB: postgres
1026+
ports:
1027+
- 5432:5432
1028+
options: >-
1029+
--health-cmd pg_isready
1030+
--health-interval 5s
1031+
--health-timeout 5s
1032+
--health-retries 10
1033+
1034+
redis:
1035+
image: redis:7.0.5-alpine
1036+
ports:
1037+
- 6379:6379
1038+
options: >-
1039+
--health-cmd "redis-cli ping"
1040+
--health-interval 5s
1041+
--health-timeout 5s
1042+
--health-retries 10
1043+
1044+
steps:
1045+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
1046+
1047+
- name: Setup Node.js
1048+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
1049+
with:
1050+
node-version: '24'
1051+
cache: npm
1052+
1053+
- name: Install dependencies
1054+
run: npm ci --include=optional
1055+
1056+
# The generated app runs `npm install` from the public registry, so this job
1057+
# needs network. It builds core, packs it, and drives the golden path from
1058+
# `configure` to a provisioned tenant schema.
1059+
- name: Clean-install smoke
1060+
env:
1061+
PG_HOST: 127.0.0.1
1062+
PG_PORT: 5432
1063+
PG_USER: postgres
1064+
PG_PASSWORD: postgres
1065+
PG_DATABASE: lasagna_clean_install
1066+
REDIS_HOST: 127.0.0.1
1067+
REDIS_PORT: 6379
1068+
run: bash scripts/clean-install-smoke.sh
1069+
10271070
deploy-validate:
10281071
name: Deploy assets (static validation)
10291072
runs-on: ubuntu-latest
@@ -1180,12 +1223,6 @@ jobs:
11801223
name: c8-raw-ai-integration
11811224
path: coverage/.v8/all
11821225

1183-
- name: Download raw satellite integration coverage (V8) — crypto
1184-
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
1185-
with:
1186-
name: c8-raw-crypto-integration
1187-
path: coverage/.v8/all
1188-
11891226
# websockets integration V8 is produced by the test-e2e-websockets job (a
11901227
# different job than test-integration above), now run under c8. This is why
11911228
# test-e2e-websockets is in this job's needs:.
@@ -1241,12 +1278,6 @@ jobs:
12411278
name: c8-raw-ai-unit
12421279
path: coverage/.v8/all
12431280

1244-
- name: Download raw satellite unit coverage (V8) — crypto
1245-
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
1246-
with:
1247-
name: c8-raw-crypto-unit
1248-
path: coverage/.v8/all
1249-
12501281
- name: Aggregate coverage report (unit + integration, remapped to src)
12511282
run: npm run coverage:report
12521283

.github/workflows/publish.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ jobs:
7373
fi
7474
}
7575
76-
# Core first so satellites resolve a published peer.
76+
# Core first so satellites resolve a published peer. Only the six
77+
# publishable workspaces belong here: `admin` and `websockets` are
78+
# `private: true` (npm refuses them with EPRIVATE, which under
79+
# `set -euo pipefail` would abort this script mid-release), and the
80+
# crypto satellite was removed from the repo. `check-publish-coverage.mjs`
81+
# asserts every non-private package appears below.
7782
publish_pkg "packages/core"
7883
publish_pkg "packages/sso"
7984
publish_pkg "packages/billing"
80-
publish_pkg "packages/admin"
8185
publish_pkg "packages/backup"
8286
publish_pkg "packages/reporting"
83-
publish_pkg "packages/websockets"
8487
publish_pkg "packages/ai"
85-
publish_pkg "packages/crypto"

.github/workflows/release.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,19 @@ permissions:
3131
jobs:
3232
release:
3333
runs-on: ubuntu-latest
34-
# `types: [completed]` fires on failure and cancellation too; this guard is
35-
# the actual gate.
36-
if: github.event.workflow_run.conclusion == 'success'
34+
# `types: [completed]` fires on failure and cancellation too; the conclusion
35+
# check is the CI gate.
36+
#
37+
# `PUBLISH_ENABLED` is the human gate, and it is OFF until the repository
38+
# variable is set to 'true' (Settings → Secrets and variables → Actions →
39+
# Variables). Without it, this workflow would publish to npm the moment CI
40+
# goes green on master: with no changeset files pending, changesets/action
41+
# skips the Version PR and runs the publish command straight away, shipping
42+
# whatever versions the package.json files happen to carry. npm versions are
43+
# immutable, so that is a one-way door taken by a merge, not by a decision.
44+
if: >-
45+
github.event.workflow_run.conclusion == 'success' &&
46+
vars.PUBLISH_ENABLED == 'true'
3747
steps:
3848
# Check out the exact commit CI validated, not the branch tip — if master
3949
# moved while CI ran, the tip's suite hasn't passed yet and its publish

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ benchmarks/results/
1818

1919
# Temp dir for the docs code-fence import gate (scripts/check-docs-code.mjs)
2020
.docs-code-tmp/
21-
docs/.vitepress/
21+
22+
# VitePress generated output only. The theme source under docs/.vitepress/theme/
23+
# IS tracked; a blanket `docs/.vitepress/` rule silently drops any NEW theme file
24+
# (existing ones survive only because git ignores untracked files, not tracked ones).
25+
docs/.vitepress/cache/
26+
docs/.vitepress/dist/
27+
docs/.vitepress/*.timestamp-*.mjs
2228

2329
# doc-coverage: the graph is generated, never committed (RFC §5). The cache is
2430
# keyed by a src+docs content hash and is always safe to delete.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
📖 **[Full documentation →](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/)** · [Quickstart](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/start/quickstart) · [Why Lasagna](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/start/why) · [Comparison vs stancl](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/reference/comparison) · [Release notes](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/reference/release-notes)
1818

19-
> **Stability: release candidate.** The isolation core is feature complete and green in CI against real Postgres and Redis, but the `stable` label is withheld until an independent security review and production mileage close. The satellite **packages** (admin, SSO, billing, backup, websockets, reporting) are **release candidate** too; the opt-in in-core features (quotas, webhooks, metrics, audit logs, branding, feature flags, impersonation) are **experimental**. Full breakdown and the 1.x semver promise in the [stability matrix](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/reference/stability).
19+
> **Stability: release candidate.** The isolation core is feature complete and green in CI against real Postgres and Redis, but the `stable` label and the `1.0.0` version are both withheld until an independent security review and production mileage close. It ships `0.3.0`, so semver promises nothing across a minor. The satellite **packages** (SSO, billing, backup, reporting, AI) ship `0.1.0` and are **experimental**, as are the opt-in in-core features (quotas, webhooks, metrics, audit logs, branding, feature flags, impersonation). Full breakdown in the [stability matrix](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/reference/stability).
2020
2121
I built this because the AdonisJS ecosystem deserved a proper multi
2222
tenancy foundation, and because every SaaS I touched eventually outgrew

benchmarks/fixture/app/models/tenant.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import type { TenantStatus } from '@adonisjs-lasagna/saas-tenancy/types'
77
/**
88
* Lean tenant registry model. The bench provisions tenant storage directly
99
* (see src/harness/provision.ts), so this model carries only what the
10-
* repository, the tenant guard, and `request.tenant()` read — no install /
11-
* migrate / connection plumbing.
10+
* repository, the tenant guard, and `request.tenant()` read, with none of the
11+
* install, migrate, or connection plumbing.
1212
*/
1313
export default class Tenant extends BackofficeBaseModel {
1414
@column({ isPrimary: true })

benchmarks/fixture/config/multitenancy.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default {
3535
// the isolation tier swaps the id per request to prove cross-tenant queries are
3636
// contained. There is no authenticated principal in a benchmark, so the
3737
// membership gate is intentionally absent. Acknowledge it so the production-mode
38-
// run boots (the WS-3 resolution-safety check hard-fails an unacknowledged
38+
// run boots (the resolution-safety check hard-fails an unacknowledged
3939
// client-controlled strategy in production).
4040
acknowledgeNoMembershipGate: true,
4141
tenantHeaderKey: env.get('TENANT_HEADER_KEY'),
@@ -46,7 +46,7 @@ export default {
4646
isolation: {
4747
driver,
4848
// The template connection that schema-pg/database-pg clone per tenant.
49-
// rowscope-pg ignores this it shares `centralConnectionName` ('public')
49+
// rowscope-pg ignores this: it shares `centralConnectionName` ('public')
5050
// since there is no per-tenant connection. `tenant` points at `public`, so
5151
// the same physical DB backs every driver under BENCH_DRIVER.
5252
templateConnectionName: 'tenant',
@@ -73,7 +73,7 @@ export default {
7373
},
7474

7575
// The breaker fires on every guarded request, so the config must carry the
76-
// block — values mirror examples/api (volumeThreshold high enough not to trip
76+
// block. Values mirror examples/api (volumeThreshold high enough not to trip
7777
// under a healthy load run).
7878
circuitBreaker: {
7979
threshold: 50,

benchmarks/fixture/start/routes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ router
5353
.prefix('tenant')
5454
.use(middleware.tenantGuard())
5555

56-
// Same read, guard removed diff vs /tenant/notes prices the guard middleware.
56+
// Same read, guard removed. The diff vs /tenant/notes prices the guard middleware.
5757
router.get('/noguard/notes', async ({ request, response }) => {
5858
const tenant = await request.tenant()
5959
const notes = await readRecentNotes(tenant)
6060
return response.ok({ tenantId: tenant.id, notes })
6161
})
6262

63-
// Same read, guard + rate-limit diff vs /tenant/notes prices the rate-limit
63+
// Same read, guard + rate-limit. The diff vs /tenant/notes prices the rate-limit
6464
// Redis pipeline. Generous limit so the limiter never trips during a load run.
6565
router
6666
.get('/ratelimited/notes', async ({ request, response }) => {

benchmarks/src/db/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Tier 2 DB per-driver query latency + connection churn. Boots the bench
2+
* Tier 2: DB per-driver query latency + connection churn. Boots the bench
33
* fixture headless (the drivers need a booted app), runs against real Postgres
44
* for the driver selected by BENCH_DRIVER.
55
*

benchmarks/src/harness/baseline.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* `npm run bench:check` — compare the newest results against a committed
3-
* baseline and print a regression verdict.
2+
* `npm run bench:check` compares the newest results against a committed
3+
* baseline and prints a regression verdict.
44
*
55
* --baseline=ci-ubuntu (default; the gate baseline)
66
* --baseline=1.0.0 (the docs baseline; for a self-compare sanity check)
@@ -47,7 +47,7 @@ const latest = latestBySuiteDriver()
4747

4848
// Correctness gate (always hard, independent of the throughput tolerance and of
4949
// BENCH_GATE_ENFORCE): any result whose meta carries a `*Check` field set to
50-
// 'FAIL' is a correctness regression a cross-tenant leak, a broken fail
50+
// 'FAIL' is a correctness regression: a cross-tenant leak, a broken fail
5151
// policy, an unstable soak. These must never be merged, noisy runner or not.
5252
const correctnessFailures: string[] = []
5353
for (const [key, file] of latest) {

0 commit comments

Comments
 (0)