feat(v1.1.3-modules): reject module scripts from routes + triggers; tighten cross-app trigger check

- `POST /api/v1/admin/scripts/{id}/routes` returns 400 when the
  target script is `kind=module`. Modules have no entry point — they
  are imported, not invoked.
- `POST /api/v1/admin/apps/{id}/triggers/{kv,docs,dead_letter}` gain
  a shared `validate_trigger_target` that loads the target script
  and rejects when:
  - the script doesn't exist
  - the script belongs to a different app  (latent v1.1.1/v1.1.2 gap
    where triggers could target a script in any app — closed here)
  - the script is `kind=module`
- `TriggersState` grows a `scripts: Arc<dyn ScriptRepository>` field
  so handlers can load the target script.
- Trigger-create test helpers split into `state_with` (empty script
  repo — for tests asserting upstream errors) and
  `state_with_endpoint` (pre-populated — for tests asserting
  successful creation). `InMemoryScriptRepo` added to the test
  module.

Workspace builds; full test suite (~440 tests) green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-02 22:15:53 +02:00
parent 84833d3e4e
commit c6211a73b9
3 changed files with 202 additions and 6 deletions

View File

@@ -217,7 +217,7 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
};
let route_admin = RouteAdminState {
routes: route_repo.clone(),
scripts: Arc::new(PostgresScriptRepoHandle(script_repo)),
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
domains: domains_repo.clone(),
table: route_table.clone(),
authz: authz.clone(),
@@ -245,6 +245,7 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
triggers: trigger_repo,
apps: apps_repo.clone(),
authz: authz.clone(),
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
config: trigger_config,
};
let dead_letters_state = DeadLettersState {