@adonis-agora/durable 1.0.0-rc.2 → 1.0.0-rc.20
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/CHANGELOG.md +198 -0
- package/dist/assets/spa/assets/index-BgoNV3TR.css +1 -0
- package/dist/assets/spa/assets/index-DzXygyDm.js +57 -0
- package/dist/assets/spa/assets/index-DzXygyDm.js.map +1 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/assets/spa/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-400-normal-CRt88UEn.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-400-normal-OW7qkl5a.woff +0 -0
- package/dist/assets/spa/assets/sora-latin-500-normal-01eiPEn0.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-500-normal-w58xtEt9.woff +0 -0
- package/dist/assets/spa/assets/sora-latin-600-normal-1_7fyUAY.woff +0 -0
- package/dist/assets/spa/assets/sora-latin-600-normal-Cdg4DaK0.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-700-normal-9waGdLWo.woff2 +0 -0
- package/dist/assets/spa/assets/sora-latin-700-normal-BKPfQAnC.woff +0 -0
- package/dist/assets/spa/index.html +2 -8
- package/dist/commands/agora_doctor.d.ts +23 -0
- package/dist/commands/agora_doctor.d.ts.map +1 -0
- package/dist/commands/agora_doctor.js +110 -0
- package/dist/commands/agora_doctor.js.map +1 -0
- package/dist/commands/durable_doctor.d.ts +8 -0
- package/dist/commands/durable_doctor.d.ts.map +1 -0
- package/dist/commands/durable_doctor.js +8 -0
- package/dist/commands/durable_doctor.js.map +1 -0
- package/dist/commands/durable_worker.d.ts +3 -3
- package/dist/commands/durable_worker.js +7 -7
- package/dist/commands/durable_worker.js.map +1 -1
- package/dist/commands/health.d.ts +12 -0
- package/dist/commands/health.d.ts.map +1 -0
- package/dist/commands/health.js +34 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/lifecycle_republish.d.ts +8 -9
- package/dist/commands/lifecycle_republish.d.ts.map +1 -1
- package/dist/commands/lifecycle_republish.js +29 -16
- package/dist/commands/lifecycle_republish.js.map +1 -1
- package/dist/commands/main.d.ts +25 -5
- package/dist/commands/main.d.ts.map +1 -1
- package/dist/commands/main.js +54 -7
- package/dist/commands/main.js.map +1 -1
- package/dist/commands/runs.d.ts +9 -16
- package/dist/commands/runs.d.ts.map +1 -1
- package/dist/commands/runs.js +60 -47
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/runs_bulk.d.ts +17 -0
- package/dist/commands/runs_bulk.d.ts.map +1 -0
- package/dist/commands/runs_bulk.js +74 -0
- package/dist/commands/runs_bulk.js.map +1 -0
- package/dist/commands/runs_cancel.d.ts +15 -0
- package/dist/commands/runs_cancel.d.ts.map +1 -0
- package/dist/commands/runs_cancel.js +39 -0
- package/dist/commands/runs_cancel.js.map +1 -0
- package/dist/commands/runs_continue.d.ts +12 -0
- package/dist/commands/runs_continue.d.ts.map +1 -0
- package/dist/commands/runs_continue.js +28 -0
- package/dist/commands/runs_continue.js.map +1 -0
- package/dist/commands/runs_event.d.ts +13 -0
- package/dist/commands/runs_event.d.ts.map +1 -0
- package/dist/commands/runs_event.js +28 -0
- package/dist/commands/runs_event.js.map +1 -0
- package/dist/commands/runs_fan.d.ts +16 -0
- package/dist/commands/runs_fan.d.ts.map +1 -0
- package/dist/commands/runs_fan.js +58 -0
- package/dist/commands/runs_fan.js.map +1 -0
- package/dist/commands/runs_redispatch.d.ts +14 -0
- package/dist/commands/runs_redispatch.d.ts.map +1 -0
- package/dist/commands/runs_redispatch.js +35 -0
- package/dist/commands/runs_redispatch.js.map +1 -0
- package/dist/commands/runs_replay.d.ts +14 -0
- package/dist/commands/runs_replay.d.ts.map +1 -0
- package/dist/commands/runs_replay.js +38 -0
- package/dist/commands/runs_replay.js.map +1 -0
- package/dist/commands/runs_retry.d.ts +13 -0
- package/dist/commands/runs_retry.d.ts.map +1 -0
- package/dist/commands/runs_retry.js +29 -0
- package/dist/commands/runs_retry.js.map +1 -0
- package/dist/commands/runs_show.d.ts +14 -0
- package/dist/commands/runs_show.d.ts.map +1 -0
- package/dist/commands/runs_show.js +96 -0
- package/dist/commands/runs_show.js.map +1 -0
- package/dist/commands/runs_signal.d.ts +17 -0
- package/dist/commands/runs_signal.d.ts.map +1 -0
- package/dist/commands/runs_signal.js +49 -0
- package/dist/commands/runs_signal.js.map +1 -0
- package/dist/commands/runs_update.d.ts +15 -0
- package/dist/commands/runs_update.d.ts.map +1 -0
- package/dist/commands/runs_update.js +39 -0
- package/dist/commands/runs_update.js.map +1 -0
- package/dist/commands/runs_values.d.ts +15 -0
- package/dist/commands/runs_values.d.ts.map +1 -0
- package/dist/commands/runs_values.js +45 -0
- package/dist/commands/runs_values.js.map +1 -0
- package/dist/commands/schedules.d.ts +18 -0
- package/dist/commands/schedules.d.ts.map +1 -0
- package/dist/commands/schedules.js +70 -0
- package/dist/commands/schedules.js.map +1 -0
- package/dist/commands/schedules_pause.d.ts +16 -0
- package/dist/commands/schedules_pause.d.ts.map +1 -0
- package/dist/commands/schedules_pause.js +40 -0
- package/dist/commands/schedules_pause.js.map +1 -0
- package/dist/commands/schedules_resume.d.ts +8 -0
- package/dist/commands/schedules_resume.d.ts.map +1 -0
- package/dist/commands/schedules_resume.js +8 -0
- package/dist/commands/schedules_resume.js.map +1 -0
- package/dist/commands/schedules_trigger.d.ts +13 -0
- package/dist/commands/schedules_trigger.d.ts.map +1 -0
- package/dist/commands/schedules_trigger.js +32 -0
- package/dist/commands/schedules_trigger.js.map +1 -0
- package/dist/commands/tasks_complete.d.ts +15 -0
- package/dist/commands/tasks_complete.d.ts.map +1 -0
- package/dist/commands/tasks_complete.js +41 -0
- package/dist/commands/tasks_complete.js.map +1 -0
- package/dist/commands/tasks_fail.d.ts +16 -0
- package/dist/commands/tasks_fail.d.ts.map +1 -0
- package/dist/commands/tasks_fail.js +47 -0
- package/dist/commands/tasks_fail.js.map +1 -0
- package/dist/commands/workers.d.ts +11 -0
- package/dist/commands/workers.d.ts.map +1 -0
- package/dist/commands/workers.js +35 -0
- package/dist/commands/workers.js.map +1 -0
- package/dist/commands/workflows_lifecycle.d.ts +12 -0
- package/dist/commands/workflows_lifecycle.d.ts.map +1 -0
- package/dist/commands/workflows_lifecycle.js +37 -0
- package/dist/commands/workflows_lifecycle.js.map +1 -0
- package/dist/configure.d.ts +1 -1
- package/dist/configure.js +1 -1
- package/dist/providers/dashboard_provider.d.ts +57 -54
- package/dist/providers/dashboard_provider.d.ts.map +1 -1
- package/dist/providers/dashboard_provider.js +399 -288
- package/dist/providers/dashboard_provider.js.map +1 -1
- package/dist/providers/durable_provider.d.ts.map +1 -1
- package/dist/providers/durable_provider.js +67 -37
- package/dist/providers/durable_provider.js.map +1 -1
- package/dist/src/commands/console_command.d.ts +82 -0
- package/dist/src/commands/console_command.d.ts.map +1 -0
- package/dist/src/commands/console_command.js +238 -0
- package/dist/src/commands/console_command.js.map +1 -0
- package/dist/src/commands/runs.js +2 -2
- package/dist/src/commands/runs.js.map +1 -1
- package/dist/src/concurrency.d.ts.map +1 -1
- package/dist/src/concurrency.js +1 -0
- package/dist/src/concurrency.js.map +1 -1
- package/dist/src/config_types.d.ts +79 -26
- package/dist/src/config_types.d.ts.map +1 -1
- package/dist/src/dashboard/access_denied_page.d.ts +5 -7
- package/dist/src/dashboard/access_denied_page.d.ts.map +1 -1
- package/dist/src/dashboard/access_denied_page.js +2 -6
- package/dist/src/dashboard/access_denied_page.js.map +1 -1
- package/dist/src/dashboard/audit.d.ts +14 -0
- package/dist/src/dashboard/audit.d.ts.map +1 -0
- package/dist/src/dashboard/audit.js +36 -0
- package/dist/src/dashboard/audit.js.map +1 -0
- package/dist/src/dashboard/console_authorize.d.ts +98 -0
- package/dist/src/dashboard/console_authorize.d.ts.map +1 -0
- package/dist/src/dashboard/console_authorize.js +163 -0
- package/dist/src/dashboard/console_authorize.js.map +1 -0
- package/dist/src/dashboard/console_shell.d.ts +45 -0
- package/dist/src/dashboard/console_shell.d.ts.map +1 -0
- package/dist/src/dashboard/console_shell.js +125 -0
- package/dist/src/dashboard/console_shell.js.map +1 -0
- package/dist/src/dashboard/console_switcher.d.ts +132 -0
- package/dist/src/dashboard/console_switcher.d.ts.map +1 -0
- package/dist/src/dashboard/console_switcher.js +201 -0
- package/dist/src/dashboard/console_switcher.js.map +1 -0
- package/dist/src/dashboard/define_config.d.ts +68 -81
- package/dist/src/dashboard/define_config.d.ts.map +1 -1
- package/dist/src/dashboard/define_config.js +18 -112
- package/dist/src/dashboard/define_config.js.map +1 -1
- package/dist/src/dashboard/dev_tools.d.ts +102 -0
- package/dist/src/dashboard/dev_tools.d.ts.map +1 -0
- package/dist/src/dashboard/dev_tools.js +502 -0
- package/dist/src/dashboard/dev_tools.js.map +1 -0
- package/dist/src/dashboard/diagnostics-recorder.d.ts +1 -1
- package/dist/src/dashboard/diagnostics-recorder.js +1 -1
- package/dist/src/dashboard/gateway-adapter.d.ts +6 -6
- package/dist/src/dashboard/gateway-adapter.d.ts.map +1 -1
- package/dist/src/dashboard/gateway-adapter.js +5 -5
- package/dist/src/dashboard/gateway-adapter.js.map +1 -1
- package/dist/src/dashboard/handlers.d.ts +48 -39
- package/dist/src/dashboard/handlers.d.ts.map +1 -1
- package/dist/src/dashboard/handlers.js +181 -83
- package/dist/src/dashboard/handlers.js.map +1 -1
- package/dist/src/dashboard/index.d.ts +12 -9
- package/dist/src/dashboard/index.d.ts.map +1 -1
- package/dist/src/dashboard/index.js +10 -7
- package/dist/src/dashboard/index.js.map +1 -1
- package/dist/src/dashboard/list-filters.d.ts +27 -0
- package/dist/src/dashboard/list-filters.d.ts.map +1 -0
- package/dist/src/dashboard/list-filters.js +69 -0
- package/dist/src/dashboard/list-filters.js.map +1 -0
- package/dist/src/dashboard/openapi.d.ts.map +1 -1
- package/dist/src/dashboard/openapi.js +127 -52
- package/dist/src/dashboard/openapi.js.map +1 -1
- package/dist/src/dashboard/run-filter.d.ts +18 -15
- package/dist/src/dashboard/run-filter.d.ts.map +1 -1
- package/dist/src/dashboard/run-filter.js +30 -22
- package/dist/src/dashboard/run-filter.js.map +1 -1
- package/dist/src/dashboard/spa.d.ts +26 -1
- package/dist/src/dashboard/spa.d.ts.map +1 -1
- package/dist/src/dashboard/spa.js +9 -2
- package/dist/src/dashboard/spa.js.map +1 -1
- package/dist/src/dashboard/tenant_scope.d.ts +38 -0
- package/dist/src/dashboard/tenant_scope.d.ts.map +1 -0
- package/dist/src/dashboard/tenant_scope.js +166 -0
- package/dist/src/dashboard/tenant_scope.js.map +1 -0
- package/dist/src/define_config.d.ts +27 -12
- package/dist/src/define_config.d.ts.map +1 -1
- package/dist/src/define_config.js +9 -4
- package/dist/src/define_config.js.map +1 -1
- package/dist/src/diagnostics/index.d.ts +11 -0
- package/dist/src/diagnostics/index.d.ts.map +1 -0
- package/dist/src/diagnostics/index.js +6 -0
- package/dist/src/diagnostics/index.js.map +1 -0
- package/dist/src/diagnostics/transport.d.ts +71 -0
- package/dist/src/diagnostics/transport.d.ts.map +1 -0
- package/dist/src/diagnostics/transport.js +188 -0
- package/dist/src/diagnostics/transport.js.map +1 -0
- package/dist/src/diagnostics-bridge.d.ts +2 -1
- package/dist/src/diagnostics-bridge.d.ts.map +1 -1
- package/dist/src/diagnostics-bridge.js +7 -1
- package/dist/src/diagnostics-bridge.js.map +1 -1
- package/dist/src/doctor/agora_doctor.d.ts +116 -0
- package/dist/src/doctor/agora_doctor.d.ts.map +1 -0
- package/dist/src/doctor/agora_doctor.js +685 -0
- package/dist/src/doctor/agora_doctor.js.map +1 -0
- package/dist/src/doctor/checks.d.ts +28 -0
- package/dist/src/doctor/checks.d.ts.map +1 -0
- package/dist/src/doctor/checks.js +288 -0
- package/dist/src/doctor/checks.js.map +1 -0
- package/dist/src/doctor/index.d.ts +9 -0
- package/dist/src/doctor/index.d.ts.map +1 -0
- package/dist/src/doctor/index.js +16 -0
- package/dist/src/doctor/index.js.map +1 -0
- package/dist/src/engine.d.ts +55 -9
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +333 -84
- package/dist/src/engine.js.map +1 -1
- package/dist/src/errors.d.ts +9 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +13 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/event-trigger-bridge.d.ts +5 -14
- package/dist/src/event-trigger-bridge.d.ts.map +1 -1
- package/dist/src/event-trigger-bridge.js +11 -99
- package/dist/src/event-trigger-bridge.js.map +1 -1
- package/dist/src/index.d.ts +8 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/interfaces.d.ts +53 -9
- package/dist/src/interfaces.d.ts.map +1 -1
- package/dist/src/interfaces.js +8 -0
- package/dist/src/interfaces.js.map +1 -1
- package/dist/src/origin.js +3 -3
- package/dist/src/role_bindings.d.ts +8 -8
- package/dist/src/role_bindings.js +8 -8
- package/dist/src/run-gateway/interface.d.ts +4 -3
- package/dist/src/run-gateway/interface.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.d.ts +4 -2
- package/dist/src/run-gateway/proxy-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/proxy-run-gateway.js +21 -6
- package/dist/src/run-gateway/proxy-run-gateway.js.map +1 -1
- package/dist/src/run-gateway/run-request-responder.d.ts +6 -1
- package/dist/src/run-gateway/run-request-responder.d.ts.map +1 -1
- package/dist/src/run-gateway/run-request-responder.js +30 -3
- package/dist/src/run-gateway/run-request-responder.js.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.d.ts +4 -4
- package/dist/src/run-gateway/store-run-gateway.d.ts.map +1 -1
- package/dist/src/run-gateway/store-run-gateway.js +4 -4
- package/dist/src/run-gateway/store-run-gateway.js.map +1 -1
- package/dist/src/run-gateway/tenant-auth.d.ts +19 -1
- package/dist/src/run-gateway/tenant-auth.d.ts.map +1 -1
- package/dist/src/run-gateway/tenant-auth.js +133 -0
- package/dist/src/run-gateway/tenant-auth.js.map +1 -1
- package/dist/src/run-gateway/tenant-scoped-gateway.d.ts +10 -0
- package/dist/src/run-gateway/tenant-scoped-gateway.d.ts.map +1 -0
- package/dist/src/run-gateway/tenant-scoped-gateway.js +107 -0
- package/dist/src/run-gateway/tenant-scoped-gateway.js.map +1 -0
- package/dist/src/run-value-facets.d.ts +1 -1
- package/dist/src/run-value-facets.d.ts.map +1 -1
- package/dist/src/run-value-facets.js +5 -1
- package/dist/src/run-value-facets.js.map +1 -1
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +2 -1
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/schedules.d.ts +16 -1
- package/dist/src/schedules.d.ts.map +1 -1
- package/dist/src/schedules.js +29 -4
- package/dist/src/schedules.js.map +1 -1
- package/dist/src/services/main.d.ts +3 -3
- package/dist/src/services/main.js +6 -6
- package/dist/src/services/main.js.map +1 -1
- package/dist/src/singleton-admission.d.ts.map +1 -1
- package/dist/src/singleton-admission.js +2 -0
- package/dist/src/singleton-admission.js.map +1 -1
- package/dist/src/singleton-gate.d.ts +1 -1
- package/dist/src/singleton-gate.d.ts.map +1 -1
- package/dist/src/singleton-gate.js +4 -3
- package/dist/src/singleton-gate.js.map +1 -1
- package/dist/src/sql-schema.d.ts +54 -0
- package/dist/src/sql-schema.d.ts.map +1 -0
- package/dist/src/sql-schema.js +145 -0
- package/dist/src/sql-schema.js.map +1 -0
- package/dist/src/stores/factory.d.ts +6 -0
- package/dist/src/stores/factory.d.ts.map +1 -1
- package/dist/src/stores/factory.js +3 -0
- package/dist/src/stores/factory.js.map +1 -1
- package/dist/src/stores/lucid-mappers.d.ts +3 -0
- package/dist/src/stores/lucid-mappers.d.ts.map +1 -1
- package/dist/src/stores/lucid-mappers.js +12 -0
- package/dist/src/stores/lucid-mappers.js.map +1 -1
- package/dist/src/stores/lucid-pg-hot-path.d.ts +6 -2
- package/dist/src/stores/lucid-pg-hot-path.d.ts.map +1 -1
- package/dist/src/stores/lucid-pg-hot-path.js +7 -3
- package/dist/src/stores/lucid-pg-hot-path.js.map +1 -1
- package/dist/src/stores/lucid-schema.d.ts +18 -4
- package/dist/src/stores/lucid-schema.d.ts.map +1 -1
- package/dist/src/stores/lucid-schema.js +106 -61
- package/dist/src/stores/lucid-schema.js.map +1 -1
- package/dist/src/stores/lucid.d.ts +42 -2
- package/dist/src/stores/lucid.d.ts.map +1 -1
- package/dist/src/stores/lucid.js +450 -231
- package/dist/src/stores/lucid.js.map +1 -1
- package/dist/src/tenancy.d.ts +104 -0
- package/dist/src/tenancy.d.ts.map +1 -0
- package/dist/src/tenancy.js +91 -0
- package/dist/src/tenancy.js.map +1 -0
- package/dist/src/testing/in-memory-state-store.d.ts.map +1 -1
- package/dist/src/testing/in-memory-state-store.js +9 -3
- package/dist/src/testing/in-memory-state-store.js.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/state-store-conformance.js +59 -0
- package/dist/src/testing-kit/state-store-conformance.js.map +1 -1
- package/dist/src/testing-kit/transport-conformance.d.ts.map +1 -1
- package/dist/src/testing-kit/transport-conformance.js +5 -1
- package/dist/src/testing-kit/transport-conformance.js.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.d.ts.map +1 -1
- package/dist/src/transports/bullmq/bullmq-transport.js +14 -3
- package/dist/src/transports/bullmq/bullmq-transport.js.map +1 -1
- package/dist/src/transports/db-schema.d.ts +13 -5
- package/dist/src/transports/db-schema.d.ts.map +1 -1
- package/dist/src/transports/db-schema.js +52 -27
- package/dist/src/transports/db-schema.js.map +1 -1
- package/dist/src/transports/db.d.ts +6 -0
- package/dist/src/transports/db.d.ts.map +1 -1
- package/dist/src/transports/db.js +91 -42
- package/dist/src/transports/db.js.map +1 -1
- package/dist/src/transports/factory.d.ts +16 -0
- package/dist/src/transports/factory.d.ts.map +1 -1
- package/dist/src/transports/factory.js +6 -1
- package/dist/src/transports/factory.js.map +1 -1
- package/dist/src/transports/queue.d.ts +2 -2
- package/dist/src/transports/queue.d.ts.map +1 -1
- package/dist/src/transports/queue.js +49 -8
- package/dist/src/transports/queue.js.map +1 -1
- package/dist/src/transports/worker-keys-db.d.ts +5 -0
- package/dist/src/transports/worker-keys-db.d.ts.map +1 -1
- package/dist/src/transports/worker-keys-db.js +10 -8
- package/dist/src/transports/worker-keys-db.js.map +1 -1
- package/dist/src/workflow-discovery.d.ts +1 -1
- package/dist/src/workflow-discovery.d.ts.map +1 -1
- package/dist/src/workflow-discovery.js +4 -7
- package/dist/src/workflow-discovery.js.map +1 -1
- package/dist/src/workflow-events.d.ts +20 -51
- package/dist/src/workflow-events.d.ts.map +1 -1
- package/dist/src/workflow-events.js +12 -30
- package/dist/src/workflow-events.js.map +1 -1
- package/dist/src/workflow-ref.d.ts +1 -1
- package/dist/stubs/config/durable_dashboard.stub +9 -29
- package/dist/stubs/database/migrations/create_durable_tables.stub +12 -2
- package/dist/stubs/database/migrations/create_durable_transport_messages.stub +34 -5
- package/dist/stubs/database/migrations/create_durable_transport_tables.stub +30 -8
- package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +24 -2
- package/package.json +25 -7
- package/skills/durable-cluster/SKILL.md +43 -23
- package/skills/durable-reliability/SKILL.md +13 -7
- package/dist/assets/spa/assets/index-70hGzOnT.css +0 -1
- package/dist/assets/spa/assets/index-CJjMAUau.js +0 -16
- package/dist/assets/spa/assets/index-CJjMAUau.js.map +0 -1
- package/dist/commands/retry.d.ts +0 -16
- package/dist/commands/retry.d.ts.map +0 -1
- package/dist/commands/retry.js +0 -34
- package/dist/commands/retry.js.map +0 -1
- package/dist/src/dashboard/auth.d.ts +0 -134
- package/dist/src/dashboard/auth.d.ts.map +0 -1
- package/dist/src/dashboard/auth.js +0 -156
- package/dist/src/dashboard/auth.js.map +0 -1
- package/dist/src/dashboard/login_page.d.ts +0 -36
- package/dist/src/dashboard/login_page.d.ts.map +0 -1
- package/dist/src/dashboard/login_page.js +0 -158
- package/dist/src/dashboard/login_page.js.map +0 -1
- package/dist/src/dashboard/session_cookie.d.ts +0 -45
- package/dist/src/dashboard/session_cookie.d.ts.map +0 -1
- package/dist/src/dashboard/session_cookie.js +0 -97
- package/dist/src/dashboard/session_cookie.js.map +0 -1
|
@@ -3,18 +3,24 @@ import { basename, resolve as resolvePath } from 'node:path';
|
|
|
3
3
|
import { PassThrough } from 'node:stream';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { resolveAccessDeniedPage, } from '../src/dashboard/access_denied_page.js';
|
|
6
|
-
import {
|
|
6
|
+
import { recordConsoleAudit } from '../src/dashboard/audit.js';
|
|
7
7
|
import { compat, enumerateLiveFleet, mergeFleets, } from '../src/dashboard/compat.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveSignOut, resolveViewer } from '../src/dashboard/console_shell.js';
|
|
9
|
+
import { durableConsoleSearch, listVisibleConsoles, otherConsoles, readAgoraConsoles, registerAgoraConsole, searchConsoles, } from '../src/dashboard/console_switcher.js';
|
|
10
|
+
import { NO_AUTHORIZE_WARNING, resolveConfig, } from '../src/dashboard/define_config.js';
|
|
11
|
+
import { registerDurableDevTools } from '../src/dashboard/dev_tools.js';
|
|
9
12
|
import { BlockedDiagnosticsRecorder } from '../src/dashboard/diagnostics-recorder.js';
|
|
10
|
-
import {
|
|
13
|
+
import { dashboardEngineForTopology } from '../src/dashboard/gateway-adapter.js';
|
|
11
14
|
import { bulkAction, cancelRun, completeTaskRun, continueRun, deliverWebhook, failTaskRun, getFanMembers, getRun, getRunEvent, health, listRuns, listSchedules, metricsText, redispatchPendingRun, republishLifecycleRun, retryRun, retryWithInputRun, runValues, scheduleAction, signalRun, topology, updateRun, workers, workflowLifecycle, } from '../src/dashboard/handlers.js';
|
|
12
|
-
import { renderLoginPage } from '../src/dashboard/login_page.js';
|
|
13
15
|
import { openApiDocument } from '../src/dashboard/openapi.js';
|
|
14
16
|
import { contentTypeFor, renderIndexHtml } from '../src/dashboard/spa.js';
|
|
17
|
+
import { resolveConsoleTenantScope, scopedDashboardEngine, } from '../src/dashboard/tenant_scope.js';
|
|
18
|
+
import { registerDoctorChecks } from '../src/doctor/agora_doctor.js';
|
|
19
|
+
import { durableConsoleDoctorChecks } from '../src/doctor/checks.js';
|
|
15
20
|
import { WorkflowEngine } from '../src/index.js';
|
|
16
21
|
import { collectMetrics } from '../src/metrics.js';
|
|
17
22
|
import { DURABLE_TRANSPORT } from '../src/role_bindings.js';
|
|
23
|
+
import { tenancyEnabled, tenancySlot } from '../src/tenancy.js';
|
|
18
24
|
/**
|
|
19
25
|
* Directory of the built SPA (`dist/spa`, copied in by `copy:stubs` — see `package.json`), relative to
|
|
20
26
|
* this compiled provider (`dist/providers`). Resolved lazily (not at module scope) for the same reason
|
|
@@ -22,6 +28,8 @@ import { DURABLE_TRANSPORT } from '../src/role_bindings.js';
|
|
|
22
28
|
* on any non-`file:` `import.meta.url`, which would make this provider unimportable under a test
|
|
23
29
|
* runner that doesn't use real file URLs.
|
|
24
30
|
*/
|
|
31
|
+
/** This console's id in the app's `'agora.consoles'` registry. */
|
|
32
|
+
const CONSOLE_ID = 'durable';
|
|
25
33
|
let spaDir;
|
|
26
34
|
function spaDirectory() {
|
|
27
35
|
spaDir ??= fileURLToPath(new URL('../assets/spa/', import.meta.url));
|
|
@@ -57,8 +65,10 @@ function spaDirectory() {
|
|
|
57
65
|
* - `POST /api/bulk/:action` -> bulk retry/cancel every run matching a filter
|
|
58
66
|
* - `GET /api/health` -> worker-group health (compact shape)
|
|
59
67
|
* - `GET /api/workers` -> worker-group health (full heartbeats; SPA)
|
|
60
|
-
* - `GET /api/topology` -> this deployment's durable
|
|
68
|
+
* - `GET /api/topology` -> this deployment's durable topology
|
|
61
69
|
* - `GET /api/compat` -> fleet health / protocol-compatibility panel
|
|
70
|
+
* - `GET /api/consoles` -> the other Agora consoles this viewer may open (switcher)
|
|
71
|
+
* - `GET /api/consoles/search?q=` -> search across those consoles (and this one)
|
|
62
72
|
* - `GET /api/openapi.json` -> the machine-readable API contract (OpenAPI 3.1)
|
|
63
73
|
*/
|
|
64
74
|
export default class DashboardProvider {
|
|
@@ -66,65 +76,229 @@ export default class DashboardProvider {
|
|
|
66
76
|
constructor(app) {
|
|
67
77
|
this.app = app;
|
|
68
78
|
}
|
|
69
|
-
/** Warn once so a throwing `login`/`session` hook doesn't spam the logs on every failed attempt. */
|
|
70
|
-
warnedOnHookThrow = false;
|
|
71
79
|
warnedOnAuthorizeThrow = false;
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/** True (and reply 429) when this address exhausted its login window. */
|
|
77
|
-
loginRateLimited(ctx) {
|
|
78
|
-
const WINDOW_MS = 60_000;
|
|
79
|
-
const MAX_ATTEMPTS = 10;
|
|
80
|
-
const key = ctx.request.ip() || 'unknown';
|
|
81
|
-
const now = Date.now();
|
|
82
|
-
const recent = (this.loginAttempts.get(key) ?? []).filter((t) => now - t < WINDOW_MS);
|
|
83
|
-
if (recent.length >= MAX_ATTEMPTS) {
|
|
84
|
-
this.loginAttempts.set(key, recent);
|
|
85
|
-
ctx.response.status(429).json({ error: 'too many login attempts — retry in a minute' });
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
recent.push(now);
|
|
89
|
-
this.loginAttempts.set(key, recent);
|
|
90
|
-
// Bounded: prune other addresses opportunistically so the map can't grow without limit.
|
|
91
|
-
if (this.loginAttempts.size > 10_000)
|
|
92
|
-
this.loginAttempts.clear();
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
80
|
+
/** Each request's `authorize` verdict, read again for the console's tenant scope. */
|
|
81
|
+
decisions = new WeakMap();
|
|
82
|
+
/** Each request's resolved tenant scope. */
|
|
83
|
+
scopes = new WeakMap();
|
|
95
84
|
/** SSE connections currently open, per client address (and in total) — see streamRun. */
|
|
96
85
|
sseByAddress = new Map();
|
|
97
86
|
sseTotal = 0;
|
|
98
87
|
/**
|
|
99
88
|
* Captures the engine's `capability.unavailable` / `protocol.incompatible` diagnostics events so the
|
|
100
89
|
* `/compat` health panel can render each blocked run's structured delta + the live-fleet compat view
|
|
101
|
-
* (design §7.6, §10). Store-
|
|
90
|
+
* (design §7.6, §10). Store-topology only — attached in {@link boot}; a `tenant` pod owns no engine.
|
|
102
91
|
*/
|
|
103
92
|
diagnostics = new BlockedDiagnosticsRecorder();
|
|
104
93
|
/** Detach handle for the diagnostics subscription, released on {@link shutdown}. */
|
|
105
94
|
detachDiagnostics = null;
|
|
106
|
-
/** This process's event counters for `GET /metrics` (store
|
|
95
|
+
/** This process's event counters for `GET /metrics` (store topologies; a tenant pod has no engine). */
|
|
107
96
|
metricsCollector = null;
|
|
108
|
-
/** The active durable
|
|
109
|
-
|
|
110
|
-
return this.app.config.get('durable', {}).
|
|
97
|
+
/** The active durable topology (`standalone` when no `durable` config is present — today's default). */
|
|
98
|
+
durableTopology() {
|
|
99
|
+
return this.app.config.get('durable', {}).topology ?? 'standalone';
|
|
111
100
|
}
|
|
112
101
|
async boot() {
|
|
113
|
-
|
|
102
|
+
await registerDoctorChecks(this.app.container, durableConsoleDoctorChecks).catch(() => undefined);
|
|
103
|
+
const config = resolveConfig(this.app.config.get('durable_dashboard', {}), { inProduction: this.app.inProduction });
|
|
114
104
|
if (!config.enabled)
|
|
115
105
|
return;
|
|
116
|
-
|
|
106
|
+
if (!config.authorizeConfigured)
|
|
107
|
+
await this.warnNoAuthorize();
|
|
108
|
+
const durableTopology = this.durableTopology();
|
|
109
|
+
// Developer tools on the app's dev MCP endpoint, when `@adonis-agora/agent/mcp_provider` bound
|
|
110
|
+
// its hub (its `register()` ran before any `boot()`). Gated by this console's own `authorize`.
|
|
111
|
+
await this.registerDevTools(config, durableTopology);
|
|
112
|
+
// The console switcher: this console joins the app's `'agora.consoles'` registry so the other
|
|
113
|
+
// Agora consoles can link to it (and search it), and it can link to them.
|
|
114
|
+
await this.registerConsole(config, durableTopology);
|
|
117
115
|
// Route registration can't happen synchronously in `boot()` — see the long-standing note this
|
|
118
116
|
// provider always carried: the router singleton isn't committed until the app's "booted" hooks
|
|
119
117
|
// run, which fire strictly AFTER every provider's own `boot()`.
|
|
120
118
|
await this.app.booted(async () => {
|
|
121
119
|
const router = await this.app.container.make('router');
|
|
122
|
-
this.registerRoutes(router, config,
|
|
123
|
-
await this.attachDiagnostics(
|
|
120
|
+
this.registerRoutes(router, config, durableTopology);
|
|
121
|
+
await this.attachDiagnostics(durableTopology);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The console's read/control port, resolved per request (see {@link registerRoutes}). With tenancy
|
|
126
|
+
* on and a request at hand, it is confined to the tenants this viewer may see (see
|
|
127
|
+
* {@link consoleTenantScope}); the process counters, which span every tenant, are then left out
|
|
128
|
+
* unless the viewer sees every tenant.
|
|
129
|
+
*/
|
|
130
|
+
consoleDeps(config, durableTopology) {
|
|
131
|
+
return async (ctx) => {
|
|
132
|
+
const base = await dashboardEngineForTopology(durableTopology, this.app.container, WorkflowEngine);
|
|
133
|
+
const scope = ctx ? await this.consoleTenantScope(config, ctx) : undefined;
|
|
134
|
+
const slot = scope ? tenancySlot() : undefined;
|
|
135
|
+
const engine = scope
|
|
136
|
+
? scopedDashboardEngine(base, scope.ids, slot ? (fn) => slot.allTenants(fn) : undefined)
|
|
137
|
+
: base;
|
|
138
|
+
const metrics = scope && scope.ids !== undefined ? undefined : this.metricsCollector;
|
|
139
|
+
return {
|
|
140
|
+
engine,
|
|
141
|
+
...(config.redact ? { redact: config.redact } : {}),
|
|
142
|
+
...(metrics ? { metrics } : {}),
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Whether tenancy is on for durable (config/durable.ts `tenancy`, else config/context.ts). */
|
|
147
|
+
tenancyOn() {
|
|
148
|
+
return tenancyEnabled(this.app.config.get('durable', {}).tenancy);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The tenants this viewer may see in the console, once per request, or `undefined` with tenancy
|
|
152
|
+
* off. `authorize` may grant `tenants` (`'*'` or a list); otherwise it is the viewer's own active
|
|
153
|
+
* tenant. A `?tenant=` pick outside the grant is answered with nothing and audited.
|
|
154
|
+
*/
|
|
155
|
+
async consoleTenantScope(config, ctx) {
|
|
156
|
+
if (!this.tenancyOn())
|
|
157
|
+
return undefined;
|
|
158
|
+
const known = this.scopes.get(ctx);
|
|
159
|
+
if (known)
|
|
160
|
+
return known;
|
|
161
|
+
let decision = this.decisions.get(ctx);
|
|
162
|
+
if (decision === undefined) {
|
|
163
|
+
decision = await config.authorize(ctx);
|
|
164
|
+
this.decisions.set(ctx, decision);
|
|
165
|
+
}
|
|
166
|
+
const granted = decision.allowed ? decision.tenants : undefined;
|
|
167
|
+
const slot = tenancySlot();
|
|
168
|
+
const option = this.app.config.get('durable', {}).tenancy;
|
|
169
|
+
const kinds = typeof option === 'object' ? option.kinds : undefined;
|
|
170
|
+
// `tenancy.required` makes a missing tenant throw: in the console that is just "no organization".
|
|
171
|
+
let filter;
|
|
172
|
+
try {
|
|
173
|
+
filter = slot?.tenantFilter(kinds ? { kinds } : undefined);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
filter = undefined;
|
|
177
|
+
}
|
|
178
|
+
const own = filter?.mode === 'tenant'
|
|
179
|
+
? filter.tenant
|
|
180
|
+
: undefined;
|
|
181
|
+
const picked = ctx.request.qs().tenant;
|
|
182
|
+
const scope = resolveConsoleTenantScope({
|
|
183
|
+
granted,
|
|
184
|
+
own: own ? { id: own.id, kind: own.kind, slug: own.slug } : undefined,
|
|
185
|
+
selected: typeof picked === 'string' ? picked : undefined,
|
|
186
|
+
});
|
|
187
|
+
if (scope.forged) {
|
|
188
|
+
this.recordAudit(config, {
|
|
189
|
+
at: new Date(),
|
|
190
|
+
actor: auditActor(ctx),
|
|
191
|
+
method: 'TENANT',
|
|
192
|
+
path: `${ctx.request.url(false)} outside scope: tenant=${scope.selected ?? ''}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
this.scopes.set(ctx, scope);
|
|
196
|
+
return scope;
|
|
197
|
+
}
|
|
198
|
+
async registerDevTools(config, durableTopology) {
|
|
199
|
+
const deps = this.consoleDeps(config, durableTopology);
|
|
200
|
+
// An auxiliary feature: a hub that throws must not take the app's boot down with it.
|
|
201
|
+
try {
|
|
202
|
+
await this.registerDurableDevToolsInto(config, deps);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
try {
|
|
206
|
+
const logger = await this.app.container.make('logger');
|
|
207
|
+
logger.warn(`[durable_dashboard] could not register the developer tools: ${error instanceof Error ? error.message : String(error)}`);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
/* logging must never break boot */
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async registerDurableDevToolsInto(config, deps) {
|
|
215
|
+
await registerDurableDevTools(this.app.container, {
|
|
216
|
+
authorize: config.authorize,
|
|
217
|
+
deps,
|
|
218
|
+
audit: (entry) => this.recordAudit(config, {
|
|
219
|
+
at: new Date(),
|
|
220
|
+
actor: entry.actor,
|
|
221
|
+
method: 'MCP',
|
|
222
|
+
path: `${entry.tool} ${entry.target}`,
|
|
223
|
+
}),
|
|
224
|
+
compat: async () => {
|
|
225
|
+
const result = await compat(await this.compatSource((await deps()).engine));
|
|
226
|
+
return result.body;
|
|
227
|
+
},
|
|
124
228
|
});
|
|
125
229
|
}
|
|
126
|
-
async
|
|
127
|
-
|
|
230
|
+
async registerConsole(config, durableTopology) {
|
|
231
|
+
const deps = this.consoleDeps(config, durableTopology);
|
|
232
|
+
let logger;
|
|
233
|
+
try {
|
|
234
|
+
logger = await this.app.container.make('logger');
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
logger = undefined;
|
|
238
|
+
}
|
|
239
|
+
await registerAgoraConsole(this.app.container, {
|
|
240
|
+
id: CONSOLE_ID,
|
|
241
|
+
title: 'Workflows',
|
|
242
|
+
package: '@adonis-agora/durable',
|
|
243
|
+
icon: 'workflow',
|
|
244
|
+
path: config.path === '' ? '/' : config.path,
|
|
245
|
+
order: 10,
|
|
246
|
+
authorize: config.authorize,
|
|
247
|
+
search: durableConsoleSearch(async (ctx) => {
|
|
248
|
+
const d = await deps(ctx);
|
|
249
|
+
return {
|
|
250
|
+
getRun: (id) => d.engine.getRun(id),
|
|
251
|
+
workflows: async (q, limit) => {
|
|
252
|
+
const res = await runValues(d, {
|
|
253
|
+
params: {},
|
|
254
|
+
query: { field: 'workflow', search: q, limit: String(limit) },
|
|
255
|
+
});
|
|
256
|
+
return res.status === 200
|
|
257
|
+
? res.body
|
|
258
|
+
: [];
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}, config.path),
|
|
262
|
+
}, logger);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* What the console's "Organization" control shows: off (tenancy off), `none` (a viewer with no
|
|
266
|
+
* tenant), `fixed` (one tenant), or `pick` (several, or every tenant with `all`).
|
|
267
|
+
*/
|
|
268
|
+
async tenantPicker(config, ctx) {
|
|
269
|
+
const scope = await this.consoleTenantScope(config, ctx);
|
|
270
|
+
if (!scope)
|
|
271
|
+
return { mode: 'off', all: false, selected: null, tenants: [] };
|
|
272
|
+
if (scope.authorized === 'none')
|
|
273
|
+
return { mode: 'none', all: false, selected: null, tenants: [] };
|
|
274
|
+
let ids;
|
|
275
|
+
if (scope.authorized === '*') {
|
|
276
|
+
// Every tenant that has runs, read across tenants.
|
|
277
|
+
const base = await dashboardEngineForTopology(this.durableTopology(), this.app.container, WorkflowEngine);
|
|
278
|
+
const slot = tenancySlot();
|
|
279
|
+
// Paged: every organization that has runs, not only the first page of them.
|
|
280
|
+
ids = [];
|
|
281
|
+
const facets = base.runValueFacets?.bind(base);
|
|
282
|
+
for (let offset = 0; facets && offset < 10_000; offset += 200) {
|
|
283
|
+
const page = () => facets({ field: 'tenant' }, {}, { limit: 200, offset });
|
|
284
|
+
const rows = (await (slot ? slot.allTenants(page) : page())) ?? [];
|
|
285
|
+
for (const row of rows)
|
|
286
|
+
if (typeof row.value === 'string')
|
|
287
|
+
ids.push(row.value);
|
|
288
|
+
if (rows.length < 200)
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
ids = scope.authorized;
|
|
294
|
+
}
|
|
295
|
+
const labels = await tenantLabelsFor(ids, scope.own);
|
|
296
|
+
const tenants = ids.map((id) => ({ id, label: labels[id] ?? id }));
|
|
297
|
+
const mode = scope.authorized === '*' || ids.length > 1 ? 'pick' : 'fixed';
|
|
298
|
+
return { mode, all: scope.authorized === '*', selected: scope.selected ?? null, tenants };
|
|
299
|
+
}
|
|
300
|
+
async attachDiagnostics(durableTopology) {
|
|
301
|
+
if (durableTopology === 'tenant')
|
|
128
302
|
return;
|
|
129
303
|
// A warmed-up app (`node ace codegen`) never starts nor shuts down: resolving the engine would open
|
|
130
304
|
// the transport/store connections nothing closes, and the command would never exit. The routes are
|
|
@@ -142,23 +316,12 @@ export default class DashboardProvider {
|
|
|
142
316
|
// No store-backed engine available — the compat panel degrades to reason-only blocked runs.
|
|
143
317
|
}
|
|
144
318
|
}
|
|
145
|
-
registerRoutes(router, config,
|
|
319
|
+
registerRoutes(router, config, durableTopology) {
|
|
146
320
|
const apiBase = `${config.path}/api`;
|
|
147
|
-
// Resolve the dashboard's read/control port lazily per request, branched by
|
|
148
|
-
// a store
|
|
321
|
+
// Resolve the dashboard's read/control port lazily per request, branched by topology (design §5/§8):
|
|
322
|
+
// a store topology adapts the engine (built by @adonis-agora/durable's provider); a store-less `tenant`
|
|
149
323
|
// pod resolves the DURABLE_RUN_GATEWAY token (a ProxyRunGateway) and adapts it — NO engine, NO store.
|
|
150
|
-
const
|
|
151
|
-
const deps = async () => ({
|
|
152
|
-
engine: await resolveEngine(),
|
|
153
|
-
...(config.redact ? { redact: config.redact } : {}),
|
|
154
|
-
...(this.metricsCollector ? { metrics: this.metricsCollector } : {}),
|
|
155
|
-
});
|
|
156
|
-
// Built-in `dashboardAuth` (Mode A `session` and/or Mode B `login`, opt-in). Registered ONLY when
|
|
157
|
-
// configured, so omitting `dashboardAuth` leaves route registration byte-for-byte as it was. These
|
|
158
|
-
// endpoints are public (behind NEITHER guard): they MINT the session the guard checks for.
|
|
159
|
-
if (config.dashboardAuth) {
|
|
160
|
-
this.registerAuthRoutes(router, config, config.dashboardAuth);
|
|
161
|
-
}
|
|
324
|
+
const deps = this.consoleDeps(config, durableTopology);
|
|
162
325
|
this.registerSpaRoutes(router, config, apiBase);
|
|
163
326
|
// JSON API.
|
|
164
327
|
const json = (handler) => {
|
|
@@ -170,7 +333,7 @@ export default class DashboardProvider {
|
|
|
170
333
|
if (isMutatingMethod(ctx))
|
|
171
334
|
this.audit(config, ctx);
|
|
172
335
|
try {
|
|
173
|
-
const result = await handler(await deps(), toApiRequest(ctx));
|
|
336
|
+
const result = await handler(await deps(ctx), toApiRequest(ctx));
|
|
174
337
|
for (const [name, value] of Object.entries(result.headers ?? {})) {
|
|
175
338
|
ctx.response.header(name, value);
|
|
176
339
|
}
|
|
@@ -238,7 +401,7 @@ export default class DashboardProvider {
|
|
|
238
401
|
if (!(await this.enforce(config, ctx, 'api')))
|
|
239
402
|
return;
|
|
240
403
|
try {
|
|
241
|
-
const text = await metricsText(await deps());
|
|
404
|
+
const text = await metricsText(await deps(ctx));
|
|
242
405
|
ctx.response.header('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
243
406
|
return ctx.response.status(200).send(text);
|
|
244
407
|
}
|
|
@@ -263,6 +426,60 @@ export default class DashboardProvider {
|
|
|
263
426
|
.post(`${apiBase}/schedules/:key/:action`, json(scheduleAction))
|
|
264
427
|
.as('durable_dashboard.schedules.action');
|
|
265
428
|
router.post(`${apiBase}/bulk/:action`, json(bulkAction)).as('durable_dashboard.bulk');
|
|
429
|
+
// Tenancy: which organizations this viewer may pick, and an audited record of a switch.
|
|
430
|
+
router
|
|
431
|
+
.get(`${apiBase}/tenants`, async (ctx) => {
|
|
432
|
+
if (!(await this.enforce(config, ctx, 'api')))
|
|
433
|
+
return;
|
|
434
|
+
ctx.response.header('cache-control', 'private, no-store');
|
|
435
|
+
try {
|
|
436
|
+
return ctx.response.status(200).json(await this.tenantPicker(config, ctx));
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
void this.logServerError(error);
|
|
440
|
+
return ctx.response.status(500).json({ error: 'internal error' });
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
.as('durable_dashboard.tenants');
|
|
444
|
+
router
|
|
445
|
+
.post(`${apiBase}/tenants/:id/select`, async (ctx) => {
|
|
446
|
+
if (!(await this.enforce(config, ctx, 'api')))
|
|
447
|
+
return;
|
|
448
|
+
// The audit line (actor, `POST …/tenants/<id>/select`) is the record of the switch.
|
|
449
|
+
this.audit(config, ctx);
|
|
450
|
+
const scope = await this.consoleTenantScope(config, ctx);
|
|
451
|
+
const id = String(ctx.params.id);
|
|
452
|
+
const allowed = scope === undefined ||
|
|
453
|
+
id === '*' ||
|
|
454
|
+
scope.authorized === '*' ||
|
|
455
|
+
(Array.isArray(scope.authorized) && scope.authorized.includes(id));
|
|
456
|
+
return ctx.response
|
|
457
|
+
.status(allowed ? 200 : 404)
|
|
458
|
+
.json(allowed ? { tenant: id } : { error: 'not found' });
|
|
459
|
+
})
|
|
460
|
+
.as('durable_dashboard.tenants.select');
|
|
461
|
+
// The console switcher: the other Agora consoles this viewer may open, and a search across them.
|
|
462
|
+
router
|
|
463
|
+
.get(`${apiBase}/consoles`, async (ctx) => {
|
|
464
|
+
if (!(await this.enforce(config, ctx, 'api')))
|
|
465
|
+
return;
|
|
466
|
+
const registry = await readAgoraConsoles(this.app.container);
|
|
467
|
+
ctx.response.header('cache-control', 'private, no-store');
|
|
468
|
+
return ctx.response.status(200).json(await listVisibleConsoles(registry, CONSOLE_ID, ctx));
|
|
469
|
+
})
|
|
470
|
+
.as('durable_dashboard.consoles');
|
|
471
|
+
router
|
|
472
|
+
.get(`${apiBase}/consoles/search`, async (ctx) => {
|
|
473
|
+
if (!(await this.enforce(config, ctx, 'api')))
|
|
474
|
+
return;
|
|
475
|
+
const registry = await readAgoraConsoles(this.app.container);
|
|
476
|
+
const q = ctx.request.qs().q;
|
|
477
|
+
ctx.response.header('cache-control', 'private, no-store');
|
|
478
|
+
return ctx.response
|
|
479
|
+
.status(200)
|
|
480
|
+
.json(await searchConsoles(registry, CONSOLE_ID, ctx, typeof q === 'string' ? q : ''));
|
|
481
|
+
})
|
|
482
|
+
.as('durable_dashboard.consoles.search');
|
|
266
483
|
router
|
|
267
484
|
.get(`${apiBase}/health`, json((d) => health(d)))
|
|
268
485
|
.as('durable_dashboard.health');
|
|
@@ -276,8 +493,8 @@ export default class DashboardProvider {
|
|
|
276
493
|
// Only a `TenantConfig` carries a `partition` (its `<name>@<tenant>` routing suffix, spec
|
|
277
494
|
// §6.1) — the field this dashboard's `DurableTopology.tenant` badge actually names.
|
|
278
495
|
const durableConfig = this.app.config.get('durable', {});
|
|
279
|
-
const tenant = durableConfig.
|
|
280
|
-
const result = topology(
|
|
496
|
+
const tenant = durableConfig.topology === 'tenant' ? durableConfig.partition : undefined;
|
|
497
|
+
const result = topology(durableTopology, tenant);
|
|
281
498
|
return ctx.response.status(result.status).json(result.body);
|
|
282
499
|
})
|
|
283
500
|
.as('durable_dashboard.topology');
|
|
@@ -288,7 +505,11 @@ export default class DashboardProvider {
|
|
|
288
505
|
if (!(await this.enforce(config, ctx, 'api')))
|
|
289
506
|
return;
|
|
290
507
|
const id = ctx.params.id;
|
|
291
|
-
const engine = await
|
|
508
|
+
const { engine } = await deps(ctx);
|
|
509
|
+
// A run outside the viewer's tenants streams nothing: answer like an unknown run.
|
|
510
|
+
if ((await engine.getRun(id)) === null && this.tenancyOn()) {
|
|
511
|
+
return ctx.response.status(404).json({ error: `run ${id} not found` });
|
|
512
|
+
}
|
|
292
513
|
await this.streamRun(ctx, engine, id);
|
|
293
514
|
})
|
|
294
515
|
.as('durable_dashboard.runs.stream');
|
|
@@ -299,7 +520,7 @@ export default class DashboardProvider {
|
|
|
299
520
|
if (!(await this.enforce(config, ctx, 'api')))
|
|
300
521
|
return;
|
|
301
522
|
try {
|
|
302
|
-
const result = await compat(await this.compatSource(await
|
|
523
|
+
const result = await compat(await this.compatSource((await deps(ctx)).engine));
|
|
303
524
|
return ctx.response.status(result.status).json(result.body);
|
|
304
525
|
}
|
|
305
526
|
catch (error) {
|
|
@@ -309,6 +530,22 @@ export default class DashboardProvider {
|
|
|
309
530
|
})
|
|
310
531
|
.as('durable_dashboard.compat');
|
|
311
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* The console-shell fields of the page's data block: the switcher, ⌘K search (whenever a console
|
|
535
|
+
* registry exists — this console registers its own search), the signed-in viewer and the
|
|
536
|
+
* sign-out (the host's option, else the one an account console announced on the registry).
|
|
537
|
+
*/
|
|
538
|
+
async shellFields(config, ctx, apiBase) {
|
|
539
|
+
const registry = await readAgoraConsoles(this.app.container);
|
|
540
|
+
const viewer = await resolveViewer(ctx, config.viewer);
|
|
541
|
+
const signOut = resolveSignOut(ctx, config.signOut, registry);
|
|
542
|
+
return {
|
|
543
|
+
switcher: { endpoint: `${apiBase}/consoles`, others: otherConsoles(registry, CONSOLE_ID) },
|
|
544
|
+
...(registry ? { search: { endpoint: `${apiBase}/consoles/search` } } : {}),
|
|
545
|
+
...(viewer ? { viewer } : {}),
|
|
546
|
+
...(signOut ? { signOut } : {}),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
312
549
|
/** Mount the React SPA at `config.path` plus its hashed asset bundle. */
|
|
313
550
|
registerSpaRoutes(router, config, apiBase) {
|
|
314
551
|
const indexPath = config.path === '' ? '/' : config.path;
|
|
@@ -328,7 +565,7 @@ export default class DashboardProvider {
|
|
|
328
565
|
return ctx.response
|
|
329
566
|
.header('content-type', 'text/html; charset=utf-8')
|
|
330
567
|
.header('cache-control', 'no-store, must-revalidate')
|
|
331
|
-
.send(renderIndexHtml(html, config.path, apiBase));
|
|
568
|
+
.send(renderIndexHtml(html, config.path, apiBase, await this.shellFields(config, ctx, apiBase)));
|
|
332
569
|
})
|
|
333
570
|
.as('durable_dashboard.index');
|
|
334
571
|
router
|
|
@@ -424,131 +661,24 @@ export default class DashboardProvider {
|
|
|
424
661
|
return null;
|
|
425
662
|
}
|
|
426
663
|
}
|
|
427
|
-
/**
|
|
428
|
-
* Mount the built-in `dashboardAuth` endpoints under `basePath`. All are public (no guard): they
|
|
429
|
-
* create/destroy the session the {@link enforce} guard checks for.
|
|
430
|
-
*
|
|
431
|
-
* - `GET <base>/login` -> the server-rendered login page (Mode B only).
|
|
432
|
-
* - `POST <base>/login` -> verifies credentials via the host `login` hook and mints the cookie.
|
|
433
|
-
* - `POST <base>/session` -> Mode A: verifies the HOST APP's own auth (off the raw request) via the
|
|
434
|
-
* `session` hook and mints the cookie. No body — the hook reads whatever it needs off `request`.
|
|
435
|
-
* `204` on success, uniform `401` on denial. Mirrors `@dudousxd/nestjs-durable-dashboard`'s
|
|
436
|
-
* `DurableAuthController.session` and `client/console-session.ts`'s `mintDurableConsoleSession`.
|
|
437
|
-
* - `GET <base>/logout` -> clears the cookie and redirects to the login page (Mode B) or the
|
|
438
|
-
* dashboard root (Mode A only). A plain `GET` (idempotent, only ever destroys the caller's own
|
|
439
|
-
* session) so a simple `<a href>` works.
|
|
440
|
-
*/
|
|
441
|
-
registerAuthRoutes(router, config, auth) {
|
|
442
|
-
const loginPath = `${config.path}/login`;
|
|
443
|
-
const sessionPath = `${config.path}/session`;
|
|
444
|
-
const logoutPath = `${config.path}/logout`;
|
|
445
|
-
if (auth.login) {
|
|
446
|
-
router
|
|
447
|
-
.get(loginPath, async (ctx) => {
|
|
448
|
-
ctx.response.header('content-type', 'text/html; charset=utf-8');
|
|
449
|
-
ctx.response.header('cache-control', 'no-store, must-revalidate');
|
|
450
|
-
const qs = ctx.request.qs();
|
|
451
|
-
const nonce = cspNonce(ctx);
|
|
452
|
-
return ctx.response.send(renderLoginPage(config.path, {
|
|
453
|
-
...(nonce !== undefined ? { nonce } : {}),
|
|
454
|
-
error: qs.error !== undefined,
|
|
455
|
-
returnTo: qs.returnTo,
|
|
456
|
-
}));
|
|
457
|
-
})
|
|
458
|
-
.as('durable_dashboard.login.page');
|
|
459
|
-
// Two callers: the page's own `fetch` (JSON in, JSON out — `{ redirectTo }` / `{ error }`)
|
|
460
|
-
// and, with JavaScript off or its inline script dropped by a CSP, the same page as a classic
|
|
461
|
-
// form post (form-encoded in, a redirect out: to `returnTo` on success, back to the login
|
|
462
|
-
// page with `?error` on failure). Same hook, same cookie, same uniform failure either way.
|
|
463
|
-
router
|
|
464
|
-
.post(loginPath, async (ctx) => {
|
|
465
|
-
if (this.loginRateLimited(ctx))
|
|
466
|
-
return;
|
|
467
|
-
const body = ctx.request.body();
|
|
468
|
-
const outcome = await performLogin(auth, body, config.path);
|
|
469
|
-
const form = isFormPost(ctx);
|
|
470
|
-
const backToLogin = () => ctx.response
|
|
471
|
-
.redirect()
|
|
472
|
-
.withQs({
|
|
473
|
-
error: '1',
|
|
474
|
-
returnTo: sanitizeReturnTo(body?.returnTo, config.path || '/'),
|
|
475
|
-
})
|
|
476
|
-
.toPath(loginPath);
|
|
477
|
-
if (outcome.kind === 'bad-request') {
|
|
478
|
-
if (form)
|
|
479
|
-
return backToLogin();
|
|
480
|
-
return ctx.response.status(400).json({ error: outcome.message });
|
|
481
|
-
}
|
|
482
|
-
if (outcome.kind === 'unauthorized') {
|
|
483
|
-
void this.warnHookThrow(outcome.hookError).catch(() => undefined);
|
|
484
|
-
if (form)
|
|
485
|
-
return backToLogin();
|
|
486
|
-
return ctx.response.status(401).json({ error: outcome.message });
|
|
487
|
-
}
|
|
488
|
-
this.writeSessionCookie(ctx, auth, outcome.cookieValue);
|
|
489
|
-
if (form)
|
|
490
|
-
return ctx.response.redirect().toPath(outcome.redirectTo);
|
|
491
|
-
return ctx.response.status(200).json({ redirectTo: outcome.redirectTo });
|
|
492
|
-
})
|
|
493
|
-
.as('durable_dashboard.login.submit');
|
|
494
|
-
}
|
|
495
|
-
if (auth.session) {
|
|
496
|
-
router
|
|
497
|
-
.post(sessionPath, async (ctx) => {
|
|
498
|
-
if (this.loginRateLimited(ctx))
|
|
499
|
-
return;
|
|
500
|
-
const outcome = await performSession(auth, ctx.request.request);
|
|
501
|
-
if (outcome.kind === 'unauthorized') {
|
|
502
|
-
void this.warnHookThrow(outcome.hookError).catch(() => undefined);
|
|
503
|
-
return ctx.response.status(401).json({ error: 'unauthorized' });
|
|
504
|
-
}
|
|
505
|
-
this.writeSessionCookie(ctx, auth, outcome.cookieValue);
|
|
506
|
-
return ctx.response.status(204).send('');
|
|
507
|
-
})
|
|
508
|
-
.as('durable_dashboard.session.mint');
|
|
509
|
-
}
|
|
510
|
-
const logout = async (ctx) => {
|
|
511
|
-
ctx.response.clearCookie(SESSION_COOKIE_NAME, { path: '/' });
|
|
512
|
-
return ctx.response.redirect().toPath(auth.login ? loginPath : config.path || '/');
|
|
513
|
-
};
|
|
514
|
-
router.get(logoutPath, logout).as('durable_dashboard.logout');
|
|
515
|
-
// POST too: the CSRF-proof spelling for clients that prefer it (the GET stays for plain <a>
|
|
516
|
-
// links — it only ever destroys the caller's own session, so a forced logout is an annoyance,
|
|
517
|
-
// not a compromise).
|
|
518
|
-
router.post(logoutPath, logout).as('durable_dashboard.logout.post');
|
|
519
|
-
}
|
|
520
664
|
/** Record one mutating console action (see DashboardAuditEntry). Never throws into the request. */
|
|
521
665
|
audit(config, ctx) {
|
|
522
666
|
try {
|
|
523
|
-
|
|
524
|
-
const session = auth ? readSession(auth, this.readSessionCookie(ctx)) : null;
|
|
525
|
-
const fromToken = ctx.request.header('authorization') !== undefined ||
|
|
526
|
-
ctx.request.header('x-durable-token') !== undefined;
|
|
527
|
-
const actor = session?.name ?? session?.sub ?? (fromToken ? 'token' : 'anonymous');
|
|
528
|
-
const entry = {
|
|
667
|
+
this.recordAudit(config, {
|
|
529
668
|
at: new Date(),
|
|
530
|
-
actor,
|
|
669
|
+
actor: auditActor(ctx),
|
|
531
670
|
method: ctx.request.method().toUpperCase(),
|
|
532
671
|
path: ctx.request.url(false),
|
|
533
|
-
};
|
|
534
|
-
if (config.audit) {
|
|
535
|
-
// Typed `void`, but an async hook is assignable to it: catch its rejection too.
|
|
536
|
-
const pending = config.audit(entry);
|
|
537
|
-
if (pending instanceof Promise)
|
|
538
|
-
pending.catch(() => undefined);
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
void this.app.container
|
|
542
|
-
.make('logger')
|
|
543
|
-
.then((logger) => {
|
|
544
|
-
logger.info(`[durable_dashboard] audit actor=${entry.actor} ${entry.method} ${entry.path}`);
|
|
545
|
-
})
|
|
546
|
-
.catch(() => undefined);
|
|
672
|
+
});
|
|
547
673
|
}
|
|
548
674
|
catch {
|
|
549
675
|
/* auditing must never break the action */
|
|
550
676
|
}
|
|
551
677
|
}
|
|
678
|
+
/** Hand one audit entry to the host's sink, or the default logger line. Never throws. */
|
|
679
|
+
recordAudit(config, entry) {
|
|
680
|
+
recordConsoleAudit(config.audit, () => this.app.container.make('logger'), entry);
|
|
681
|
+
}
|
|
552
682
|
/** Route a handler 500's detail to the app logger (the client only ever sees a generic body). */
|
|
553
683
|
async logServerError(error) {
|
|
554
684
|
try {
|
|
@@ -567,95 +697,67 @@ export default class DashboardProvider {
|
|
|
567
697
|
const logger = await this.app.container.make('logger');
|
|
568
698
|
logger.warn(`[durable_dashboard] authorize hook threw; treating as denial (403). ${message}`);
|
|
569
699
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
700
|
+
/** The once-per-boot warning for a console with no `authorize` (open in dev, 403 in production). */
|
|
701
|
+
async warnNoAuthorize() {
|
|
702
|
+
try {
|
|
703
|
+
const logger = await this.app.container.make('logger');
|
|
704
|
+
logger.warn(NO_AUTHORIZE_WARNING);
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
console.warn(NO_AUTHORIZE_WARNING);
|
|
708
|
+
}
|
|
577
709
|
}
|
|
578
710
|
/**
|
|
579
|
-
* Run
|
|
580
|
-
* token/custom) with the optional `dashboardAuth` session guard — BOTH must pass:
|
|
711
|
+
* Run `authorize` for a dashboard resource. On a denial:
|
|
581
712
|
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
* 2. `dashboardAuth` configured AND no valid session -> for a `page` request, redirect `302` to
|
|
588
|
-
* the login page (Mode B) carrying a sanitized `returnTo`, or the `401` "open this console
|
|
589
|
-
* from your app" page (Mode A only, no login page to redirect to); for an `api` request, a
|
|
590
|
-
* plain `401 { error: 'unauthorized', auth: { modes } }` — the `modes` list lets the SPA's
|
|
591
|
-
* client (`durable-client.ts`) decide whether to offer the login page.
|
|
713
|
+
* - `{ redirectTo }`: a `page` request is redirected there (`302`); an `api` request gets
|
|
714
|
+
* `401 { error: 'unauthenticated', redirectTo }`;
|
|
715
|
+
* - `reason: 'unauthenticated'`: `401` (the access-denied page, or `{ error: 'unauthenticated' }`);
|
|
716
|
+
* - anything else (`false`, `{ allowed: false }`, a throw): `403` (the access-denied page, or
|
|
717
|
+
* `{ error: 'forbidden' }`).
|
|
592
718
|
*
|
|
593
|
-
*
|
|
719
|
+
* A hook that already answered the request (a redirect, a body) keeps its answer.
|
|
594
720
|
* Returns `false` (and has already written the response) when the request must short-circuit.
|
|
595
721
|
*/
|
|
596
722
|
async enforce(config, ctx, mode) {
|
|
597
|
-
// CSRF defense-in-depth
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
723
|
+
// CSRF defense-in-depth: a browser-originated CROSS-SITE mutating request is rejected outright,
|
|
724
|
+
// whatever `authorize` would say. The mutating endpoints need no body and read flags from the
|
|
725
|
+
// query string, so without this a malicious page could fire them as SIMPLE cross-origin POSTs
|
|
726
|
+
// (no preflight) against a console the operator's browser is signed in to. Non-browser clients
|
|
727
|
+
// send neither header and pass.
|
|
602
728
|
if (mode === 'api' && isMutatingMethod(ctx) && crossSiteRequest(ctx)) {
|
|
603
729
|
ctx.response.status(403).json({ error: 'cross-site request rejected' });
|
|
604
730
|
return false;
|
|
605
731
|
}
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
decision = normalizeAuthorizeResult(await config.authorize(ctx));
|
|
611
|
-
}
|
|
612
|
-
catch (error) {
|
|
613
|
-
void this.warnAuthorizeThrow(error).catch(() => undefined);
|
|
614
|
-
decision = { allowed: false, reason: 'forbidden' };
|
|
615
|
-
}
|
|
616
|
-
if (!decision.allowed) {
|
|
617
|
-
const unauthenticated = decision.reason === 'unauthenticated';
|
|
618
|
-
if (mode === 'page') {
|
|
619
|
-
await this.denyPage(ctx, config, unauthenticated
|
|
620
|
-
? { status: 401, reason: 'unauthenticated' }
|
|
621
|
-
: { status: 403, reason: 'forbidden' });
|
|
622
|
-
}
|
|
623
|
-
else if (!responseAnswered(ctx)) {
|
|
624
|
-
ctx.response
|
|
625
|
-
.status(unauthenticated ? 401 : 403)
|
|
626
|
-
.json({ error: unauthenticated ? 'unauthorized' : 'forbidden' });
|
|
627
|
-
}
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
const auth = config.dashboardAuth;
|
|
631
|
-
if (!auth)
|
|
732
|
+
// The resolved gate never throws: a throwing `authorize` comes back as a 403 carrying the error.
|
|
733
|
+
const decision = await config.authorize(ctx);
|
|
734
|
+
this.decisions.set(ctx, decision);
|
|
735
|
+
if (decision.allowed)
|
|
632
736
|
return true;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
// never set them); a session WITH roles must carry `operator` (or `admin`) to mutate.
|
|
637
|
-
// Previously every authenticated principal — including a read-only support login — could
|
|
638
|
-
// bulk-cancel with compensation or fix-and-replay.
|
|
639
|
-
if (mode === 'api' && isMutatingMethod(ctx) && !sessionCanMutate(session)) {
|
|
640
|
-
ctx.response
|
|
641
|
-
.status(403)
|
|
642
|
-
.json({ error: "this session's roles do not include 'operator' (or 'admin')" });
|
|
643
|
-
return false;
|
|
644
|
-
}
|
|
645
|
-
return true;
|
|
646
|
-
}
|
|
737
|
+
if (decision.error !== undefined)
|
|
738
|
+
void this.warnAuthorizeThrow(decision.error).catch(() => undefined);
|
|
739
|
+
const unauthenticated = decision.reason === 'unauthenticated';
|
|
647
740
|
if (mode === 'page') {
|
|
648
|
-
if (
|
|
649
|
-
|
|
650
|
-
ctx.response.redirect().withQs('returnTo', returnTo).toPath(`${config.path}/login`);
|
|
741
|
+
if (decision.redirectTo !== undefined && !responseAnswered(ctx)) {
|
|
742
|
+
ctx.response.redirect().toPath(decision.redirectTo);
|
|
651
743
|
return false;
|
|
652
744
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
745
|
+
await this.denyPage(ctx, config, unauthenticated
|
|
746
|
+
? { status: 401, reason: 'unauthenticated' }
|
|
747
|
+
: { status: 403, reason: 'forbidden' });
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
if (responseAnswered(ctx))
|
|
656
751
|
return false;
|
|
752
|
+
if (!unauthenticated) {
|
|
753
|
+
ctx.response.status(403).json({ error: 'forbidden' });
|
|
754
|
+
}
|
|
755
|
+
else if (decision.redirectTo !== undefined) {
|
|
756
|
+
ctx.response.status(401).json({ error: 'unauthenticated', redirectTo: decision.redirectTo });
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
ctx.response.status(401).json({ error: 'unauthenticated' });
|
|
657
760
|
}
|
|
658
|
-
ctx.response.status(401).json({ error: 'unauthorized', auth: { modes: auth.modes } });
|
|
659
761
|
return false;
|
|
660
762
|
}
|
|
661
763
|
/**
|
|
@@ -672,7 +774,6 @@ export default class DashboardProvider {
|
|
|
672
774
|
const info = {
|
|
673
775
|
...denial,
|
|
674
776
|
basePath: config.path,
|
|
675
|
-
...(config.dashboardAuth?.login ? { loginHref: `${config.path}/login` } : {}),
|
|
676
777
|
...(nonce !== undefined ? { nonce } : {}),
|
|
677
778
|
};
|
|
678
779
|
const html = await resolveAccessDeniedPage(info, config.accessDenied, ctx, answered);
|
|
@@ -684,22 +785,6 @@ export default class DashboardProvider {
|
|
|
684
785
|
.header('cache-control', 'no-store, must-revalidate')
|
|
685
786
|
.send(html);
|
|
686
787
|
}
|
|
687
|
-
readSessionCookie(ctx) {
|
|
688
|
-
const value = ctx.request.plainCookie(SESSION_COOKIE_NAME, undefined, false);
|
|
689
|
-
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
690
|
-
}
|
|
691
|
-
writeSessionCookie(ctx, auth, value) {
|
|
692
|
-
ctx.response.plainCookie(SESSION_COOKIE_NAME, value, {
|
|
693
|
-
httpOnly: true,
|
|
694
|
-
sameSite: 'lax',
|
|
695
|
-
// Forced on in production even when `request.secure()` reads false (a TLS-terminating proxy
|
|
696
|
-
// without AdonisJS trustProxy) — exactly the deployments that most need the Secure flag.
|
|
697
|
-
secure: isProduction() || ctx.request.secure(),
|
|
698
|
-
path: '/',
|
|
699
|
-
maxAge: Math.floor(auth.ttlMs / 1000),
|
|
700
|
-
encode: false,
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
788
|
async shutdown() {
|
|
704
789
|
this.detachDiagnostics?.();
|
|
705
790
|
this.detachDiagnostics = null;
|
|
@@ -707,12 +792,6 @@ export default class DashboardProvider {
|
|
|
707
792
|
this.metricsCollector = null;
|
|
708
793
|
}
|
|
709
794
|
}
|
|
710
|
-
/**
|
|
711
|
-
* Whether something already answered this request: a redirect (`location` header — the signal the
|
|
712
|
-
* `authorize` contract has always honoured) or a body queued on the response. The body check reads
|
|
713
|
-
* AdonisJS's `response.hasLazyBody` structurally so a plain-object `ctx` double in a unit test
|
|
714
|
-
* (which has neither) still works.
|
|
715
|
-
*/
|
|
716
795
|
/** Whether this request can change state (everything but GET/HEAD/OPTIONS). Exported for tests. */
|
|
717
796
|
export function isMutatingMethod(ctx) {
|
|
718
797
|
const method = ctx.request.method().toUpperCase();
|
|
@@ -739,15 +818,12 @@ export function crossSiteRequest(ctx) {
|
|
|
739
818
|
return true; // an unparseable Origin is not a browser being honest — reject
|
|
740
819
|
}
|
|
741
820
|
}
|
|
742
|
-
/**
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return role === 'operator' || role === 'admin';
|
|
749
|
-
});
|
|
750
|
-
}
|
|
821
|
+
/**
|
|
822
|
+
* Whether something already answered this request: a redirect (`location` header — the signal the
|
|
823
|
+
* `authorize` contract has always honoured) or a body queued on the response. The body check reads
|
|
824
|
+
* AdonisJS's `response.hasLazyBody` structurally so a plain-object `ctx` double in a unit test
|
|
825
|
+
* (which has neither) still works.
|
|
826
|
+
*/
|
|
751
827
|
function responseAnswered(ctx) {
|
|
752
828
|
if (ctx.response.getHeader('location'))
|
|
753
829
|
return true;
|
|
@@ -764,21 +840,56 @@ function cspNonce(ctx) {
|
|
|
764
840
|
return typeof nonce === 'string' && nonce !== '' ? nonce : undefined;
|
|
765
841
|
}
|
|
766
842
|
/**
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
843
|
+
* Who a mutating console action is attributed to: the signed-in user (`ctx.auth.user`'s `email`,
|
|
844
|
+
* `name` or `id`, read structurally — this package does not depend on `@adonisjs/auth`), `'bearer'`
|
|
845
|
+
* for a caller with an `Authorization` header and no user, else `'anonymous'`.
|
|
770
846
|
*/
|
|
771
|
-
function
|
|
772
|
-
const
|
|
773
|
-
|
|
847
|
+
export function auditActor(ctx) {
|
|
848
|
+
const user = ctx.auth?.user;
|
|
849
|
+
if (user) {
|
|
850
|
+
for (const key of ['email', 'name', 'id']) {
|
|
851
|
+
const value = user[key];
|
|
852
|
+
if (typeof value === 'string' && value !== '')
|
|
853
|
+
return value;
|
|
854
|
+
if (typeof value === 'number')
|
|
855
|
+
return String(value);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return ctx.request.header('authorization') !== undefined ? 'bearer' : 'anonymous';
|
|
774
859
|
}
|
|
775
860
|
/** Adapt an AdonisJS `HttpContext` to the framework-light {@link ApiRequest}. */
|
|
776
861
|
function toApiRequest(ctx) {
|
|
777
862
|
return {
|
|
778
863
|
params: ctx.params,
|
|
779
|
-
|
|
864
|
+
// `tenant` picks the console's organization (see consoleTenantScope); it is not a run filter.
|
|
865
|
+
query: withoutTenantParam(ctx.request.qs()),
|
|
780
866
|
body: ctx.request.body(),
|
|
781
867
|
headers: { 'if-none-match': ctx.request.header('if-none-match') },
|
|
782
868
|
};
|
|
783
869
|
}
|
|
870
|
+
function withoutTenantParam(query) {
|
|
871
|
+
if (!('tenant' in query))
|
|
872
|
+
return query;
|
|
873
|
+
const { tenant: _tenant, ...rest } = query;
|
|
874
|
+
return rest;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Display labels for tenant ids: the viewer's own tenant by its slug, the rest through context's
|
|
878
|
+
* label resolver (`tenancy.label` in config/context.ts, or the one authkit installs), else the id.
|
|
879
|
+
*/
|
|
880
|
+
async function tenantLabelsFor(ids, own) {
|
|
881
|
+
const labels = {};
|
|
882
|
+
const slot = tenancySlot();
|
|
883
|
+
if (slot?.tenantLabels && ids.length > 0) {
|
|
884
|
+
try {
|
|
885
|
+
Object.assign(labels, await slot.tenantLabels(ids));
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
/* labels are cosmetic: fall back to ids */
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (own?.slug && labels[own.id] === undefined)
|
|
892
|
+
labels[own.id] = own.slug;
|
|
893
|
+
return labels;
|
|
894
|
+
}
|
|
784
895
|
//# sourceMappingURL=dashboard_provider.js.map
|