@alneas/eaos-server 0.0.0-stage → 0.2.0
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 +41 -0
- package/LICENSE +21 -0
- package/README.ja.md +49 -0
- package/README.md +46 -2
- package/dist/agentic/agent-jobs.d.ts +333 -0
- package/dist/agentic/agent-jobs.js +15 -0
- package/dist/agentic/agent-jobs.js.map +1 -0
- package/dist/agentic/agent-node.d.ts +40 -0
- package/dist/agentic/agent-node.js +7 -0
- package/dist/agentic/agent-node.js.map +1 -0
- package/dist/agentic/agent-runner.d.ts +33 -0
- package/dist/agentic/agent-runner.js +7 -0
- package/dist/agentic/agent-runner.js.map +1 -0
- package/dist/agentic/ai-broker.d.ts +169 -0
- package/dist/agentic/ai-broker.js +8 -0
- package/dist/agentic/ai-broker.js.map +1 -0
- package/dist/agentic/atomic-checkpointer.d.ts +137 -0
- package/dist/agentic/atomic-checkpointer.js +9 -0
- package/dist/agentic/atomic-checkpointer.js.map +1 -0
- package/dist/agentic/autonomous-worker.d.ts +122 -0
- package/dist/agentic/autonomous-worker.js +10 -0
- package/dist/agentic/autonomous-worker.js.map +1 -0
- package/dist/agentic/event-bridge.d.ts +350 -0
- package/dist/agentic/event-bridge.js +9 -0
- package/dist/agentic/event-bridge.js.map +1 -0
- package/dist/agentic/executor.d.ts +91 -0
- package/dist/agentic/executor.js +12 -0
- package/dist/agentic/executor.js.map +1 -0
- package/dist/agentic/graph-projection.d.ts +119 -0
- package/dist/agentic/graph-projection.js +7 -0
- package/dist/agentic/graph-projection.js.map +1 -0
- package/dist/agentic/graph.d.ts +524 -0
- package/dist/agentic/graph.js +7 -0
- package/dist/agentic/graph.js.map +1 -0
- package/dist/agentic/index.d.ts +28 -0
- package/dist/agentic/index.js +87 -0
- package/dist/agentic/index.js.map +1 -0
- package/dist/agentic/local-runner.d.ts +106 -0
- package/dist/agentic/local-runner.js +7 -0
- package/dist/agentic/local-runner.js.map +1 -0
- package/dist/agentic/remote-executor.d.ts +67 -0
- package/dist/agentic/remote-executor.js +8 -0
- package/dist/agentic/remote-executor.js.map +1 -0
- package/dist/agentic/types.d.ts +157 -0
- package/dist/agentic/types.js +7 -0
- package/dist/agentic/types.js.map +1 -0
- package/dist/agentic/workflow-catalog.d.ts +152 -0
- package/dist/agentic/workflow-catalog.js +7 -0
- package/dist/agentic/workflow-catalog.js.map +1 -0
- package/dist/agentic/workflow-compiler.d.ts +44 -0
- package/dist/agentic/workflow-compiler.js +8 -0
- package/dist/agentic/workflow-compiler.js.map +1 -0
- package/dist/agentic/workflow-coordinator.d.ts +170 -0
- package/dist/agentic/workflow-coordinator.js +8 -0
- package/dist/agentic/workflow-coordinator.js.map +1 -0
- package/dist/agentic/workflow-manager.d.ts +91 -0
- package/dist/agentic/workflow-manager.js +7 -0
- package/dist/agentic/workflow-manager.js.map +1 -0
- package/dist/agentic/workflow-registry.d.ts +334 -0
- package/dist/agentic/workflow-registry.js +7 -0
- package/dist/agentic/workflow-registry.js.map +1 -0
- package/dist/artifacts-CteHmCMI.d.ts +614 -0
- package/dist/auth-hooks/api-key.d.ts +75 -0
- package/dist/auth-hooks/api-key.js +7 -0
- package/dist/auth-hooks/api-key.js.map +1 -0
- package/dist/auth-hooks/index.d.ts +80 -0
- package/dist/auth-hooks/index.js +28 -0
- package/dist/auth-hooks/index.js.map +1 -0
- package/dist/browser-auth/account-memberships.d.ts +213 -0
- package/dist/browser-auth/account-memberships.js +13 -0
- package/dist/browser-auth/account-memberships.js.map +1 -0
- package/dist/browser-auth/index.d.ts +8 -0
- package/dist/browser-auth/index.js +59 -0
- package/dist/browser-auth/index.js.map +1 -0
- package/dist/browser-auth/kratos-session.d.ts +37 -0
- package/dist/browser-auth/kratos-session.js +7 -0
- package/dist/browser-auth/kratos-session.js.map +1 -0
- package/dist/browser-auth/platform-session.d.ts +120 -0
- package/dist/browser-auth/platform-session.js +7 -0
- package/dist/browser-auth/platform-session.js.map +1 -0
- package/dist/browser-auth/public-memberships.d.ts +22 -0
- package/dist/browser-auth/public-memberships.js +7 -0
- package/dist/browser-auth/public-memberships.js.map +1 -0
- package/dist/cache/index.d.ts +181 -0
- package/dist/cache/index.js +197 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/chunk-32746UQZ.js +131 -0
- package/dist/chunk-32746UQZ.js.map +1 -0
- package/dist/chunk-3NLSWP2Y.js +223 -0
- package/dist/chunk-3NLSWP2Y.js.map +1 -0
- package/dist/chunk-3P4XTTQH.js +346 -0
- package/dist/chunk-3P4XTTQH.js.map +1 -0
- package/dist/chunk-3VITSHN3.js +15 -0
- package/dist/chunk-3VITSHN3.js.map +1 -0
- package/dist/chunk-46UWPEW2.js +226 -0
- package/dist/chunk-46UWPEW2.js.map +1 -0
- package/dist/chunk-4BARRLJR.js +180 -0
- package/dist/chunk-4BARRLJR.js.map +1 -0
- package/dist/chunk-4VORUFKR.js +45 -0
- package/dist/chunk-4VORUFKR.js.map +1 -0
- package/dist/chunk-55D4ERWO.js +1 -0
- package/dist/chunk-55D4ERWO.js.map +1 -0
- package/dist/chunk-5TBBZIND.js +18 -0
- package/dist/chunk-5TBBZIND.js.map +1 -0
- package/dist/chunk-5WOZWPZA.js +211 -0
- package/dist/chunk-5WOZWPZA.js.map +1 -0
- package/dist/chunk-6CBVZ6CT.js +168 -0
- package/dist/chunk-6CBVZ6CT.js.map +1 -0
- package/dist/chunk-6XZXYNJR.js +74 -0
- package/dist/chunk-6XZXYNJR.js.map +1 -0
- package/dist/chunk-753HWDSC.js +222 -0
- package/dist/chunk-753HWDSC.js.map +1 -0
- package/dist/chunk-7AHSS7SO.js +81 -0
- package/dist/chunk-7AHSS7SO.js.map +1 -0
- package/dist/chunk-7FTJN4SA.js +1 -0
- package/dist/chunk-7FTJN4SA.js.map +1 -0
- package/dist/chunk-AVDMLTPF.js +24 -0
- package/dist/chunk-AVDMLTPF.js.map +1 -0
- package/dist/chunk-BKAHUEIT.js +95 -0
- package/dist/chunk-BKAHUEIT.js.map +1 -0
- package/dist/chunk-CJ7OFEF7.js +134 -0
- package/dist/chunk-CJ7OFEF7.js.map +1 -0
- package/dist/chunk-EJKWDZIU.js +111 -0
- package/dist/chunk-EJKWDZIU.js.map +1 -0
- package/dist/chunk-F2J6IQ6I.js +247 -0
- package/dist/chunk-F2J6IQ6I.js.map +1 -0
- package/dist/chunk-F3O7CELI.js +23 -0
- package/dist/chunk-F3O7CELI.js.map +1 -0
- package/dist/chunk-F4IITPGX.js +221 -0
- package/dist/chunk-F4IITPGX.js.map +1 -0
- package/dist/chunk-FD6VC2NA.js +66 -0
- package/dist/chunk-FD6VC2NA.js.map +1 -0
- package/dist/chunk-FRRRWAHO.js +14 -0
- package/dist/chunk-FRRRWAHO.js.map +1 -0
- package/dist/chunk-HCKAGLLB.js +17 -0
- package/dist/chunk-HCKAGLLB.js.map +1 -0
- package/dist/chunk-HVC7FG6B.js +191 -0
- package/dist/chunk-HVC7FG6B.js.map +1 -0
- package/dist/chunk-HXFVOR7Z.js +496 -0
- package/dist/chunk-HXFVOR7Z.js.map +1 -0
- package/dist/chunk-IJWYS2PF.js +122 -0
- package/dist/chunk-IJWYS2PF.js.map +1 -0
- package/dist/chunk-INCHXAFK.js +1395 -0
- package/dist/chunk-INCHXAFK.js.map +1 -0
- package/dist/chunk-JT6BRGQM.js +1 -0
- package/dist/chunk-JT6BRGQM.js.map +1 -0
- package/dist/chunk-L6PMAEMB.js +115 -0
- package/dist/chunk-L6PMAEMB.js.map +1 -0
- package/dist/chunk-LOUGW6MU.js +126 -0
- package/dist/chunk-LOUGW6MU.js.map +1 -0
- package/dist/chunk-LYPZPE53.js +129 -0
- package/dist/chunk-LYPZPE53.js.map +1 -0
- package/dist/chunk-NNCCN7JP.js +186 -0
- package/dist/chunk-NNCCN7JP.js.map +1 -0
- package/dist/chunk-PBC3MZUL.js +212 -0
- package/dist/chunk-PBC3MZUL.js.map +1 -0
- package/dist/chunk-R4ADP2DC.js +20 -0
- package/dist/chunk-R4ADP2DC.js.map +1 -0
- package/dist/chunk-RHN4Y6VS.js +44 -0
- package/dist/chunk-RHN4Y6VS.js.map +1 -0
- package/dist/chunk-SDJNYPYK.js +770 -0
- package/dist/chunk-SDJNYPYK.js.map +1 -0
- package/dist/chunk-UGNTCOVR.js +28 -0
- package/dist/chunk-UGNTCOVR.js.map +1 -0
- package/dist/chunk-VC2PCK2S.js +31 -0
- package/dist/chunk-VC2PCK2S.js.map +1 -0
- package/dist/chunk-VF5YDZJC.js +305 -0
- package/dist/chunk-VF5YDZJC.js.map +1 -0
- package/dist/chunk-VISRDVTK.js +675 -0
- package/dist/chunk-VISRDVTK.js.map +1 -0
- package/dist/chunk-W6O3LVBY.js +102 -0
- package/dist/chunk-W6O3LVBY.js.map +1 -0
- package/dist/chunk-WFEAKJLR.js +47 -0
- package/dist/chunk-WFEAKJLR.js.map +1 -0
- package/dist/chunk-XIEY2OF4.js +67 -0
- package/dist/chunk-XIEY2OF4.js.map +1 -0
- package/dist/chunk-XITLHDG3.js +37 -0
- package/dist/chunk-XITLHDG3.js.map +1 -0
- package/dist/chunk-XMWPYZWZ.js +61 -0
- package/dist/chunk-XMWPYZWZ.js.map +1 -0
- package/dist/chunk-XUP6CNOM.js +34 -0
- package/dist/chunk-XUP6CNOM.js.map +1 -0
- package/dist/chunk-YABTQZDJ.js +25 -0
- package/dist/chunk-YABTQZDJ.js.map +1 -0
- package/dist/chunk-YMHVRBUU.js +199 -0
- package/dist/chunk-YMHVRBUU.js.map +1 -0
- package/dist/chunk-YMJ7BRKU.js +510 -0
- package/dist/chunk-YMJ7BRKU.js.map +1 -0
- package/dist/chunk-ZC3XWU7B.js +28 -0
- package/dist/chunk-ZC3XWU7B.js.map +1 -0
- package/dist/chunk-ZHMTD5KV.js +90 -0
- package/dist/chunk-ZHMTD5KV.js.map +1 -0
- package/dist/chunk-ZWZDLOE7.js +160 -0
- package/dist/chunk-ZWZDLOE7.js.map +1 -0
- package/dist/db-package/db/local-first-sqlite-adapter.d.ts +432 -0
- package/dist/db-package/db/local-first-sqlite-adapter.js +7 -0
- package/dist/db-package/db/local-first-sqlite-adapter.js.map +1 -0
- package/dist/db-package/index.d.ts +7 -0
- package/dist/db-package/index.js +16 -0
- package/dist/db-package/index.js.map +1 -0
- package/dist/db-package/migration/aggregator.d.ts +86 -0
- package/dist/db-package/migration/aggregator.js +7 -0
- package/dist/db-package/migration/aggregator.js.map +1 -0
- package/dist/db-package/migration/index.d.ts +3 -0
- package/dist/db-package/migration/index.js +8 -0
- package/dist/db-package/migration/index.js.map +1 -0
- package/dist/engine.d.ts +205 -0
- package/dist/engine.js +20 -0
- package/dist/engine.js.map +1 -0
- package/dist/entitlements/index.d.ts +313 -0
- package/dist/entitlements/index.js +23 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/execution/chat.d.ts +7 -0
- package/dist/execution/chat.js +7 -0
- package/dist/execution/chat.js.map +1 -0
- package/dist/execution/index.d.ts +342 -0
- package/dist/execution/index.js +66 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/tool-agent.d.ts +7 -0
- package/dist/execution/tool-agent.js +8 -0
- package/dist/execution/tool-agent.js.map +1 -0
- package/dist/extensions/index.d.ts +172 -0
- package/dist/extensions/index.js +95 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/health/index.d.ts +35 -0
- package/dist/health/index.js +7 -0
- package/dist/health/index.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/index.d.ts +133 -0
- package/dist/lifecycle/index.js +13 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle-engine/backend-migration.d.ts +2 -0
- package/dist/lifecycle-engine/backend-migration.js +11 -0
- package/dist/lifecycle-engine/backend-migration.js.map +1 -0
- package/dist/lifecycle-engine/baseline.d.ts +2 -0
- package/dist/lifecycle-engine/baseline.js +9 -0
- package/dist/lifecycle-engine/baseline.js.map +1 -0
- package/dist/lifecycle-engine/index.d.ts +1497 -0
- package/dist/lifecycle-engine/index.js +41 -0
- package/dist/lifecycle-engine/index.js.map +1 -0
- package/dist/lifecycle-engine/platform/artifacts.d.ts +4 -0
- package/dist/lifecycle-engine/platform/artifacts.js +7 -0
- package/dist/lifecycle-engine/platform/artifacts.js.map +1 -0
- package/dist/lifecycle-engine/platform/control-plane.d.ts +12 -0
- package/dist/lifecycle-engine/platform/control-plane.js +2 -0
- package/dist/lifecycle-engine/platform/control-plane.js.map +1 -0
- package/dist/lifecycle-engine/platform/database-router.d.ts +118 -0
- package/dist/lifecycle-engine/platform/database-router.js +7 -0
- package/dist/lifecycle-engine/platform/database-router.js.map +1 -0
- package/dist/lifecycle-engine/platform/deployment-placements.d.ts +464 -0
- package/dist/lifecycle-engine/platform/deployment-placements.js +16 -0
- package/dist/lifecycle-engine/platform/deployment-placements.js.map +1 -0
- package/dist/lifecycle-engine/platform/deployment.d.ts +4 -0
- package/dist/lifecycle-engine/platform/deployment.js +17 -0
- package/dist/lifecycle-engine/platform/deployment.js.map +1 -0
- package/dist/lifecycle-engine/platform/index.d.ts +737 -0
- package/dist/lifecycle-engine/platform/index.js +60 -0
- package/dist/lifecycle-engine/platform/index.js.map +1 -0
- package/dist/lifecycle-engine/platform/runtime.d.ts +80 -0
- package/dist/lifecycle-engine/platform/runtime.js +11 -0
- package/dist/lifecycle-engine/platform/runtime.js.map +1 -0
- package/dist/lifecycle-engine/platform/site-publication.d.ts +102 -0
- package/dist/lifecycle-engine/platform/site-publication.js +7 -0
- package/dist/lifecycle-engine/platform/site-publication.js.map +1 -0
- package/dist/lifecycle-engine/platform/user-directory.d.ts +139 -0
- package/dist/lifecycle-engine/platform/user-directory.js +13 -0
- package/dist/lifecycle-engine/platform/user-directory.js.map +1 -0
- package/dist/mcp.d.ts +92 -0
- package/dist/mcp.js +10 -0
- package/dist/mcp.js.map +1 -0
- package/dist/middleware/auth.d.ts +118 -0
- package/dist/middleware/auth.js +7 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +35 -0
- package/dist/middleware/error-handler.js +7 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/module-guard.d.ts +33 -0
- package/dist/middleware/module-guard.js +7 -0
- package/dist/middleware/module-guard.js.map +1 -0
- package/dist/platform-admin/index.d.ts +37 -0
- package/dist/platform-admin/index.js +11 -0
- package/dist/platform-admin/index.js.map +1 -0
- package/dist/platform-admin/store.d.ts +346 -0
- package/dist/platform-admin/store.js +7 -0
- package/dist/platform-admin/store.js.map +1 -0
- package/dist/plugin-data/annotations.d.ts +6 -0
- package/dist/plugin-data/annotations.js +12 -0
- package/dist/plugin-data/annotations.js.map +1 -0
- package/dist/plugin-data/atomic-context.d.ts +41 -0
- package/dist/plugin-data/atomic-context.js +9 -0
- package/dist/plugin-data/atomic-context.js.map +1 -0
- package/dist/plugin-data/atomic.d.ts +6 -0
- package/dist/plugin-data/atomic.js +26 -0
- package/dist/plugin-data/atomic.js.map +1 -0
- package/dist/plugin-data/audit.d.ts +6 -0
- package/dist/plugin-data/audit.js +12 -0
- package/dist/plugin-data/audit.js.map +1 -0
- package/dist/plugin-data/commands.d.ts +6 -0
- package/dist/plugin-data/commands.js +8 -0
- package/dist/plugin-data/commands.js.map +1 -0
- package/dist/plugin-data/composite.d.ts +6 -0
- package/dist/plugin-data/composite.js +8 -0
- package/dist/plugin-data/composite.js.map +1 -0
- package/dist/plugin-data/delivery.d.ts +6 -0
- package/dist/plugin-data/delivery.js +16 -0
- package/dist/plugin-data/delivery.js.map +1 -0
- package/dist/plugin-data/index.d.ts +2798 -0
- package/dist/plugin-data/index.js +102 -0
- package/dist/plugin-data/index.js.map +1 -0
- package/dist/plugin-data/json-documents.d.ts +6 -0
- package/dist/plugin-data/json-documents.js +16 -0
- package/dist/plugin-data/json-documents.js.map +1 -0
- package/dist/plugin-data/outbox.d.ts +6 -0
- package/dist/plugin-data/outbox.js +16 -0
- package/dist/plugin-data/outbox.js.map +1 -0
- package/dist/plugin-data/scoped-records.d.ts +6 -0
- package/dist/plugin-data/scoped-records.js +10 -0
- package/dist/plugin-data/scoped-records.js.map +1 -0
- package/dist/plugin-sdk.d.ts +41 -0
- package/dist/plugin-sdk.js +10 -0
- package/dist/plugin-sdk.js.map +1 -0
- package/dist/sql-lifecycle/index.d.ts +138 -0
- package/dist/sql-lifecycle/index.js +167 -0
- package/dist/sql-lifecycle/index.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/s3-compatible.d.ts +139 -0
- package/dist/storage/s3-compatible.js +11 -0
- package/dist/storage/s3-compatible.js.map +1 -0
- package/dist/tenant.d.ts +108 -0
- package/dist/tenant.js +9 -0
- package/dist/tenant.js.map +1 -0
- package/dist/workspace-context.d.ts +119 -0
- package/dist/workspace-context.js +9 -0
- package/dist/workspace-context.js.map +1 -0
- package/docs/api.md +30560 -0
- package/docs/db/README.md +49 -0
- package/docs/db/docs/api.md +1613 -0
- package/docs/db/docs/guide.md +47 -0
- package/docs/entitlements.md +60 -0
- package/docs/guide.md +92 -0
- package/docs/index.md +57 -0
- package/docs/ja/db/README.md +46 -0
- package/docs/ja/db/docs/guide.md +53 -0
- package/docs/ja/guide.md +46 -0
- package/docs/ja/lifecycle/README.md +46 -0
- package/docs/ja/lifecycle/docs/guide.md +45 -0
- package/docs/ja/storage/README.md +46 -0
- package/docs/ja/storage/docs/guide.md +43 -0
- package/docs/lifecycle/README.md +49 -0
- package/docs/lifecycle/docs/api.md +3770 -0
- package/docs/lifecycle/docs/baseline.md +50 -0
- package/docs/lifecycle/docs/guide.md +47 -0
- package/docs/storage/README.md +49 -0
- package/docs/storage/docs/api.md +470 -0
- package/docs/storage/docs/guide.md +47 -0
- package/package.json +354 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-JT6BRGQM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/lifecycle/index.ts
|
|
2
|
+
import { mkdir, readFile, open, rename, unlink, rmdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createMiddleware } from "hono/factory";
|
|
6
|
+
import { emptyLifecycleState, normalizeLifecycleState } from "@alneas/eaos-server/lifecycle-engine";
|
|
7
|
+
var FileLifecycleStore = class {
|
|
8
|
+
/** Absolute ledger path resolved at construction; lock directory is path + ".lock". */
|
|
9
|
+
path;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a host-selected ledger filename without reading or creating it.
|
|
12
|
+
* @param path - Ledger filename, resolved relative to the current process directory.
|
|
13
|
+
*/
|
|
14
|
+
constructor(path) {
|
|
15
|
+
this.path = resolve(path);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reads and normalizes the JSON lifecycle ledger.
|
|
19
|
+
* @returns Stored lifecycle state, or a fresh empty state if the file does not exist.
|
|
20
|
+
* @throws Rejects filesystem, JSON parsing, or state-normalization failures other than ENOENT.
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { FileLifecycleStore } from "@alneas/eaos-server/lifecycle";
|
|
24
|
+
* const state = await new FileLifecycleStore("./state/lifecycle.json").read();
|
|
25
|
+
* console.log(state.platform);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
async read() {
|
|
29
|
+
try {
|
|
30
|
+
return normalizeLifecycleState(JSON.parse(await readFile(this.path, "utf8")));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code === "ENOENT") return emptyLifecycleState();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Runs one writer callback under an exclusive filesystem lock directory.
|
|
38
|
+
* @typeParam T - Callback result passed through unchanged.
|
|
39
|
+
* @param fn - Called after lock acquisition with ledger read and durable write operations; must await its writes before returning.
|
|
40
|
+
* @returns The callback result after lock directory removal. Writer.write resolves without a value after temporary-file fsync, rename, and directory fsync.
|
|
41
|
+
* @throws Rejects an existing lock, filesystem failures, callback failures, or final lock-removal failures.
|
|
42
|
+
* @remarks Lock acquisition does not wait or retry. Ordinary callback failure removes the lock; a process crash can leave it behind and requires operator reconciliation. Writes may have committed before a later failure. The store does not enforce that all external writers use this lock.
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { FileLifecycleStore } from "@alneas/eaos-server/lifecycle";
|
|
46
|
+
* const store = new FileLifecycleStore("./state/lifecycle.json");
|
|
47
|
+
* const state = await store.withLock(async writer => writer.read());
|
|
48
|
+
* console.log(state.platform);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
async withLock(fn) {
|
|
52
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
53
|
+
const lock = this.path + ".lock";
|
|
54
|
+
try {
|
|
55
|
+
await mkdir(lock, { mode: 448 });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code === "EEXIST") throw new Error("Lifecycle writer is locked; a crashed operation requires reconciliation", { cause: error });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await fn({ read: () => this.read(), write: (state) => this.write(state) });
|
|
62
|
+
} finally {
|
|
63
|
+
await rmdir(lock);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async write(state) {
|
|
67
|
+
const temporary = this.path + "." + randomUUID() + ".tmp";
|
|
68
|
+
try {
|
|
69
|
+
const handle = await open(temporary, "wx", 384);
|
|
70
|
+
try {
|
|
71
|
+
await handle.writeFile(JSON.stringify(state) + "\n");
|
|
72
|
+
await handle.sync();
|
|
73
|
+
} finally {
|
|
74
|
+
await handle.close();
|
|
75
|
+
}
|
|
76
|
+
await rename(temporary, this.path);
|
|
77
|
+
const directory = await open(dirname(this.path), "r");
|
|
78
|
+
try {
|
|
79
|
+
await directory.sync();
|
|
80
|
+
} finally {
|
|
81
|
+
await directory.close();
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
await unlink(temporary).catch((error) => {
|
|
85
|
+
if (error.code !== "ENOENT") throw error;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
async function resolveLifecycleBackend(runtime, tenantId) {
|
|
91
|
+
const backendId = runtime.resolveBackend ? await runtime.resolveBackend(tenantId) : runtime.backendId;
|
|
92
|
+
if (!backendId) throw new Error("Tenant lifecycle target is unavailable");
|
|
93
|
+
return backendId;
|
|
94
|
+
}
|
|
95
|
+
async function runLifecycleJob(runtime, job, callback) {
|
|
96
|
+
return runtime.lifecycle.run(await resolveLifecycleBackend(runtime, job.tenantId), job.tenantId, job.pluginId, callback);
|
|
97
|
+
}
|
|
98
|
+
function createLifecycleGuard(runtime, pluginId) {
|
|
99
|
+
return createMiddleware(async (c, next) => {
|
|
100
|
+
const tenantId = await runtime.resolveTenant(c);
|
|
101
|
+
if (!tenantId) return c.json({ error: "Unauthorized" }, 401);
|
|
102
|
+
const backendId = runtime.resolveBackend ? await runtime.resolveBackend(tenantId) : runtime.backendId;
|
|
103
|
+
if (!backendId || !await runtime.lifecycle.isEnabled(backendId, tenantId, pluginId)) return c.json({ error: "Plugin is inactive" }, 403);
|
|
104
|
+
c.set("tenantId", tenantId);
|
|
105
|
+
await next();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
FileLifecycleStore,
|
|
111
|
+
resolveLifecycleBackend,
|
|
112
|
+
runLifecycleJob,
|
|
113
|
+
createLifecycleGuard
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=chunk-L6PMAEMB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lifecycle/index.ts"],"sourcesContent":["import { mkdir, readFile, open, rename, unlink, rmdir } from 'node:fs/promises'\nimport { dirname, resolve } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport type { Context } from 'hono'\nimport { createMiddleware } from 'hono/factory'\nimport { emptyLifecycleState, normalizeLifecycleState, type LifecycleStore, type LifecycleWriter, type LifecycleState, type PluginLifecycle } from '@alneas/eaos-server/lifecycle-engine'\n\n/** Single control-plane filesystem. All backend processes must read the same ledger.\n * A crashed writer leaves its lock in place: reconcile DB/infra before operator recovery.\n * For independent hosts, implement LifecycleStore using a shared transactional control DB.\n * @example\n * ```typescript\n * import { FileLifecycleStore } from \"@alneas/eaos-server/lifecycle\";\n * const store = new FileLifecycleStore(\"./state/lifecycle.json\");\n * console.log((await store.read()).platform);\n * ```\n */\nexport class FileLifecycleStore implements LifecycleStore {\n /** Absolute ledger path resolved at construction; lock directory is path + \".lock\". */\n readonly path: string\n /**\n * Resolves a host-selected ledger filename without reading or creating it.\n * @param path - Ledger filename, resolved relative to the current process directory.\n */\n constructor(path: string) { this.path = resolve(path) }\n /**\n * Reads and normalizes the JSON lifecycle ledger.\n * @returns Stored lifecycle state, or a fresh empty state if the file does not exist.\n * @throws Rejects filesystem, JSON parsing, or state-normalization failures other than ENOENT.\n * @example\n * ```typescript\n * import { FileLifecycleStore } from \"@alneas/eaos-server/lifecycle\";\n * const state = await new FileLifecycleStore(\"./state/lifecycle.json\").read();\n * console.log(state.platform);\n * ```\n */\n async read(): Promise<LifecycleState> {\n try { return normalizeLifecycleState(JSON.parse(await readFile(this.path, 'utf8')) as LifecycleState) }\n catch (error: any) { if (error.code === 'ENOENT') return emptyLifecycleState(); throw error }\n }\n /**\n * Runs one writer callback under an exclusive filesystem lock directory.\n * @typeParam T - Callback result passed through unchanged.\n * @param fn - Called after lock acquisition with ledger read and durable write operations; must await its writes before returning.\n * @returns The callback result after lock directory removal. Writer.write resolves without a value after temporary-file fsync, rename, and directory fsync.\n * @throws Rejects an existing lock, filesystem failures, callback failures, or final lock-removal failures.\n * @remarks Lock acquisition does not wait or retry. Ordinary callback failure removes the lock; a process crash can leave it behind and requires operator reconciliation. Writes may have committed before a later failure. The store does not enforce that all external writers use this lock.\n * @example\n * ```typescript\n * import { FileLifecycleStore } from \"@alneas/eaos-server/lifecycle\";\n * const store = new FileLifecycleStore(\"./state/lifecycle.json\");\n * const state = await store.withLock(async writer => writer.read());\n * console.log(state.platform);\n * ```\n */\n async withLock<T>(fn: (writer: LifecycleWriter) => Promise<T>): Promise<T> {\n await mkdir(dirname(this.path), { recursive: true })\n const lock = this.path + '.lock'\n try { await mkdir(lock, { mode: 0o700 }) }\n catch (error: any) { if (error.code === 'EEXIST') throw new Error('Lifecycle writer is locked; a crashed operation requires reconciliation', { cause: error }); throw error }\n try {\n return await fn({ read: () => this.read(), write: state => this.write(state) })\n } finally { await rmdir(lock) }\n }\n private async write(state: LifecycleState): Promise<void> {\n const temporary = this.path + '.' + randomUUID() + '.tmp'\n try {\n const handle = await open(temporary, 'wx', 0o600)\n try { await handle.writeFile(JSON.stringify(state) + '\\n'); await handle.sync() } finally { await handle.close() }\n await rename(temporary, this.path)\n const directory = await open(dirname(this.path), 'r')\n try { await directory.sync() } finally { await directory.close() }\n } finally { await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error }) }\n }\n}\n/**\n * Host identity, backend routing, and lifecycle admission used by jobs and HTTP guards.\n * @see createLifecycleGuard.\n */\nexport interface LifecycleRuntime {\n /** Lifecycle engine consulted for current plugin admission. */\n lifecycle: PluginLifecycle\n /** Fallback registered backend ID used only when resolveBackend is absent. */\n backendId: string\n /** Resolve the registered target for a verified tenant; null denies admission without fallback. */\n resolveBackend?: (tenantId: string) => Promise<string | null>\n /** Must resolve authenticated identity / verified webhook signature, never a raw tenant header alone. */\n resolveTenant: (context: Context) => Promise<string | null>\n}\n/**\n * Resolves the host-selected backend for a verified tenant without falling back after a resolver denial.\n * @param runtime - Lifecycle runtime with either a backend resolver or fixed backend ID.\n * @param tenantId - Verified tenant whose placement is being resolved.\n * @returns Nonempty resolved backend ID, or runtime.backendId when no callback exists.\n * @throws Rejects an empty/null backend result or propagated resolver errors.\n * @example\n * ```typescript\n * import { resolveLifecycleBackend, type LifecycleRuntime } from \"@alneas/eaos-server/lifecycle\";\n * export async function backend(runtime: LifecycleRuntime, tenantId: string) {\n * return resolveLifecycleBackend(runtime, tenantId);\n * }\n * ```\n */\nexport async function resolveLifecycleBackend(runtime: LifecycleRuntime, tenantId: string): Promise<string> {\n const backendId = runtime.resolveBackend ? await runtime.resolveBackend(tenantId) : runtime.backendId\n if (!backendId) throw new Error('Tenant lifecycle target is unavailable')\n return backendId\n}\n/**\n * Runs a host job through lifecycle admission for its verified tenant and plugin.\n * @typeParam T - Result resolved by the admitted callback.\n * @param runtime - Backend routing and lifecycle engine.\n * @param job - Trusted tenantId/pluginId binding of the queued job.\n * @param callback - Invoked without arguments by lifecycle.run after admission; resolves the job result.\n * @returns The callback result after lifecycle.run completes.\n * @throws Rejects unavailable backend, lifecycle admission/store failure, or callback failure.\n * @remarks Persistence, retries, and idempotency of the job body belong to the host. This wrapper delegates concurrency/drain behavior to lifecycle.run.\n * @example\n * ```typescript\n * import { runLifecycleJob, type LifecycleRuntime } from \"@alneas/eaos-server/lifecycle\";\n * export async function runJob<T>(runtime: LifecycleRuntime, tenantId: string, pluginId: string, execute: () => Promise<T>) {\n * return runLifecycleJob(runtime, { tenantId, pluginId }, execute);\n * }\n * ```\n */\nexport async function runLifecycleJob<T>(runtime: LifecycleRuntime, job: {\n /**\n * Verified tenant identity of the queued job, used to select its lifecycle backend.\n */\ntenantId: string;\n /**\n * Exact plugin ID whose current lifecycle admission gates this job body.\n */\npluginId: string }, callback: () => Promise<T>): Promise<T> {\n return runtime.lifecycle.run(await resolveLifecycleBackend(runtime, job.tenantId), job.tenantId, job.pluginId, callback)\n}\n/**\n * Creates a request guard requiring a verified tenant and currently enabled plugin placement.\n * @param runtime - Host tenant verification, backend routing, and lifecycle engine.\n * @param pluginId - Exact installed plugin ID required by the route.\n * @returns Hono middleware returning 401 for absent tenant, 403 for unavailable backend/inactive plugin, or completing downstream handling without a value after setting tenantId.\n * @throws Request handling propagates host resolver, lifecycle store, and downstream failures.\n * @remarks Authenticate webhook signatures or browser identity in resolveTenant; a raw header is insufficient. This guard performs a point-in-time isEnabled check and does not hold a lifecycle.run execution lease across downstream work.\n * @example\n * ```typescript\n * import { Hono } from \"hono\";\n * import { createLifecycleGuard, type LifecycleRuntime } from \"@alneas/eaos-server/lifecycle\";\n * export function pluginRoutes(runtime: LifecycleRuntime) {\n * const app = new Hono();\n * app.use(\"*\", createLifecycleGuard(runtime, \"example-plugin\"));\n * app.get(\"/\", c => c.json({ available: true }));\n * return app;\n * }\n * ```\n */\nexport function createLifecycleGuard(runtime: LifecycleRuntime, pluginId: string) {\n return createMiddleware(async (c, next) => {\n const tenantId = await runtime.resolveTenant(c)\n if (!tenantId) return c.json({ error: 'Unauthorized' }, 401)\n const backendId = runtime.resolveBackend ? await runtime.resolveBackend(tenantId) : runtime.backendId\n if (!backendId || !await runtime.lifecycle.isEnabled(backendId, tenantId, pluginId)) return c.json({ error: 'Plugin is inactive' }, 403)\n c.set('tenantId', tenantId)\n await next()\n })\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/lifecycle-engine/platform/database-router.ts
|
|
2
|
+
var TenantDatabaseRouter = class {
|
|
3
|
+
/**
|
|
4
|
+
* Snapshots configured physical database adapters and requires a single shared dialect.
|
|
5
|
+
* @param resolveTarget - Resolve the verified tenant to its trusted lifecycle backend; synchronous errors propagate.
|
|
6
|
+
* @param adapters - Nonempty map keyed by physical databaseId; adapter objects are shared and later owned by close().
|
|
7
|
+
* @throws Error for no adapters or multiple adapter dialects.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';
|
|
12
|
+
* import type { DatabaseAdapter } from '@alneas/eaos-core/db/adapter';
|
|
13
|
+
* import type { BackendTarget } from '@alneas/eaos-server/lifecycle-engine';
|
|
14
|
+
* export function connectRouter(resolve: (verifiedTenantId: string) => BackendTarget, adapters: ReadonlyMap<string, DatabaseAdapter>) { return new TenantDatabaseRouter(resolve, adapters); }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
constructor(resolveTarget, adapters) {
|
|
18
|
+
this.resolveTarget = resolveTarget;
|
|
19
|
+
this.adapters = new Map(adapters);
|
|
20
|
+
const types = new Set([...this.adapters.values()].map((adapter) => adapter.type));
|
|
21
|
+
if (!this.adapters.size || types.size !== 1) throw new Error("Tenant DB routing requires configured adapters of one dialect");
|
|
22
|
+
this.type = types.values().next().value;
|
|
23
|
+
}
|
|
24
|
+
resolveTarget;
|
|
25
|
+
/**
|
|
26
|
+
* The single SQL dialect shared by every configured adapter.
|
|
27
|
+
*/
|
|
28
|
+
type;
|
|
29
|
+
adapters;
|
|
30
|
+
/**
|
|
31
|
+
* Rejects unscoped business database access to require a verified tenant context.
|
|
32
|
+
* @typeParam T - Callback result type required by DatabaseAdapter; this method always rejects.
|
|
33
|
+
* @param _domain - Unused business domain; no connection is opened.
|
|
34
|
+
* @param _callback - Unused callback; it is never invoked.
|
|
35
|
+
* @returns A rejected Promise; this method never resolves a callback result.
|
|
36
|
+
* @throws Always rejects with the verified-context requirement.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';
|
|
41
|
+
* export async function rejectUnscoped(router: TenantDatabaseRouter) { await router.withConnection('reports', async () => undefined); }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
withConnection(_domain, _callback) {
|
|
45
|
+
return Promise.reject(new Error("Business DB routing requires a verified tenant context; use a separate control adapter for administration"));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Delegates one verified tenant connection to its exact configured physical database adapter.
|
|
49
|
+
* @typeParam T - Value resolved by work on the selected tenant connection.
|
|
50
|
+
* @param domain - Business domain forwarded to the selected adapter.
|
|
51
|
+
* @param callback - Async work invoked by the adapter with its connection; must enforce resource permission.
|
|
52
|
+
* @param context - Verified nonblank trimmed tenant session; a copied context is forwarded.
|
|
53
|
+
* @returns The selected adapter callback result.
|
|
54
|
+
* @throws Rejects invalid tenant, dedicated mismatch, missing adapter or adapter/callback failure; target resolver errors may throw synchronously.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* Dedicated target tenant must match. Missing target adapters and dedicated failures never fall back to shared storage. Cancellation/transaction/connection cleanup follow the selected adapter contract.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';
|
|
62
|
+
* export function withVerifiedTenant<T>(router: TenantDatabaseRouter, tenantId: string, work: (connection: unknown) => Promise<T>) { return router.withTenantConnection('reports', work, { tenantId }); }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
withTenantConnection(domain, callback, context) {
|
|
66
|
+
if (!context?.tenantId || context.tenantId === "*" || context.tenantId !== context.tenantId.trim()) return Promise.reject(new Error("Verified tenant context is required"));
|
|
67
|
+
const target = this.resolveTarget(context.tenantId);
|
|
68
|
+
if (target.mode === "dedicated" && target.tenantId !== context.tenantId) return Promise.reject(new Error("Dedicated database tenant mismatch"));
|
|
69
|
+
const adapter = this.adapters.get(target.databaseId);
|
|
70
|
+
if (!adapter) return Promise.reject(new Error("Tenant DB adapter is not configured; shared fallback is disabled"));
|
|
71
|
+
return adapter.withTenantConnection(domain, callback, { ...context });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Checks every distinct configured database adapter concurrently.
|
|
75
|
+
* @returns True only when every adapter ping resolves true; false if any resolves false.
|
|
76
|
+
* @throws Rejects if any adapter ping rejects.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';
|
|
81
|
+
* export const health = (router: TenantDatabaseRouter) => router.ping();
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
async ping() {
|
|
85
|
+
return (await Promise.all([...new Set(this.adapters.values())].map((adapter) => adapter.ping()))).every(Boolean);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Aggregates connection and pool counts from every distinct configured adapter.
|
|
89
|
+
* @returns ConnectionStats with the router dialect and summed counts.
|
|
90
|
+
* @throws Propagates adapter getStats errors.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';
|
|
95
|
+
* export const stats = (router: TenantDatabaseRouter) => router.getStats();
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
getStats() {
|
|
99
|
+
const stats = [...new Set(this.adapters.values())].map((adapter) => adapter.getStats());
|
|
100
|
+
return { driver: this.type, activeConnections: stats.reduce((n, s) => n + s.activeConnections, 0), poolSize: stats.reduce((n, s) => n + s.poolSize, 0) };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Closes all distinct configured adapters and collects asynchronous cleanup failures.
|
|
104
|
+
* @returns Resolves without a value after every adapter close settles successfully.
|
|
105
|
+
* @throws Rejects with AggregateError for one or more rejected close Promises; synchronous adapter errors may also reject.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* The router does not mark itself closed or make repeated calls idempotent; each adapter owns its lifecycle semantics. A synchronously throwing close implementation can interrupt initial dispatch.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';
|
|
113
|
+
* export async function shutdown(router: TenantDatabaseRouter) { await router.close(); }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
async close() {
|
|
117
|
+
const results = await Promise.allSettled([...new Set(this.adapters.values())].map((adapter) => adapter.close()));
|
|
118
|
+
const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
119
|
+
if (errors.length) throw new AggregateError(errors, "Tenant DB adapters failed to close");
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
TenantDatabaseRouter
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=chunk-LOUGW6MU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lifecycle-engine/platform/database-router.ts"],"sourcesContent":["import type { DatabaseAdapter, TenantSessionContext, ConnectionStats } from '@alneas/eaos-core/db/adapter'\nimport type { BackendTarget } from '../index'\n\n/**\n * Routes verified tenant operations to isolated existing database pools without implicit fallback.\n * \n * \n * One execution host, isolated pools per DB. No implicit shared fallback or unscoped business access.\n * @remarks\n * All configured adapters must share one dialect. The host supplies membership-verified tenant context, trusted target resolution and real adapter pools. Unscoped business connections always reject; administration requires a separate control adapter. close owns and closes every distinct supplied adapter.\n * \n * @example\n * ```typescript\n * import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';\n * import type { DatabaseAdapter } from '@alneas/eaos-core/db/adapter';\n * import type { BackendTarget } from '@alneas/eaos-server/lifecycle-engine';\n * export function connectRouter(resolve: (verifiedTenantId: string) => BackendTarget, adapters: ReadonlyMap<string, DatabaseAdapter>) { return new TenantDatabaseRouter(resolve, adapters); }\n * ```\n */\nexport class TenantDatabaseRouter implements DatabaseAdapter {\n /**\n * The single SQL dialect shared by every configured adapter.\n */\n readonly type: DatabaseAdapter['type']\n private readonly adapters: Map<string, DatabaseAdapter>\n /**\n * Snapshots configured physical database adapters and requires a single shared dialect.\n * @param resolveTarget - Resolve the verified tenant to its trusted lifecycle backend; synchronous errors propagate.\n * @param adapters - Nonempty map keyed by physical databaseId; adapter objects are shared and later owned by close().\n * @throws Error for no adapters or multiple adapter dialects.\n * \n * @example\n * ```typescript\n * import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';\n * import type { DatabaseAdapter } from '@alneas/eaos-core/db/adapter';\n * import type { BackendTarget } from '@alneas/eaos-server/lifecycle-engine';\n * export function connectRouter(resolve: (verifiedTenantId: string) => BackendTarget, adapters: ReadonlyMap<string, DatabaseAdapter>) { return new TenantDatabaseRouter(resolve, adapters); }\n * ```\n */\n constructor(private readonly resolveTarget: (verifiedTenantId: string) => BackendTarget, adapters: ReadonlyMap<string, DatabaseAdapter>) {\n this.adapters = new Map(adapters)\n const types = new Set([...this.adapters.values()].map(adapter => adapter.type))\n if (!this.adapters.size || types.size !== 1) throw new Error('Tenant DB routing requires configured adapters of one dialect')\n this.type = types.values().next().value!\n }\n /**\n * Rejects unscoped business database access to require a verified tenant context.\n * @typeParam T - Callback result type required by DatabaseAdapter; this method always rejects.\n * @param _domain - Unused business domain; no connection is opened.\n * @param _callback - Unused callback; it is never invoked.\n * @returns A rejected Promise; this method never resolves a callback result.\n * @throws Always rejects with the verified-context requirement.\n * \n * @example\n * ```typescript\n * import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';\n * export async function rejectUnscoped(router: TenantDatabaseRouter) { await router.withConnection('reports', async () => undefined); }\n * ```\n */\n withConnection<T>(_domain: string, _callback: (connection: unknown) => Promise<T>): Promise<T> {\n return Promise.reject(new Error('Business DB routing requires a verified tenant context; use a separate control adapter for administration'))\n }\n /**\n * Delegates one verified tenant connection to its exact configured physical database adapter.\n * @typeParam T - Value resolved by work on the selected tenant connection.\n * @param domain - Business domain forwarded to the selected adapter.\n * @param callback - Async work invoked by the adapter with its connection; must enforce resource permission.\n * @param context - Verified nonblank trimmed tenant session; a copied context is forwarded.\n * @returns The selected adapter callback result.\n * @throws Rejects invalid tenant, dedicated mismatch, missing adapter or adapter/callback failure; target resolver errors may throw synchronously.\n * \n * @remarks\n * Dedicated target tenant must match. Missing target adapters and dedicated failures never fall back to shared storage. Cancellation/transaction/connection cleanup follow the selected adapter contract.\n * \n * @example\n * ```typescript\n * import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';\n * export function withVerifiedTenant<T>(router: TenantDatabaseRouter, tenantId: string, work: (connection: unknown) => Promise<T>) { return router.withTenantConnection('reports', work, { tenantId }); }\n * ```\n */\n withTenantConnection<T>(domain: string, callback: (connection: unknown) => Promise<T>, context: TenantSessionContext): Promise<T> {\n if (!context?.tenantId || context.tenantId === '*' || context.tenantId !== context.tenantId.trim()) return Promise.reject(new Error('Verified tenant context is required'))\n const target = this.resolveTarget(context.tenantId)\n if (target.mode === 'dedicated' && target.tenantId !== context.tenantId) return Promise.reject(new Error('Dedicated database tenant mismatch'))\n const adapter = this.adapters.get(target.databaseId)\n if (!adapter) return Promise.reject(new Error('Tenant DB adapter is not configured; shared fallback is disabled'))\n return adapter.withTenantConnection(domain, callback, { ...context })\n }\n /**\n * Checks every distinct configured database adapter concurrently.\n * @returns True only when every adapter ping resolves true; false if any resolves false.\n * @throws Rejects if any adapter ping rejects.\n * \n * @example\n * ```typescript\n * import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';\n * export const health = (router: TenantDatabaseRouter) => router.ping();\n * ```\n */\n async ping(): Promise<boolean> { return (await Promise.all([...new Set(this.adapters.values())].map(adapter => adapter.ping()))).every(Boolean) }\n /**\n * Aggregates connection and pool counts from every distinct configured adapter.\n * @returns ConnectionStats with the router dialect and summed counts.\n * @throws Propagates adapter getStats errors.\n * \n * @example\n * ```typescript\n * import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';\n * export const stats = (router: TenantDatabaseRouter) => router.getStats();\n * ```\n */\n getStats(): ConnectionStats {\n const stats = [...new Set(this.adapters.values())].map(adapter => adapter.getStats())\n return { driver: this.type, activeConnections: stats.reduce((n, s) => n + s.activeConnections, 0), poolSize: stats.reduce((n, s) => n + s.poolSize, 0) }\n }\n /**\n * Closes all distinct configured adapters and collects asynchronous cleanup failures.\n * @returns Resolves without a value after every adapter close settles successfully.\n * @throws Rejects with AggregateError for one or more rejected close Promises; synchronous adapter errors may also reject.\n * \n * @remarks\n * The router does not mark itself closed or make repeated calls idempotent; each adapter owns its lifecycle semantics. A synchronously throwing close implementation can interrupt initial dispatch.\n * \n * @example\n * ```typescript\n * import { TenantDatabaseRouter } from '@alneas/eaos-server/platform/database-router';\n * export async function shutdown(router: TenantDatabaseRouter) { await router.close(); }\n * ```\n */\n async close(): Promise<void> {\n const results = await Promise.allSettled([...new Set(this.adapters.values())].map(adapter => adapter.close()))\n const errors = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected').map(result => result.reason)\n if (errors.length) throw new AggregateError(errors, 'Tenant DB adapters failed to close')\n }\n}\n"],"mappings":";AAmBO,IAAM,uBAAN,MAAsD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoB3D,YAA6B,eAA4D,UAAgD;AAA5G;AAC3B,SAAK,WAAW,IAAI,IAAI,QAAQ;AAChC,UAAM,QAAQ,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,EAAE,IAAI,aAAW,QAAQ,IAAI,CAAC;AAC9E,QAAI,CAAC,KAAK,SAAS,QAAQ,MAAM,SAAS,EAAG,OAAM,IAAI,MAAM,+DAA+D;AAC5H,SAAK,OAAO,MAAM,OAAO,EAAE,KAAK,EAAE;AAAA,EACpC;AAAA,EAL6B;AAAA;AAAA;AAAA;AAAA,EAhBpB;AAAA,EACQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmCjB,eAAkB,SAAiB,WAA4D;AAC7F,WAAO,QAAQ,OAAO,IAAI,MAAM,2GAA2G,CAAC;AAAA,EAC9I;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,qBAAwB,QAAgB,UAA+C,SAA2C;AAChI,QAAI,CAAC,SAAS,YAAY,QAAQ,aAAa,OAAO,QAAQ,aAAa,QAAQ,SAAS,KAAK,EAAG,QAAO,QAAQ,OAAO,IAAI,MAAM,qCAAqC,CAAC;AAC1K,UAAM,SAAS,KAAK,cAAc,QAAQ,QAAQ;AAClD,QAAI,OAAO,SAAS,eAAe,OAAO,aAAa,QAAQ,SAAU,QAAO,QAAQ,OAAO,IAAI,MAAM,oCAAoC,CAAC;AAC9I,UAAM,UAAU,KAAK,SAAS,IAAI,OAAO,UAAU;AACnD,QAAI,CAAC,QAAS,QAAO,QAAQ,OAAO,IAAI,MAAM,kEAAkE,CAAC;AACjH,WAAO,QAAQ,qBAAqB,QAAQ,UAAU,EAAE,GAAG,QAAQ,CAAC;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,OAAyB;AAAE,YAAQ,MAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,IAAI,KAAK,SAAS,OAAO,CAAC,CAAC,EAAE,IAAI,aAAW,QAAQ,KAAK,CAAC,CAAC,GAAG,MAAM,OAAO;AAAA,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYhJ,WAA4B;AAC1B,UAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,KAAK,SAAS,OAAO,CAAC,CAAC,EAAE,IAAI,aAAW,QAAQ,SAAS,CAAC;AACpF,WAAO,EAAE,QAAQ,KAAK,MAAM,mBAAmB,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,mBAAmB,CAAC,GAAG,UAAU,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,UAAU,CAAC,EAAE;AAAA,EACzJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,QAAuB;AAC3B,UAAM,UAAU,MAAM,QAAQ,WAAW,CAAC,GAAG,IAAI,IAAI,KAAK,SAAS,OAAO,CAAC,CAAC,EAAE,IAAI,aAAW,QAAQ,MAAM,CAAC,CAAC;AAC7G,UAAM,SAAS,QAAQ,OAAO,CAAC,WAA4C,OAAO,WAAW,UAAU,EAAE,IAAI,YAAU,OAAO,MAAM;AACpI,QAAI,OAAO,OAAQ,OAAM,IAAI,eAAe,QAAQ,oCAAoC;AAAA,EAC1F;AACF;","names":[]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAgentJob,
|
|
3
|
+
enqueueAgentJob
|
|
4
|
+
} from "./chunk-PBC3MZUL.js";
|
|
5
|
+
|
|
6
|
+
// src/agentic/ai-broker.ts
|
|
7
|
+
import {
|
|
8
|
+
createAIExecutionBroker
|
|
9
|
+
} from "@alneas/eaos-core";
|
|
10
|
+
var DurableAIExecutionBroker = class {
|
|
11
|
+
routesById;
|
|
12
|
+
selector;
|
|
13
|
+
now;
|
|
14
|
+
/**
|
|
15
|
+
* Validates and clones server routes and configures the core allocation selector.
|
|
16
|
+
* @param options - Nonempty routes list; optional policyVersion labels selector decisions and now supplies job timestamps, defaulting to fresh Date.
|
|
17
|
+
* @throws Invalid/missing routes, incompatible local dispatch bindings, core selector validation, or clone failures.
|
|
18
|
+
*/
|
|
19
|
+
constructor(options) {
|
|
20
|
+
if (!options || !Array.isArray(options.routes) || options.routes.length === 0) throw new Error("AI execution routes are required");
|
|
21
|
+
const routes = options.routes.map(validateServerRoute);
|
|
22
|
+
this.routesById = new Map(routes.map((route) => [route.routeId, structuredClone(route)]));
|
|
23
|
+
this.selector = createAIExecutionBroker({ routes, policyVersion: options.policyVersion });
|
|
24
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lists registered server route bindings without exposing mutable stored objects.
|
|
28
|
+
* @returns Cloned route records in registration map order.
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { DurableAIExecutionBroker } from "@alneas/eaos-server/agentic/ai-broker";
|
|
32
|
+
* export function routes(broker: DurableAIExecutionBroker) { return broker.listRoutes(); }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
listRoutes() {
|
|
36
|
+
return [...this.routesById.values()].map((route) => structuredClone(route));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Selects an eligible host-advertised runtime using the core routing policy.
|
|
40
|
+
* @param request - Verified AI execution requirements and context.
|
|
41
|
+
* @returns The core selector decision; no job is persisted or runtime started.
|
|
42
|
+
* @throws Propagates core routing validation/no-eligible-route errors.
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* import { DurableAIExecutionBroker } from "@alneas/eaos-server/agentic/ai-broker";
|
|
46
|
+
* import type { AIExecutionRequest } from "@alneas/eaos-core";
|
|
47
|
+
* export function decide(broker: DurableAIExecutionBroker, request: AIExecutionRequest) { return broker.decide(request); }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
decide(request) {
|
|
51
|
+
return this.selector.decide(request);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Selects a route and creates a queue record with host-controlled worker/dispatch/runner bindings.
|
|
55
|
+
* @param input - Routing and task must have matching tenantId/userId; queue metadata follows AIJobSubmission.
|
|
56
|
+
* @returns Decision and newly created job; caller-supplied broker route/dispatch/runner execution fields are stripped before selected bindings are written.
|
|
57
|
+
* @throws Principal mismatch, unavailable route, invalid core routing, clone/hash/clock, or job creation failures.
|
|
58
|
+
* @remarks No persistence or deduplication occurs until enqueue; repeated plan calls generate different job IDs.
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { DurableAIExecutionBroker } from "@alneas/eaos-server/agentic/ai-broker";
|
|
62
|
+
* import type { AIJobSubmission } from "@alneas/eaos-server/agentic/ai-broker";
|
|
63
|
+
* export function plan(broker: DurableAIExecutionBroker, input: AIJobSubmission) { return broker.plan(input); }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
plan(input) {
|
|
67
|
+
if (!input?.routing || !input.task || input.routing.context.tenantId !== input.task.tenantId || input.routing.context.userId !== input.task.userId) throw new Error("AI routing context does not match task context");
|
|
68
|
+
const decision = this.decide(input.routing);
|
|
69
|
+
const binding = this.routesById.get(decision.route.routeId);
|
|
70
|
+
if (!binding) throw new Error("Selected AI execution route is not registered");
|
|
71
|
+
const request = {
|
|
72
|
+
...structuredClone(input.task),
|
|
73
|
+
workerType: binding.workerType,
|
|
74
|
+
jobKind: input.kind,
|
|
75
|
+
executionContext: {
|
|
76
|
+
...withoutBrokerFields(input.task.executionContext),
|
|
77
|
+
routeId: binding.routeId,
|
|
78
|
+
dispatchMode: binding.dispatchMode,
|
|
79
|
+
...binding.runnerId === void 0 ? {} : { runnerId: binding.runnerId }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const job = createAgentJob({
|
|
83
|
+
request,
|
|
84
|
+
kind: input.kind,
|
|
85
|
+
workflowInstanceId: input.workflowInstanceId,
|
|
86
|
+
correlationId: input.correlationId ?? input.routing.context.operation,
|
|
87
|
+
idempotencyKey: input.idempotencyKey,
|
|
88
|
+
maxAttempts: input.maxAttempts,
|
|
89
|
+
now: this.now()
|
|
90
|
+
});
|
|
91
|
+
return { decision, job };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Plans routing and atomically inserts or deduplicates the queue job in a caller-owned session.
|
|
95
|
+
* @param tx - Authorized atomic transaction/session; caller owns commit/rollback.
|
|
96
|
+
* @param input - Verified routing/task and queue metadata.
|
|
97
|
+
* @returns Decision plus the newly saved or existing idempotent job.
|
|
98
|
+
* @throws Planning errors, conflicting deduplication input, corrupt index, or session failures.
|
|
99
|
+
* @remarks Selection still runs before deduplication; the returned existing job is authoritative for its persisted routing. No runtime execution occurs.
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* import { DurableAIExecutionBroker } from "@alneas/eaos-server/agentic/ai-broker";
|
|
103
|
+
* import type { AIJobSubmission } from "@alneas/eaos-server/agentic/ai-broker";
|
|
104
|
+
* import type { AtomicSession } from "@alneas/eaos-server/plugin-data/atomic";
|
|
105
|
+
* export async function enqueue(broker: DurableAIExecutionBroker, tx: AtomicSession, input: AIJobSubmission) { return broker.enqueue(tx, input); }
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
async enqueue(tx, input) {
|
|
109
|
+
const plan = this.plan(input);
|
|
110
|
+
const saved = await enqueueAgentJob(tx, plan.job);
|
|
111
|
+
return { ...plan, job: saved };
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function validateServerRoute(route) {
|
|
115
|
+
if (!route || typeof route.workerType !== "string" || !route.workerType.trim()) throw new Error("Invalid AI route worker type");
|
|
116
|
+
if (route.dispatchMode === "local-pull" && route.target !== "local-subscription-cli" && route.target !== "local-mlx") throw new Error("Only local routes may use local-pull dispatch");
|
|
117
|
+
if ((route.target === "local-subscription-cli" || route.target === "local-mlx") && route.dispatchMode !== "local-pull") throw new Error("Local CLI and MLX routes must use local-pull dispatch");
|
|
118
|
+
return structuredClone(route);
|
|
119
|
+
}
|
|
120
|
+
function withoutBrokerFields(context) {
|
|
121
|
+
if (!context) return {};
|
|
122
|
+
const { routeId: _routeId, dispatchMode: _dispatchMode, runnerId: _runnerId, ...callerFields } = context;
|
|
123
|
+
return callerFields;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export {
|
|
127
|
+
DurableAIExecutionBroker
|
|
128
|
+
};
|
|
129
|
+
//# sourceMappingURL=chunk-LYPZPE53.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agentic/ai-broker.ts"],"sourcesContent":["import {\n createAIExecutionBroker,\n type AIExecutionCandidate,\n type AIExecutionDecision,\n type AIExecutionRequest,\n type AgentJobKind,\n type AgentJobRecord,\n} from '@alneas/eaos-core'\nimport type { AtomicSession } from '../plugin-data/atomic'\nimport { createAgentJob, enqueueAgentJob } from './agent-jobs'\nimport type {\n ServerAgentDispatchMode,\n ServerAgentExecutionContext,\n ServerAgentTaskRequest,\n ServerAgentWorkerType,\n} from './types'\n\n/**\n * Host-advertised AI candidate with server worker and queue/local-runner delivery binding.\n * @see DurableAIExecutionBroker.\n */\nexport interface ServerAIExecutionRoute extends AIExecutionCandidate {\n /** Executor key bound to the selected host route. */\n workerType: ServerAgentWorkerType\n /** Local CLI/MLX candidates must use local-pull; nonlocal candidates cannot use local-pull. */\n dispatchMode: ServerAgentDispatchMode\n /** Pin a route to one registered local runner when the host requires it. */\n runnerId?: string\n}\n\n/**\n * Verified task input before the broker chooses immutable runtime routing fields.\n * @see DurableAIExecutionBroker.plan.\n */\nexport interface AIJobTaskInput {\n /** Stable task identifier. */\n id: string\n /** Verified tenant, required to match routing context. */\n tenantId: string\n /** Verified user, required to match routing context. */\n userId: string\n /** Human task label forwarded to runtime. */\n title: string\n /** Runtime instructions forwarded without domain validation here. */\n instructions: string\n /** Optional domain execution context. */\n context?: Record<string, unknown>\n /** Optional workflow/tool context; broker-owned routing fields are replaced. */\n executionContext?: ServerAgentExecutionContext\n /** Optional runtime timeout in milliseconds, forwarded for executor validation. */\n timeoutMs?: number\n}\n\n/**\n * Verified routing request, task, and queue deduplication/retry metadata.\n * @see DurableAIExecutionBroker.plan.\n */\nexport interface AIJobSubmission {\n /** Core execution requirements and verified context used for route allocation. */\n routing: AIExecutionRequest\n /** Verified task before worker/routing binding. */\n task: AIJobTaskInput\n /** Queue job kind and idempotency partition. */\n kind: AgentJobKind\n /** Optional associated workflow instance. */\n workflowInstanceId?: string\n /** Optional queue correlation ID; defaults to routing.context.operation. */\n correlationId?: string\n /** Optional durable deduplication key; absent uses task execution key then task.id. */\n idempotencyKey?: string\n /** Optional job attempt ceiling; defaults to 5 through createAgentJob. */\n maxAttempts?: number\n}\n\n/**\n * Selected runtime decision and unpersisted or saved queue job.\n * @see DurableAIExecutionBroker.enqueue.\n */\nexport interface AIJobPlan {\n /** Current selector decision, even when enqueue returns an existing deduplicated job. */\n decision: AIExecutionDecision\n /** Created or deduplicated queue record with persisted runtime routing. */\n job: AgentJobRecord<ServerAgentTaskRequest>\n}\n\n/**\n * Micro2 controller for dynamic model/runtime allocation.\n *\n * It selects only host-advertised routes and persists the selected route in\n * the durable job. It never reads a subscription credential or starts a local\n * process. A local-pull route is later claimed by LocalRunnerPullCoordinator.\n * @remarks Route selection and planning are process-local. The host verifies identity and supplies current candidate availability/policy; authorization, runtime execution, and safe retries remain separate responsibilities.\n * @example\n * ```typescript\n * import { DurableAIExecutionBroker } from \"@alneas/eaos-server/agentic/ai-broker\";\n * export function broker(options: ConstructorParameters<typeof DurableAIExecutionBroker>[0]) {\n * return new DurableAIExecutionBroker(options);\n * }\n * ```\n */\nexport class DurableAIExecutionBroker {\n private readonly routesById: ReadonlyMap<string, ServerAIExecutionRoute>\n private readonly selector: ReturnType<typeof createAIExecutionBroker>\n private readonly now: () => Date\n\n /**\n * Validates and clones server routes and configures the core allocation selector.\n * @param options - Nonempty routes list; optional policyVersion labels selector decisions and now supplies job timestamps, defaulting to fresh Date.\n * @throws Invalid/missing routes, incompatible local dispatch bindings, core selector validation, or clone failures.\n */\n constructor(options: { \n /**\n * Nonempty host-approved execution routes, validated/cloned before configuring the selector; route IDs and runtime dispatch bindings must be compatible.\n */\n routes: readonly ServerAIExecutionRoute[]; \n /**\n * Optional decision policy label forwarded to the Core selector; omission selects ai-execution-policy-v1.\n */\n policyVersion?: string; \n /**\n * Optional clock used when constructing agent job records; omission creates a new current Date per call.\n * @returns A valid Date used for planned job timestamps; clock failures propagate from planning.\n * @see {@link DurableAIExecutionBroker.plan}\n */\n now?: () => Date }) {\n if (!options || !Array.isArray(options.routes) || options.routes.length === 0) throw new Error('AI execution routes are required')\n const routes = options.routes.map(validateServerRoute)\n this.routesById = new Map(routes.map(route => [route.routeId, structuredClone(route)]))\n this.selector = createAIExecutionBroker({ routes, policyVersion: options.policyVersion })\n this.now = options.now ?? (() => new Date())\n }\n\n /**\n * Lists registered server route bindings without exposing mutable stored objects.\n * @returns Cloned route records in registration map order.\n * @example\n * ```typescript\n * import { DurableAIExecutionBroker } from \"@alneas/eaos-server/agentic/ai-broker\";\n * export function routes(broker: DurableAIExecutionBroker) { return broker.listRoutes(); }\n * ```\n */\n listRoutes(): readonly ServerAIExecutionRoute[] {\n return [...this.routesById.values()].map(route => structuredClone(route))\n }\n\n /**\n * Selects an eligible host-advertised runtime using the core routing policy.\n * @param request - Verified AI execution requirements and context.\n * @returns The core selector decision; no job is persisted or runtime started.\n * @throws Propagates core routing validation/no-eligible-route errors.\n * @example\n * ```typescript\n * import { DurableAIExecutionBroker } from \"@alneas/eaos-server/agentic/ai-broker\";\n * import type { AIExecutionRequest } from \"@alneas/eaos-core\";\n * export function decide(broker: DurableAIExecutionBroker, request: AIExecutionRequest) { return broker.decide(request); }\n * ```\n */\n decide(request: AIExecutionRequest): AIExecutionDecision {\n return this.selector.decide(request)\n }\n\n /**\n * Selects a route and creates a queue record with host-controlled worker/dispatch/runner bindings.\n * @param input - Routing and task must have matching tenantId/userId; queue metadata follows AIJobSubmission.\n * @returns Decision and newly created job; caller-supplied broker route/dispatch/runner execution fields are stripped before selected bindings are written.\n * @throws Principal mismatch, unavailable route, invalid core routing, clone/hash/clock, or job creation failures.\n * @remarks No persistence or deduplication occurs until enqueue; repeated plan calls generate different job IDs.\n * @example\n * ```typescript\n * import { DurableAIExecutionBroker } from \"@alneas/eaos-server/agentic/ai-broker\";\n * import type { AIJobSubmission } from \"@alneas/eaos-server/agentic/ai-broker\";\n * export function plan(broker: DurableAIExecutionBroker, input: AIJobSubmission) { return broker.plan(input); }\n * ```\n */\n plan(input: AIJobSubmission): AIJobPlan {\n if (!input?.routing || !input.task || input.routing.context.tenantId !== input.task.tenantId || input.routing.context.userId !== input.task.userId) throw new Error('AI routing context does not match task context')\n const decision = this.decide(input.routing)\n const binding = this.routesById.get(decision.route.routeId)\n if (!binding) throw new Error('Selected AI execution route is not registered')\n const request: ServerAgentTaskRequest = {\n ...structuredClone(input.task),\n workerType: binding.workerType,\n jobKind: input.kind,\n executionContext: {\n ...withoutBrokerFields(input.task.executionContext),\n routeId: binding.routeId,\n dispatchMode: binding.dispatchMode,\n ...(binding.runnerId === undefined ? {} : { runnerId: binding.runnerId }),\n },\n }\n const job = createAgentJob({\n request,\n kind: input.kind,\n workflowInstanceId: input.workflowInstanceId,\n correlationId: input.correlationId ?? input.routing.context.operation,\n idempotencyKey: input.idempotencyKey,\n maxAttempts: input.maxAttempts,\n now: this.now(),\n })\n return { decision, job }\n }\n\n /**\n * Plans routing and atomically inserts or deduplicates the queue job in a caller-owned session.\n * @param tx - Authorized atomic transaction/session; caller owns commit/rollback.\n * @param input - Verified routing/task and queue metadata.\n * @returns Decision plus the newly saved or existing idempotent job.\n * @throws Planning errors, conflicting deduplication input, corrupt index, or session failures.\n * @remarks Selection still runs before deduplication; the returned existing job is authoritative for its persisted routing. No runtime execution occurs.\n * @example\n * ```typescript\n * import { DurableAIExecutionBroker } from \"@alneas/eaos-server/agentic/ai-broker\";\n * import type { AIJobSubmission } from \"@alneas/eaos-server/agentic/ai-broker\";\n * import type { AtomicSession } from \"@alneas/eaos-server/plugin-data/atomic\";\n * export async function enqueue(broker: DurableAIExecutionBroker, tx: AtomicSession, input: AIJobSubmission) { return broker.enqueue(tx, input); }\n * ```\n */\n async enqueue(tx: AtomicSession, input: AIJobSubmission): Promise<AIJobPlan> {\n const plan = this.plan(input)\n const saved = await enqueueAgentJob(tx, plan.job)\n return { ...plan, job: saved }\n }\n}\n\nfunction validateServerRoute(route: ServerAIExecutionRoute): ServerAIExecutionRoute {\n if (!route || typeof route.workerType !== 'string' || !route.workerType.trim()) throw new Error('Invalid AI route worker type')\n if (route.dispatchMode === 'local-pull' && route.target !== 'local-subscription-cli' && route.target !== 'local-mlx') throw new Error('Only local routes may use local-pull dispatch')\n if ((route.target === 'local-subscription-cli' || route.target === 'local-mlx') && route.dispatchMode !== 'local-pull') throw new Error('Local CLI and MLX routes must use local-pull dispatch')\n return structuredClone(route)\n}\n\nfunction withoutBrokerFields(context: ServerAgentExecutionContext | undefined): ServerAgentExecutionContext {\n if (!context) return {}\n const { routeId: _routeId, dispatchMode: _dispatchMode, runnerId: _runnerId, ...callerFields } = context\n return callerFields\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// src/storage/index.ts
|
|
2
|
+
function segment(value) {
|
|
3
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value)) throw new Error("Invalid storage scope ID");
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
function path(value, prefix = false) {
|
|
7
|
+
if (typeof value !== "string" || !prefix && !value || /[\\%\x00-\x1f\x7f?#]/.test(value) || value.startsWith("/")) throw new Error("Invalid storage key");
|
|
8
|
+
const parts = value.split("/");
|
|
9
|
+
if (prefix && parts.at(-1) === "") parts.pop();
|
|
10
|
+
if (parts.some((part) => !part || part === "." || part === "..")) throw new Error("Invalid storage key");
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function createTenantStorage(adapter, options) {
|
|
14
|
+
const tenantId = segment(options.tenantId), pluginId = segment(options.pluginId);
|
|
15
|
+
const scope = `tenants/${tenantId}/plugins/${pluginId}/`;
|
|
16
|
+
const authorize = options.authorize;
|
|
17
|
+
const maxAge = options.maxUrlLifetimeSeconds ?? 300;
|
|
18
|
+
if (!Number.isInteger(maxAge) || maxAge < 1) throw new Error("Invalid URL lifetime");
|
|
19
|
+
async function check(operation, keys) {
|
|
20
|
+
if (await authorize({ tenantId, pluginId, operation, keys: Object.freeze([...keys]) }) !== true) throw new Error("Storage access denied");
|
|
21
|
+
}
|
|
22
|
+
function relative(key, prefix = false) {
|
|
23
|
+
if (!key.startsWith(scope)) throw new Error("Storage returned an object outside the authorized scope");
|
|
24
|
+
return path(key.slice(scope.length), prefix);
|
|
25
|
+
}
|
|
26
|
+
function object(value) {
|
|
27
|
+
return { ...value, key: relative(value.key) };
|
|
28
|
+
}
|
|
29
|
+
function exact(value, key) {
|
|
30
|
+
const result = object(value);
|
|
31
|
+
if (result.key !== key) throw new Error("Storage returned an unexpected object");
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
async upload(key, data, settings) {
|
|
36
|
+
key = path(key);
|
|
37
|
+
if (settings?.public) throw new Error("Public tenant objects are disabled");
|
|
38
|
+
const safe = { ...settings, public: false };
|
|
39
|
+
await check("upload", [key]);
|
|
40
|
+
return exact(await adapter.upload(scope + key, data, safe), key);
|
|
41
|
+
},
|
|
42
|
+
async download(key) {
|
|
43
|
+
key = path(key);
|
|
44
|
+
await check("download", [key]);
|
|
45
|
+
return adapter.download(scope + key);
|
|
46
|
+
},
|
|
47
|
+
async delete(key) {
|
|
48
|
+
key = path(key);
|
|
49
|
+
await check("delete", [key]);
|
|
50
|
+
return adapter.delete(scope + key);
|
|
51
|
+
},
|
|
52
|
+
async exists(key) {
|
|
53
|
+
key = path(key);
|
|
54
|
+
await check("exists", [key]);
|
|
55
|
+
return adapter.exists(scope + key);
|
|
56
|
+
},
|
|
57
|
+
async head(key) {
|
|
58
|
+
key = path(key);
|
|
59
|
+
await check("head", [key]);
|
|
60
|
+
const result = await adapter.head(scope + key);
|
|
61
|
+
return result ? exact(result, key) : null;
|
|
62
|
+
},
|
|
63
|
+
async getUrl(key, settings) {
|
|
64
|
+
key = path(key);
|
|
65
|
+
const expiresIn = settings?.expiresIn ?? maxAge;
|
|
66
|
+
if (settings?.signed === false || !Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > maxAge) throw new Error("Signed URL policy violation");
|
|
67
|
+
const safe = { ...settings, signed: true, expiresIn };
|
|
68
|
+
await check("getUrl", [key]);
|
|
69
|
+
return adapter.getUrl(scope + key, safe);
|
|
70
|
+
},
|
|
71
|
+
async list(settings = {}) {
|
|
72
|
+
const prefix = path(settings.prefix ?? "", true);
|
|
73
|
+
if (settings.delimiter !== void 0 && settings.delimiter !== "/") throw new Error("Invalid delimiter");
|
|
74
|
+
let cursor;
|
|
75
|
+
if (settings.cursor) {
|
|
76
|
+
try {
|
|
77
|
+
const data = JSON.parse(Buffer.from(settings.cursor, "base64url").toString());
|
|
78
|
+
if (data.scope !== scope || data.prefix !== prefix || typeof data.token !== "string") throw new Error();
|
|
79
|
+
cursor = data.token;
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error("Invalid scoped storage cursor");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const safe = { ...settings, prefix: scope + prefix, cursor };
|
|
85
|
+
await check("list", [prefix]);
|
|
86
|
+
const result = await adapter.list(safe);
|
|
87
|
+
const objects = result.objects.map(object), prefixes = result.prefixes?.map((key) => relative(key, true));
|
|
88
|
+
if (objects.some((o) => !o.key.startsWith(prefix)) || prefixes?.some((p) => !p.startsWith(prefix))) throw new Error("Storage returned objects outside the requested prefix");
|
|
89
|
+
return { objects, prefixes, hasMore: result.hasMore, nextCursor: result.nextCursor ? Buffer.from(JSON.stringify({ scope, prefix, token: result.nextCursor })).toString("base64url") : void 0 };
|
|
90
|
+
},
|
|
91
|
+
...adapter.copy ? { async copy(source, destination) {
|
|
92
|
+
source = path(source);
|
|
93
|
+
destination = path(destination);
|
|
94
|
+
await check("copy", [source, destination]);
|
|
95
|
+
return exact(await adapter.copy(scope + source, scope + destination), destination);
|
|
96
|
+
} } : {},
|
|
97
|
+
...adapter.move ? { async move(source, destination) {
|
|
98
|
+
source = path(source);
|
|
99
|
+
destination = path(destination);
|
|
100
|
+
await check("move", [source, destination]);
|
|
101
|
+
return exact(await adapter.move(scope + source, scope + destination), destination);
|
|
102
|
+
} } : {}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/storage/s3-compatible.ts
|
|
107
|
+
async function createS3CompatibleStorage(config) {
|
|
108
|
+
if (!["s3", "r2"].includes(config.provider)) throw new Error("Unsupported storage provider");
|
|
109
|
+
const maxAge = config.maxUrlLifetimeSeconds ?? 300;
|
|
110
|
+
if (!Number.isInteger(maxAge) || maxAge < 1 || maxAge > 604800) throw new Error("Invalid URL lifetime");
|
|
111
|
+
if (!config.bucket || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(config.bucket)) throw new Error("Invalid storage bucket");
|
|
112
|
+
if (config.provider === "r2" && !/^[a-f0-9]{32}$/i.test(config.accountId)) throw new Error("Invalid R2 account ID");
|
|
113
|
+
if (config.provider === "s3" && !config.region) throw new Error("Storage region required");
|
|
114
|
+
if (config.endpoint) {
|
|
115
|
+
const endpoint = new URL(config.endpoint);
|
|
116
|
+
if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) throw new Error("Invalid storage endpoint");
|
|
117
|
+
}
|
|
118
|
+
const credentials = typeof config.credentials === "function" ? await config.credentials() : config.credentials;
|
|
119
|
+
if (!credentials.accessKeyId || !credentials.secretAccessKey) throw new Error("Storage credentials required");
|
|
120
|
+
const { provider, credentials: _credentials, maxUrlLifetimeSeconds: _maxAge, ...settings } = config;
|
|
121
|
+
const { R2StorageAdapter, S3StorageAdapter } = await import("@alneas/appkit-core/storage");
|
|
122
|
+
const adapter = provider === "r2" ? new R2StorageAdapter({ ...settings, ...credentials, defaultExpiresIn: maxAge }) : new S3StorageAdapter({ ...settings, ...credentials, defaultExpiresIn: maxAge });
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
async upload(key, data, options) {
|
|
125
|
+
if (options?.public) throw new Error("Public storage uploads are disabled");
|
|
126
|
+
return adapter.upload(key, data, { ...options, public: false });
|
|
127
|
+
},
|
|
128
|
+
download: adapter.download.bind(adapter),
|
|
129
|
+
delete: adapter.delete.bind(adapter),
|
|
130
|
+
exists: adapter.exists.bind(adapter),
|
|
131
|
+
head: adapter.head.bind(adapter),
|
|
132
|
+
list: adapter.list.bind(adapter),
|
|
133
|
+
async getUrl(key, options) {
|
|
134
|
+
const expiresIn = options?.expiresIn ?? maxAge;
|
|
135
|
+
if (options?.signed === false || !Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > maxAge) throw new Error("Signed URL policy violation");
|
|
136
|
+
return adapter.getUrl(key, { ...options, signed: true, expiresIn });
|
|
137
|
+
},
|
|
138
|
+
...adapter.copy ? { copy: adapter.copy.bind(adapter) } : {},
|
|
139
|
+
...adapter.move ? { move: adapter.move.bind(adapter) } : {}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function createTenantS3CompatibleStorage(config, options) {
|
|
143
|
+
const adapter = await createS3CompatibleStorage(config);
|
|
144
|
+
return createTenantStorage(adapter, { ...options, maxUrlLifetimeSeconds: options.maxUrlLifetimeSeconds ?? config.maxUrlLifetimeSeconds });
|
|
145
|
+
}
|
|
146
|
+
function readS3CompatibleStorageConfig(env = process.env) {
|
|
147
|
+
const provider = env.EAOS_STORAGE_PROVIDER?.trim().toLowerCase();
|
|
148
|
+
if (!provider) return null;
|
|
149
|
+
const maxAge = env.EAOS_STORAGE_MAX_URL_SECONDS ? Number(env.EAOS_STORAGE_MAX_URL_SECONDS) : void 0;
|
|
150
|
+
if (provider === "r2") {
|
|
151
|
+
const accountId = env.R2_ACCOUNT_ID?.trim() ?? "", bucket = env.R2_BUCKET?.trim() ?? "";
|
|
152
|
+
const accessKeyId = env.R2_ACCESS_KEY_ID?.trim() ?? "", secretAccessKey = env.R2_SECRET_ACCESS_KEY?.trim() ?? "";
|
|
153
|
+
if (!accountId || !bucket || !accessKeyId || !secretAccessKey) throw new Error("Incomplete R2 storage environment");
|
|
154
|
+
return {
|
|
155
|
+
provider: "r2",
|
|
156
|
+
accountId,
|
|
157
|
+
bucket,
|
|
158
|
+
...env.R2_ENDPOINT ? { endpoint: env.R2_ENDPOINT.trim() } : {},
|
|
159
|
+
...maxAge !== void 0 ? { maxUrlLifetimeSeconds: maxAge } : {},
|
|
160
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (provider === "s3") {
|
|
164
|
+
const bucket = env.S3_BUCKET?.trim() ?? "", region = env.S3_REGION?.trim() ?? "";
|
|
165
|
+
const accessKeyId = env.S3_ACCESS_KEY_ID?.trim() ?? "", secretAccessKey = env.S3_SECRET_ACCESS_KEY?.trim() ?? "";
|
|
166
|
+
if (!bucket || !region || !accessKeyId || !secretAccessKey) throw new Error("Incomplete S3 storage environment");
|
|
167
|
+
return {
|
|
168
|
+
provider: "s3",
|
|
169
|
+
bucket,
|
|
170
|
+
region,
|
|
171
|
+
...env.S3_ENDPOINT ? { endpoint: env.S3_ENDPOINT.trim() } : {},
|
|
172
|
+
...env.S3_FORCE_PATH_STYLE === "true" || env.S3_FORCE_PATH_STYLE === "1" ? { forcePathStyle: true } : {},
|
|
173
|
+
...maxAge !== void 0 ? { maxUrlLifetimeSeconds: maxAge } : {},
|
|
174
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
throw new Error("Unsupported storage provider");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
createS3CompatibleStorage,
|
|
182
|
+
createTenantS3CompatibleStorage,
|
|
183
|
+
readS3CompatibleStorageConfig,
|
|
184
|
+
createTenantStorage
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=chunk-NNCCN7JP.js.map
|