@codemation/host 0.6.0 → 0.7.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 +431 -0
- package/LICENSE +1 -37
- package/dist/{ApiPaths-CLTHphYZ.js → ApiPaths-Dv1dcHu_.js} +4 -4
- package/dist/ApiPaths-Dv1dcHu_.js.map +1 -0
- package/dist/{AppConfigFactory-C6q-CSKb.js → AppConfigFactory-Cx4qQvRk.js} +112 -52
- package/dist/AppConfigFactory-Cx4qQvRk.js.map +1 -0
- package/dist/{AppConfigFactory-YnveXE9k.d.ts → AppConfigFactory-DnLoQ9Li.d.ts} +8490 -5548
- package/dist/{AppContainerFactory-qaqc-R1D.js → AppContainerFactory-DqKYCRNP.js} +7641 -2083
- package/dist/AppContainerFactory-DqKYCRNP.js.map +1 -0
- package/dist/{CodemationAppContext-DRu1Dpri.d.ts → CodemationAppContext-CKVv9W9q.d.ts} +8 -4
- package/dist/{CodemationAuthoring.types-fBRppnmi.d.ts → CodemationAuthoring.types-DA3G3s6d.d.ts} +25 -5
- package/dist/{CodemationAuthoring.types-DZl-sJaM.js → CodemationAuthoring.types-NGkBcmmT.js} +18 -6
- package/dist/CodemationAuthoring.types-NGkBcmmT.js.map +1 -0
- package/dist/{CodemationConfigNormalizer-DVko3cVN.d.ts → CodemationConfigNormalizer-BAKjetJ6.d.ts} +3 -3
- package/dist/{CodemationConsumerConfigLoader-BeAUS144.js → CodemationConsumerConfigLoader-GYpBBvqE.js} +79 -10
- package/dist/CodemationConsumerConfigLoader-GYpBBvqE.js.map +1 -0
- package/dist/{CodemationConsumerConfigLoader-DJWr86f-.d.ts → CodemationConsumerConfigLoader-nxOqvv46.d.ts} +17 -2
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js → CodemationPluginListMerger-D1B1IEbt.js} +1 -1
- package/dist/{CodemationPluginListMerger-B-W5Fa_X.js.map → CodemationPluginListMerger-D1B1IEbt.js.map} +1 -1
- package/dist/{CodemationPluginListMerger-DGc-jfa2.d.ts → CodemationPluginListMerger-DKLAHT2b.d.ts} +123 -16
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js +97 -0
- package/dist/CodemationTsyringeTypeInfoRegistrar-Bj6FJYFz.js.map +1 -0
- package/dist/{CodemationWhitelabelConfig-CWbcyQqn.d.ts → CodemationWhitelabelConfig-Ca2mCUeC.d.ts} +2 -2
- package/dist/{CollectionContracts.types-DdpHft0i.d.ts → CollectionContracts.types-DDyFYT_D.d.ts} +1 -1
- package/dist/{CredentialContractsRegistry-DrMIDSw8.d.ts → CredentialContractsRegistry-Bq2bq28t.d.ts} +2 -2
- package/dist/{CredentialServices-UfvHB-rN.d.ts → CredentialServices-Be2I60Th.d.ts} +65 -20
- package/dist/{CredentialServices-CgxwguAv.js → CredentialServices-Dk8yypeL.js} +310 -51
- package/dist/CredentialServices-Dk8yypeL.js.map +1 -0
- package/dist/InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts +17 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js +221 -0
- package/dist/InternalPingRegistrar-DY3kSfxP.js.map +1 -0
- package/dist/{ItemsInputNormalizer-C-KHg9Mo.d.ts → ItemsInputNormalizer-_RwIfRIQ.d.ts} +89 -25
- package/dist/{LogLevelPolicyFactory-CampWO0l.d.ts → LogLevelPolicyFactory-ewCHLDLn.d.ts} +2 -2
- package/dist/{PublicFrontendBootstrap-DzBgwOnG.d.ts → PublicFrontendBootstrap-Cev3qK46.d.ts} +9 -2
- package/dist/PublicFrontendBootstrapFactory-CY2FS-5g.d.ts +82 -0
- package/dist/{PublicFrontendBootstrapJsonCodec-Cl_DLRh0.d.ts → PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts} +3 -3
- package/dist/{PublicFrontendBootstrapJsonCodec-DzqvA0uo.js → PublicFrontendBootstrapJsonCodec-CegIF_ne.js} +7 -2
- package/dist/PublicFrontendBootstrapJsonCodec-CegIF_ne.js.map +1 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js +223 -0
- package/dist/ServerLoggerFactory-Ckk52S3w.js.map +1 -0
- package/dist/{TelemetryContracts-DbaNomrH.d.ts → TelemetryContracts-BtDx84Cp.d.ts} +13 -4
- package/dist/{WorkflowPolicyUiPresentationFactory-DQEY-h_S.d.ts → WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts} +2 -2
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js} +1 -1
- package/dist/{WorkflowPolicyUiPresentationFactory-DhPqQ9aB.js.map → WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map} +1 -1
- package/dist/{WorkflowViewContracts-CzK2KFuz.d.ts → WorkflowViewContracts-B7aFQcIw.d.ts} +10 -1
- package/dist/authoring.d.ts +5 -5
- package/dist/authoring.js +1 -1
- package/dist/client.d.ts +4 -4
- package/dist/client.js +2 -2
- package/dist/consumer.d.ts +6 -6
- package/dist/consumer.js +2 -2
- package/dist/credentials.d.ts +6 -6
- package/dist/credentials.js +1 -1
- package/dist/devServerSidecar.d.ts +2 -2
- package/dist/devServerSidecar.js +1 -94
- package/dist/devServerSidecar.js.map +1 -1
- package/dist/dto.d.ts +6 -6
- package/dist/{index-BbBk26m0.d.ts → index-DilAYwnH.d.ts} +49 -3
- package/dist/index.d.ts +110 -21
- package/dist/index.js +15 -13
- package/dist/mapping.d.ts +2 -2
- package/dist/mapping.js +1 -1
- package/dist/nextServer.d.ts +43 -88
- package/dist/nextServer.js +9 -7
- package/dist/pairing.d.ts +93 -0
- package/dist/pairing.js +5 -0
- package/dist/pairing.types-snfZ_OzB.d.ts +19 -0
- package/dist/{persistenceServer-CmsIKnO9.js → persistenceServer-C-hH4z6l.js} +2 -2
- package/dist/{persistenceServer-CmsIKnO9.js.map → persistenceServer-C-hH4z6l.js.map} +1 -1
- package/dist/persistenceServer-CeTHtC6E.d.ts +30 -0
- package/dist/persistenceServer.d.ts +8 -8
- package/dist/persistenceServer.js +3 -3
- package/dist/{server-MUNGsBYK.d.ts → server-C4bS62rg.d.ts} +21 -6
- package/dist/{server-CJFfY67o.js → server-Y7kxwtCK.js} +7 -6
- package/dist/{server-CJFfY67o.js.map → server-Y7kxwtCK.js.map} +1 -1
- package/dist/server.d.ts +14 -14
- package/dist/server.js +13 -11
- package/package.json +29 -42
- package/prisma/migrations/20260519000000_workflow_audit_log/migration.sql +23 -0
- package/prisma/migrations/20260519100000_storage_growth_fixes/migration.sql +61 -0
- package/prisma/migrations.sqlite/20260519000000_workflow_audit_log/migration.sql +21 -0
- package/prisma/migrations.sqlite/20260519100000_storage_growth_fixes/migration.sql +29 -0
- package/prisma/schema.postgresql.prisma +55 -17
- package/prisma/schema.sqlite.prisma +55 -17
- package/prisma-generated/prisma-postgresql-client/edge.js +33 -5
- package/prisma-generated/prisma-postgresql-client/index-browser.js +29 -1
- package/prisma-generated/prisma-postgresql-client/index.d.ts +8933 -5716
- package/prisma-generated/prisma-postgresql-client/index.js +33 -5
- package/prisma-generated/prisma-postgresql-client/package.json +1 -1
- package/prisma-generated/prisma-postgresql-client/schema.prisma +38 -0
- package/prisma-generated/prisma-sqlite-client/edge.js +33 -5
- package/prisma-generated/prisma-sqlite-client/index-browser.js +29 -1
- package/prisma-generated/prisma-sqlite-client/index.d.ts +8925 -5713
- package/prisma-generated/prisma-sqlite-client/index.js +33 -5
- package/prisma-generated/prisma-sqlite-client/package.json +1 -1
- package/prisma-generated/prisma-sqlite-client/schema.prisma +38 -0
- package/scripts/check-collections.mjs +18 -0
- package/scripts/generate-prisma-clients.mjs +20 -11
- package/src/application/WorkflowAuditLogPruneScheduler.ts +96 -0
- package/src/application/auth/AuthenticatedPrincipal.ts +4 -0
- package/src/application/commands/StartWorkflowRunCommandHandler.ts +4 -0
- package/src/application/contracts/WorkflowViewContracts.ts +6 -0
- package/src/application/contracts/WorkflowWebsocketMessage.ts +3 -1
- package/src/application/mapping/WorkflowDefinitionMapper.ts +40 -1
- package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +7 -1
- package/src/application/telemetry/OtelExecutionTelemetry.types.ts +5 -0
- package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +4 -0
- package/src/application/telemetry/StoredTelemetrySpanScope.ts +6 -2
- package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +27 -17
- package/src/application/telemetry/TelemetrySpanPublisher.ts +11 -0
- package/src/application/websocket/TelemetrySpanWebsocketRelay.ts +31 -0
- package/src/applicationTokens.ts +20 -1
- package/src/audit/IAuditEmitter.ts +32 -0
- package/src/audit/PrismaWorkflowAuditLogRepository.ts +34 -0
- package/src/audit/WorkflowAuditLogWriter.ts +125 -0
- package/src/auth/managed/ManagedAuthConfig.ts +29 -0
- package/src/auth/managed/ManagedAuthMiddleware.ts +52 -0
- package/src/auth/managed/ManagedCorsMiddleware.ts +43 -0
- package/src/auth/managed/ManagedModeBootGuard.ts +27 -0
- package/src/auth/managed/index.ts +5 -0
- package/src/bootstrap/AppContainerFactory.ts +277 -29
- package/src/bootstrap/AppContainerLifecycle.ts +31 -0
- package/src/bootstrap/perf/BootTimer.ts +168 -0
- package/src/bootstrap/runtime/AppConfigFactory.ts +21 -65
- package/src/bootstrap/runtime/FrontendRuntime.ts +4 -1
- package/src/bootstrap/runtime/WorkerRuntime.ts +2 -1
- package/src/credentials/BrokerClient.ts +49 -0
- package/src/credentials/BrokerRefreshError.ts +12 -0
- package/src/credentials/BrokerRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/ControlPlaneCatalogFetcher.ts +261 -0
- package/src/credentials/CredentialOAuth2MaterialReader.ts +136 -0
- package/src/credentials/InternalCredentialsListRegistrar.ts +48 -0
- package/src/credentials/InternalCredentialsPushRegistrar.ts +125 -0
- package/src/credentials/LocalOAuthFlowExecutor.ts +316 -0
- package/src/credentials/ManagedOAuthFlowExecutor.ts +94 -0
- package/src/credentials/ManagedOAuthRefreshInvalidGrantError.ts +13 -0
- package/src/credentials/catalogTypes.ts +4 -0
- package/src/credentials/refresh/CredentialDisconnectedError.ts +11 -0
- package/src/domain/credentials/CredentialBindingService.ts +54 -2
- package/src/domain/credentials/CredentialKeyRotatedError.ts +22 -0
- package/src/domain/credentials/CredentialSecretCipher.ts +68 -6
- package/src/domain/credentials/CredentialTypeRegistryImpl.ts +117 -10
- package/src/domain/credentials/OAuth2RedirectUriResolver.ts +79 -0
- package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +14 -5
- package/src/domain/telemetry/TelemetryContracts.ts +7 -1
- package/src/domain/workflows/WorkflowActivationPreflight.ts +24 -1
- package/src/domain/workflows/WorkflowActivationPreflightRules.ts +40 -1
- package/src/index.ts +6 -0
- package/src/infrastructure/binary/LocalFilesystemBinaryStorageRegistry.ts +29 -1
- package/src/infrastructure/binary/S3BinaryStorage.ts +169 -0
- package/src/infrastructure/binary/S3BinaryStorageConfig.ts +17 -0
- package/src/infrastructure/config/CodemationPluginRegistrar.ts +3 -1
- package/src/infrastructure/persistence/CodemationDatabaseUrlParser.ts +41 -0
- package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +8 -3
- package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +21 -13
- package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +43 -8
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +26 -3
- package/src/infrastructure/persistence/PrismaWorkflowSnapshotRepository.ts +48 -0
- package/src/mcp/AgentMcpIntegrationImpl.ts +344 -0
- package/src/mcp/McpClientFactory.ts +29 -0
- package/src/mcp/McpConnectionPool.ts +184 -0
- package/src/mcp/McpConnectionPool.types.ts +12 -0
- package/src/mcp/McpServerCatalog.ts +104 -0
- package/src/mcp/index.ts +5 -0
- package/src/pairing/HmacRequestSigner.ts +32 -0
- package/src/pairing/IncomingHmacVerifier.ts +82 -0
- package/src/pairing/InternalHmacAuthMiddleware.ts +33 -0
- package/src/pairing/InternalPingRegistrar.ts +25 -0
- package/src/pairing/PairedFetch.ts +33 -0
- package/src/pairing/PairingConfigFactory.ts +35 -0
- package/src/pairing/PairingConfigToken.ts +6 -0
- package/src/pairing/index.ts +14 -0
- package/src/pairing/pairing.types.ts +18 -0
- package/src/pairing.ts +17 -0
- package/src/persistenceServer.ts +1 -0
- package/src/presentation/config/AppConfig.ts +7 -1
- package/src/presentation/config/CodemationAuthConfig.ts +1 -1
- package/src/presentation/config/CodemationAuthoring.types.ts +54 -5
- package/src/presentation/config/CodemationConfig.ts +3 -0
- package/src/presentation/config/CodemationConfigNormalizer.ts +39 -1
- package/src/presentation/config/CodemationPlugin.ts +2 -1
- package/src/presentation/frontend/CodemationFrontendAuthSnapshot.ts +5 -0
- package/src/presentation/frontend/CodemationFrontendAuthSnapshotFactory.ts +7 -1
- package/src/presentation/frontend/PublicFrontendBootstrap.ts +2 -0
- package/src/presentation/frontend/PublicFrontendBootstrapFactory.ts +5 -1
- package/src/presentation/frontend/PublicFrontendBootstrapJsonCodec.ts +4 -1
- package/src/presentation/http/ApiPaths.ts +4 -4
- package/src/presentation/http/ServerHttpErrorResponseFactory.ts +39 -2
- package/src/presentation/http/hono/CodemationHonoApiAppFactory.ts +33 -8
- package/src/presentation/http/hono/InternalHonoApiRouteRegistrar.ts +12 -0
- package/src/presentation/http/hono/registrars/ManagedMeHonoApiRouteRegistrar.ts +35 -0
- package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +2 -2
- package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +28 -0
- package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +98 -41
- package/src/presentation/server/CodemationConsumerConfigLoader.ts +54 -7
- package/src/presentation/server/CodemationPluginDiscovery.ts +5 -0
- package/src/presentation/server/WorkflowDefinitionExportsResolver.ts +18 -0
- package/src/presentation/server/WorkflowModulePathFinder.ts +12 -1
- package/src/presentation/websocket/ManagedWebsocketAuthenticator.ts +50 -0
- package/src/presentation/websocket/WebsocketAuthenticator.types.ts +12 -0
- package/src/presentation/websocket/WorkflowWebsocketServer.ts +24 -3
- package/src/process/ExecaProcessRunner.ts +41 -0
- package/src/process/ProcessRunner.types.ts +39 -0
- package/src/server.ts +2 -0
- package/src/workflows/InternalWorkflowActivationRegistrar.ts +42 -0
- package/src/workflows/InternalWorkflowDetailRegistrar.ts +33 -0
- package/src/workflows/InternalWorkflowTestRunRegistrar.ts +91 -0
- package/src/workflows/InternalWorkflowsListRegistrar.ts +28 -0
- package/src/workflows/discovery/WorkflowDirectoryDiscoverer.ts +79 -0
- package/tsconfig.json +2 -0
- package/vitest.shared.ts +5 -0
- package/dist/ApiPaths-CLTHphYZ.js.map +0 -1
- package/dist/AppConfigFactory-C6q-CSKb.js.map +0 -1
- package/dist/AppContainerFactory-qaqc-R1D.js.map +0 -1
- package/dist/CodemationAuthoring.types-DZl-sJaM.js.map +0 -1
- package/dist/CodemationConsumerConfigLoader-BeAUS144.js.map +0 -1
- package/dist/CredentialServices-CgxwguAv.js.map +0 -1
- package/dist/PublicFrontendBootstrapFactory-Cb2pLmDd.d.ts +0 -45
- package/dist/PublicFrontendBootstrapJsonCodec-DzqvA0uo.js.map +0 -1
- package/dist/ServerLoggerFactory-BKSIh9Xv.js +0 -98
- package/dist/ServerLoggerFactory-BKSIh9Xv.js.map +0 -1
- package/dist/persistenceServer-vtJAGDat.d.ts +0 -9
- package/src/domain/credentials/OAuth2ConnectServiceFactory.ts +0 -411
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ApplicationRequestError } from "../../application/ApplicationRequestError";
|
|
2
|
-
import {
|
|
2
|
+
import { ApplicationTokens } from "../../applicationTokens";
|
|
3
|
+
import { CoreTokens, inject, injectable, type CredentialTypeRegistry, type WorkflowRepository } from "@codemation/core";
|
|
3
4
|
import { CredentialBindingService } from "../credentials/CredentialServices";
|
|
5
|
+
import { CredentialOAuth2ScopeResolver } from "../credentials/CredentialOAuth2ScopeResolver";
|
|
6
|
+
import type { CredentialStore } from "../credentials/CredentialServices";
|
|
4
7
|
import { WorkflowActivationPreflightRules } from "./WorkflowActivationPreflightRules";
|
|
5
8
|
|
|
6
9
|
@injectable()
|
|
@@ -12,6 +15,12 @@ export class WorkflowActivationPreflight {
|
|
|
12
15
|
private readonly credentialBindingService: CredentialBindingService,
|
|
13
16
|
@inject(WorkflowActivationPreflightRules)
|
|
14
17
|
private readonly rules: WorkflowActivationPreflightRules,
|
|
18
|
+
@inject(CoreTokens.CredentialTypeRegistry)
|
|
19
|
+
private readonly credentialTypeRegistry: CredentialTypeRegistry,
|
|
20
|
+
@inject(ApplicationTokens.CredentialStore)
|
|
21
|
+
private readonly credentialStore: CredentialStore,
|
|
22
|
+
@inject(CredentialOAuth2ScopeResolver)
|
|
23
|
+
private readonly credentialOAuth2ScopeResolver: CredentialOAuth2ScopeResolver,
|
|
15
24
|
) {}
|
|
16
25
|
|
|
17
26
|
async assertCanActivate(workflowId: string): Promise<void> {
|
|
@@ -21,9 +30,23 @@ export class WorkflowActivationPreflight {
|
|
|
21
30
|
throw new ApplicationRequestError(404, `Unknown workflowId: ${decodedId}`);
|
|
22
31
|
}
|
|
23
32
|
const health = await this.credentialBindingService.listWorkflowHealth(decodedId);
|
|
33
|
+
const scopeErrors = await this.rules.collectScopeMismatchErrors(health, {
|
|
34
|
+
getRequiredScopes: (typeId, _requirement) => {
|
|
35
|
+
const type = this.credentialTypeRegistry.getType(typeId);
|
|
36
|
+
if (type?.auth?.kind === "oauth2") {
|
|
37
|
+
return this.credentialOAuth2ScopeResolver.resolveRequestedScopes(type.auth, {});
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
},
|
|
41
|
+
getGrantedScopes: async (instanceId) => {
|
|
42
|
+
const material = await this.credentialStore.getOAuth2Material(instanceId);
|
|
43
|
+
return material?.scopes ?? [];
|
|
44
|
+
},
|
|
45
|
+
});
|
|
24
46
|
const errors = [
|
|
25
47
|
...this.rules.collectNonManualTriggerErrors(workflow),
|
|
26
48
|
...this.rules.collectRequiredCredentialErrors(health),
|
|
49
|
+
...scopeErrors,
|
|
27
50
|
];
|
|
28
51
|
if (errors.length > 0) {
|
|
29
52
|
throw new ApplicationRequestError(400, "Workflow cannot be activated.", errors);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WorkflowCredentialHealthDto } from "../../application/contracts/CredentialContractsRegistry";
|
|
2
|
-
import { getPersistedRuntimeTypeMetadata, injectable, type WorkflowDefinition } from "@codemation/core";
|
|
2
|
+
import { getPersistedRuntimeTypeMetadata, injectable, type CredentialRequirement, type WorkflowDefinition } from "@codemation/core";
|
|
3
3
|
import { MissingRuntimeTriggerToken } from "@codemation/core/bootstrap";
|
|
4
4
|
import { ManualTriggerNode } from "@codemation/core-nodes";
|
|
5
5
|
|
|
@@ -74,4 +74,43 @@ export class WorkflowActivationPreflightRules {
|
|
|
74
74
|
}
|
|
75
75
|
return lines;
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
async collectScopeMismatchErrors(
|
|
79
|
+
health: WorkflowCredentialHealthDto,
|
|
80
|
+
opts: {
|
|
81
|
+
getRequiredScopes: (typeId: string, slotRequirement: CredentialRequirement) => ReadonlyArray<string>;
|
|
82
|
+
getGrantedScopes: (instanceId: string) => Promise<ReadonlyArray<string>>;
|
|
83
|
+
},
|
|
84
|
+
): Promise<ReadonlyArray<string>> {
|
|
85
|
+
const checked = new Set<string>();
|
|
86
|
+
const lines: string[] = [];
|
|
87
|
+
|
|
88
|
+
for (const slot of health.slots) {
|
|
89
|
+
const { instance } = slot;
|
|
90
|
+
if (!instance) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const { instanceId, typeId, displayName } = instance;
|
|
94
|
+
if (checked.has(instanceId)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
checked.add(instanceId);
|
|
98
|
+
|
|
99
|
+
const required = opts.getRequiredScopes(typeId, slot.requirement);
|
|
100
|
+
if (required.length === 0) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const granted = await opts.getGrantedScopes(instanceId);
|
|
105
|
+
const grantedSet = new Set(granted);
|
|
106
|
+
const missing = required.filter((s) => !grantedSet.has(s));
|
|
107
|
+
if (missing.length > 0) {
|
|
108
|
+
lines.push(
|
|
109
|
+
`Credential "${displayName}" missing scopes: ${missing.join(", ")}. Reconnect to grant.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
77
116
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { UpsertLocalBootstrapUserCommand } from "./application/commands/UpsertLo
|
|
|
6
6
|
export type { UpsertLocalBootstrapUserResultDto } from "./application/contracts/userDirectoryContracts.types";
|
|
7
7
|
export { AppContainerFactory } from "./bootstrap/AppContainerFactory";
|
|
8
8
|
export { AppContainerLifecycle } from "./bootstrap/AppContainerLifecycle";
|
|
9
|
+
export { BootTimer } from "./bootstrap/perf/BootTimer";
|
|
10
|
+
export type { BootTracePhase } from "./bootstrap/perf/BootTimer";
|
|
9
11
|
export { DatabaseMigrations } from "./bootstrap/runtime/DatabaseMigrations";
|
|
10
12
|
export { CollectionSchemaSyncerHolder } from "./infrastructure/collections/CollectionSchemaSyncerHolder";
|
|
11
13
|
export { FrontendRuntime } from "./bootstrap/runtime/FrontendRuntime";
|
|
@@ -56,6 +58,10 @@ export { InsertCollectionRowCommand } from "./application/collections/InsertColl
|
|
|
56
58
|
export { UpdateCollectionRowCommand } from "./application/collections/UpdateCollectionRowCommand";
|
|
57
59
|
export { DeleteCollectionRowCommand } from "./application/collections/DeleteCollectionRowCommand";
|
|
58
60
|
export { SyncCollectionsCommand } from "./application/collections/SyncCollectionsCommand";
|
|
61
|
+
export { StartWorkflowRunCommand } from "./application/commands/StartWorkflowRunCommand";
|
|
62
|
+
export type { RunCommandResult } from "./application/contracts/RunContracts";
|
|
63
|
+
export { ApplicationRequestError } from "./application/ApplicationRequestError";
|
|
64
|
+
export { GetRunStateQuery } from "./application/queries/GetRunStateQuery";
|
|
59
65
|
|
|
60
66
|
export type {
|
|
61
67
|
CodemationFrontendAuthProviderSnapshot,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
2
|
|
|
3
|
-
import { mkdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
4
4
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
@@ -72,6 +72,34 @@ export class LocalFilesystemBinaryStorage implements BinaryStorage {
|
|
|
72
72
|
await rm(this.resolveAbsolutePath(storageKey), { force: true });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
async deleteMany(storageKeys: ReadonlyArray<string>): Promise<void> {
|
|
76
|
+
await Promise.all(storageKeys.map((key) => this.delete(key)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async listByPrefix(prefix: string): Promise<ReadonlyArray<string>> {
|
|
80
|
+
const results: string[] = [];
|
|
81
|
+
await this.collectKeysWithPrefix(prefix, this.baseDirectory, results);
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async collectKeysWithPrefix(prefix: string, dir: string, results: string[]): Promise<void> {
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
89
|
+
} catch {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const entryPath = path.join(dir, entry.name);
|
|
94
|
+
const relKey = path.relative(path.resolve(this.baseDirectory), entryPath).replace(/\\/g, "/");
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
await this.collectKeysWithPrefix(prefix, entryPath, results);
|
|
97
|
+
} else if (relKey.startsWith(prefix)) {
|
|
98
|
+
results.push(relKey);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
private resolveAbsolutePath(storageKey: string): string {
|
|
76
104
|
const absoluteBaseDirectory = path.resolve(this.baseDirectory);
|
|
77
105
|
const targetPath = path.resolve(absoluteBaseDirectory, storageKey);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { PassThrough, Readable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DeleteObjectCommand,
|
|
5
|
+
DeleteObjectsCommand,
|
|
6
|
+
HeadBucketCommand,
|
|
7
|
+
HeadObjectCommand,
|
|
8
|
+
ListObjectsV2Command,
|
|
9
|
+
S3Client,
|
|
10
|
+
} from "@aws-sdk/client-s3";
|
|
11
|
+
|
|
12
|
+
import { Upload } from "@aws-sdk/lib-storage";
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
BinaryBody,
|
|
16
|
+
BinaryStorage,
|
|
17
|
+
BinaryStorageReadResult,
|
|
18
|
+
BinaryStorageStatResult,
|
|
19
|
+
BinaryStorageWriteResult,
|
|
20
|
+
} from "@codemation/core";
|
|
21
|
+
|
|
22
|
+
import { BinaryBodyNodeReadableFactory } from "./BinaryBodyNodeReadableFactory";
|
|
23
|
+
import type { S3BinaryStorageConfig } from "./S3BinaryStorageConfig";
|
|
24
|
+
|
|
25
|
+
const DELETE_BATCH_SIZE = 1000;
|
|
26
|
+
|
|
27
|
+
export class S3BinaryStorage implements BinaryStorage {
|
|
28
|
+
readonly driverName = "s3";
|
|
29
|
+
|
|
30
|
+
private readonly client: S3Client;
|
|
31
|
+
private readonly bucket: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param config - S3 connection details.
|
|
35
|
+
* @param forcePathStyle - Use path-style addressing (true for MinIO / testcontainers; false for Scaleway). Default false.
|
|
36
|
+
*/
|
|
37
|
+
constructor(config: S3BinaryStorageConfig, forcePathStyle = false) {
|
|
38
|
+
this.bucket = config.bucket;
|
|
39
|
+
this.client = new S3Client({
|
|
40
|
+
endpoint: config.endpoint,
|
|
41
|
+
region: config.region,
|
|
42
|
+
forcePathStyle,
|
|
43
|
+
credentials: {
|
|
44
|
+
accessKeyId: config.accessKeyId,
|
|
45
|
+
secretAccessKey: config.secretAccessKey,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async write(args: { storageKey: string; body: BinaryBody }): Promise<BinaryStorageWriteResult> {
|
|
51
|
+
const readable = new BinaryBodyNodeReadableFactory(args.body).create();
|
|
52
|
+
let size = 0;
|
|
53
|
+
const passThrough = new PassThrough();
|
|
54
|
+
readable.on("data", (chunk: Buffer) => {
|
|
55
|
+
size += chunk.byteLength;
|
|
56
|
+
});
|
|
57
|
+
readable.pipe(passThrough);
|
|
58
|
+
|
|
59
|
+
const upload = new Upload({
|
|
60
|
+
client: this.client,
|
|
61
|
+
params: {
|
|
62
|
+
Bucket: this.bucket,
|
|
63
|
+
Key: args.storageKey,
|
|
64
|
+
Body: passThrough,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await upload.done();
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
storageKey: args.storageKey,
|
|
72
|
+
size,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async openReadStream(storageKey: string): Promise<BinaryStorageReadResult | undefined> {
|
|
77
|
+
const { GetObjectCommand } = await import("@aws-sdk/client-s3");
|
|
78
|
+
let response;
|
|
79
|
+
try {
|
|
80
|
+
response = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: storageKey }));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (this.isNotFoundError(err)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!response.Body) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const nodeReadable = Readable.from(response.Body as AsyncIterable<Uint8Array>);
|
|
93
|
+
return {
|
|
94
|
+
body: Readable.toWeb(nodeReadable) as BinaryStorageReadResult["body"],
|
|
95
|
+
size: response.ContentLength,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async stat(storageKey: string): Promise<BinaryStorageStatResult> {
|
|
100
|
+
try {
|
|
101
|
+
const response = await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: storageKey }));
|
|
102
|
+
return { exists: true, size: response.ContentLength };
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (this.isNotFoundError(err)) {
|
|
105
|
+
return { exists: false };
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async delete(storageKey: string): Promise<void> {
|
|
112
|
+
await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: storageKey }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async deleteMany(storageKeys: ReadonlyArray<string>): Promise<void> {
|
|
116
|
+
for (let i = 0; i < storageKeys.length; i += DELETE_BATCH_SIZE) {
|
|
117
|
+
const batch = storageKeys.slice(i, i + DELETE_BATCH_SIZE);
|
|
118
|
+
await this.client.send(
|
|
119
|
+
new DeleteObjectsCommand({
|
|
120
|
+
Bucket: this.bucket,
|
|
121
|
+
Delete: {
|
|
122
|
+
Objects: batch.map((key) => ({ Key: key })),
|
|
123
|
+
Quiet: true,
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async listByPrefix(prefix: string): Promise<ReadonlyArray<string>> {
|
|
131
|
+
const keys: string[] = [];
|
|
132
|
+
let continuationToken: string | undefined;
|
|
133
|
+
|
|
134
|
+
do {
|
|
135
|
+
const response = await this.client.send(
|
|
136
|
+
new ListObjectsV2Command({
|
|
137
|
+
Bucket: this.bucket,
|
|
138
|
+
Prefix: prefix,
|
|
139
|
+
ContinuationToken: continuationToken,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
for (const obj of response.Contents ?? []) {
|
|
144
|
+
if (obj.Key) {
|
|
145
|
+
keys.push(obj.Key);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
continuationToken = response.NextContinuationToken;
|
|
150
|
+
} while (continuationToken);
|
|
151
|
+
|
|
152
|
+
return keys;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Checks that the configured bucket is reachable. Throws if not. */
|
|
156
|
+
async checkConnectivity(): Promise<void> {
|
|
157
|
+
await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private isNotFoundError(err: unknown): boolean {
|
|
161
|
+
if (typeof err !== "object" || err === null) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const anyErr = err as Record<string, unknown>;
|
|
165
|
+
const statusCode =
|
|
166
|
+
anyErr["$metadata"] != null ? (anyErr["$metadata"] as Record<string, unknown>)["httpStatusCode"] : undefined;
|
|
167
|
+
return statusCode === 404 || anyErr["name"] === "NotFound" || anyErr["name"] === "NoSuchKey";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export interface S3BinaryStorageConfig {
|
|
4
|
+
readonly endpoint: string;
|
|
5
|
+
readonly region: string;
|
|
6
|
+
readonly bucket: string;
|
|
7
|
+
readonly accessKeyId: string;
|
|
8
|
+
readonly secretAccessKey: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const S3BinaryStorageConfigSchema = z.object({
|
|
12
|
+
endpoint: z.string().min(1),
|
|
13
|
+
region: z.string().min(1),
|
|
14
|
+
bucket: z.string().min(1),
|
|
15
|
+
accessKeyId: z.string().min(1),
|
|
16
|
+
secretAccessKey: z.string().min(1),
|
|
17
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyCredentialType, CollectionDefinition, Container } from "@codemation/core";
|
|
1
|
+
import type { AnyCredentialType, CollectionDefinition, Container, McpServerDeclaration } from "@codemation/core";
|
|
2
2
|
import type { LoggerFactory } from "../../application/logging/Logger";
|
|
3
3
|
import type { AppConfig } from "../../presentation/config/AppConfig";
|
|
4
4
|
import type { CodemationPlugin } from "../../presentation/config/CodemationPlugin";
|
|
@@ -11,6 +11,7 @@ export class CodemationPluginRegistrar {
|
|
|
11
11
|
appConfig: AppConfig;
|
|
12
12
|
registerCredentialType: (type: AnyCredentialType) => void;
|
|
13
13
|
registerCollection: (definition: CollectionDefinition) => void;
|
|
14
|
+
mergeMcpServers: (declarations: ReadonlyArray<McpServerDeclaration>) => void;
|
|
14
15
|
loggerFactory: LoggerFactory;
|
|
15
16
|
}>,
|
|
16
17
|
): Promise<void> {
|
|
@@ -18,6 +19,7 @@ export class CodemationPluginRegistrar {
|
|
|
18
19
|
for (const credentialType of plugin.credentialTypes ?? []) {
|
|
19
20
|
args.registerCredentialType(credentialType);
|
|
20
21
|
}
|
|
22
|
+
args.mergeMcpServers(plugin.mcpServers ?? []);
|
|
21
23
|
if (!plugin.register) {
|
|
22
24
|
continue;
|
|
23
25
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { AppPersistenceConfig } from "../../presentation/config/AppConfig";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parses `CODEMATION_DATABASE_URL` into an {@link AppPersistenceConfig}.
|
|
6
|
+
*
|
|
7
|
+
* Supported schemes (case-insensitive):
|
|
8
|
+
* - `sqlite://relative/path/to/file.db` → resolved relative to consumerRoot
|
|
9
|
+
* - `sqlite:///absolute/path/to/file.db` → leading slash = POSIX absolute
|
|
10
|
+
* - `sqlite://C:/path/file.db` → Windows-style absolute (path.isAbsolute()
|
|
11
|
+
* returns true for these)
|
|
12
|
+
* - `pgsql://user:pass@host:5432/dbname` → normalised to postgresql://
|
|
13
|
+
* - `postgresql://user:pass@host:5432/db` → pass-through (Prisma's expected scheme)
|
|
14
|
+
* - `postgres://user:pass@host:5432/db` → pass-through (common alias)
|
|
15
|
+
*
|
|
16
|
+
* Throws on any other scheme. Empty / whitespace input is also an error — callers
|
|
17
|
+
* should default before calling parse().
|
|
18
|
+
*/
|
|
19
|
+
export class CodemationDatabaseUrlParser {
|
|
20
|
+
parse(url: string, consumerRoot: string): AppPersistenceConfig {
|
|
21
|
+
const trimmed = url.trim();
|
|
22
|
+
if (trimmed.length === 0) {
|
|
23
|
+
throw new Error("CODEMATION_DATABASE_URL is empty.");
|
|
24
|
+
}
|
|
25
|
+
if (trimmed.toLowerCase().startsWith("sqlite://")) {
|
|
26
|
+
const remainder = trimmed.slice("sqlite://".length);
|
|
27
|
+
const filePath = path.isAbsolute(remainder) ? remainder : path.resolve(consumerRoot, remainder);
|
|
28
|
+
return { kind: "sqlite", databaseFilePath: filePath };
|
|
29
|
+
}
|
|
30
|
+
if (trimmed.toLowerCase().startsWith("pgsql://")) {
|
|
31
|
+
return { kind: "postgresql", databaseUrl: `postgresql://${trimmed.slice("pgsql://".length)}` };
|
|
32
|
+
}
|
|
33
|
+
if (trimmed.toLowerCase().startsWith("postgresql://") || trimmed.toLowerCase().startsWith("postgres://")) {
|
|
34
|
+
return { kind: "postgresql", databaseUrl: trimmed };
|
|
35
|
+
}
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Unsupported CODEMATION_DATABASE_URL scheme: "${trimmed}". ` +
|
|
38
|
+
`Use sqlite://, pgsql://, postgresql://, or postgres://.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
TelemetryArtifactRecord,
|
|
5
5
|
TelemetryArtifactStore,
|
|
6
6
|
TelemetryArtifactWrite,
|
|
7
|
+
TelemetryPruneArgs,
|
|
7
8
|
} from "../../domain/telemetry/TelemetryContracts";
|
|
8
9
|
|
|
9
10
|
@injectable()
|
|
@@ -43,14 +44,18 @@ export class InMemoryTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
43
44
|
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
async pruneExpired(args:
|
|
47
|
+
async pruneExpired(args: TelemetryPruneArgs): Promise<{ count: number; storageKeys: ReadonlyArray<string> }> {
|
|
47
48
|
const candidates = [...this.rows.entries()]
|
|
48
49
|
.filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
|
|
49
50
|
.sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
|
|
50
51
|
.slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
|
|
51
|
-
|
|
52
|
+
const storageKeys: string[] = [];
|
|
53
|
+
for (const [key, row] of candidates) {
|
|
54
|
+
if (row.payloadStorageKey) {
|
|
55
|
+
storageKeys.push(row.payloadStorageKey);
|
|
56
|
+
}
|
|
52
57
|
this.rows.delete(key);
|
|
53
58
|
}
|
|
54
|
-
return candidates.length;
|
|
59
|
+
return { count: candidates.length, storageKeys };
|
|
55
60
|
}
|
|
56
61
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Client } from "@libsql/client";
|
|
2
2
|
import { injectable } from "@codemation/core";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
@@ -105,11 +105,12 @@ export class PrismaMigrationDeployer {
|
|
|
105
105
|
env?: Readonly<NodeJS.ProcessEnv>;
|
|
106
106
|
}>,
|
|
107
107
|
): Promise<void> {
|
|
108
|
-
const
|
|
108
|
+
const resolverEnv = { ...process.env, ...(args.env ?? {}) };
|
|
109
|
+
const prismaConfigPath = this.resolveAbsolutePrismaConfigPath(resolverEnv);
|
|
109
110
|
await new Promise<void>((resolve, reject) => {
|
|
110
111
|
const command = spawn(
|
|
111
112
|
process.execPath,
|
|
112
|
-
[...[this.resolvePrismaCliPath(), ...args.prismaArgs], "--config", path.basename(prismaConfigPath)],
|
|
113
|
+
[...[this.resolvePrismaCliPath(resolverEnv), ...args.prismaArgs], "--config", path.basename(prismaConfigPath)],
|
|
113
114
|
{
|
|
114
115
|
cwd: path.dirname(prismaConfigPath),
|
|
115
116
|
env: this.createProcessEnvironment(args.databaseUrl, args.provider, args.env),
|
|
@@ -177,6 +178,10 @@ export class PrismaMigrationDeployer {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
private async repairPartiallyAppliedNormalizedRuntimeSqliteDatabase(databaseFilePath: string): Promise<boolean> {
|
|
181
|
+
// Lazy import: @libsql/client pulls in platform-specific native bindings that confuse the
|
|
182
|
+
// Next.js / Turbopack module tracer (forcing the whole project to be traced via NFT). This
|
|
183
|
+
// recovery path is rarely needed, so defer the load until it's actually invoked.
|
|
184
|
+
const { createClient } = await import("@libsql/client");
|
|
180
185
|
const client = createClient({ url: this.sqliteFilePathToDatabaseUrl(databaseFilePath) });
|
|
181
186
|
try {
|
|
182
187
|
const failedMigration = await this.hasActiveFailedMigrationRecord(
|
|
@@ -288,6 +293,7 @@ export class PrismaMigrationDeployer {
|
|
|
288
293
|
}
|
|
289
294
|
|
|
290
295
|
private async cleanupNormalizedRuntimeLegacyArtifacts(databaseFilePath: string): Promise<void> {
|
|
296
|
+
const { createClient } = await import("@libsql/client");
|
|
291
297
|
const client = createClient({ url: this.sqliteFilePathToDatabaseUrl(databaseFilePath) });
|
|
292
298
|
try {
|
|
293
299
|
const runColumns = await this.readSqliteTableColumns(client, "Run");
|
|
@@ -326,14 +332,15 @@ export class PrismaMigrationDeployer {
|
|
|
326
332
|
};
|
|
327
333
|
}
|
|
328
334
|
|
|
329
|
-
private resolvePrismaCliPath(): string {
|
|
330
|
-
const configuredPath =
|
|
335
|
+
private resolvePrismaCliPath(env: Readonly<NodeJS.ProcessEnv>): string {
|
|
336
|
+
const configuredPath = env.CODEMATION_PRISMA_CLI_PATH;
|
|
331
337
|
if (configuredPath && existsSync(configuredPath)) {
|
|
332
338
|
return configuredPath;
|
|
333
339
|
}
|
|
340
|
+
const packageRoot = this.resolvePackageRoot(env);
|
|
334
341
|
const packageManagerCandidates = [
|
|
335
342
|
path.resolve(process.cwd(), "node_modules", "prisma", "build", "index.js"),
|
|
336
|
-
path.resolve(
|
|
343
|
+
path.resolve(packageRoot, "node_modules", "prisma", "build", "index.js"),
|
|
337
344
|
];
|
|
338
345
|
for (const candidate of packageManagerCandidates) {
|
|
339
346
|
if (existsSync(candidate)) {
|
|
@@ -342,7 +349,7 @@ export class PrismaMigrationDeployer {
|
|
|
342
349
|
}
|
|
343
350
|
try {
|
|
344
351
|
return this.require.resolve("prisma/build/index.js", {
|
|
345
|
-
paths: [process.cwd(),
|
|
352
|
+
paths: [process.cwd(), packageRoot],
|
|
346
353
|
});
|
|
347
354
|
} catch {
|
|
348
355
|
throw new Error(
|
|
@@ -351,16 +358,17 @@ export class PrismaMigrationDeployer {
|
|
|
351
358
|
}
|
|
352
359
|
}
|
|
353
360
|
|
|
354
|
-
private resolveAbsolutePrismaConfigPath(): string {
|
|
355
|
-
const configuredPath =
|
|
361
|
+
private resolveAbsolutePrismaConfigPath(env: Readonly<NodeJS.ProcessEnv>): string {
|
|
362
|
+
const configuredPath = env.CODEMATION_PRISMA_CONFIG_PATH;
|
|
363
|
+
const packageRoot = this.resolvePackageRoot(env);
|
|
356
364
|
if (configuredPath) {
|
|
357
|
-
return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(
|
|
365
|
+
return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(packageRoot, configuredPath);
|
|
358
366
|
}
|
|
359
|
-
return path.resolve(
|
|
367
|
+
return path.resolve(packageRoot, "prisma.config.ts");
|
|
360
368
|
}
|
|
361
369
|
|
|
362
|
-
resolvePackageRoot(): string {
|
|
363
|
-
const configuredRoot =
|
|
370
|
+
resolvePackageRoot(env: Readonly<NodeJS.ProcessEnv> = process.env): string {
|
|
371
|
+
const configuredRoot = env.CODEMATION_HOST_PACKAGE_ROOT;
|
|
364
372
|
if (configuredRoot) {
|
|
365
373
|
return configuredRoot;
|
|
366
374
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BinaryBody, BinaryStorage } from "@codemation/core";
|
|
2
|
+
import { CoreTokens, inject, injectable } from "@codemation/core";
|
|
2
3
|
import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
|
|
3
4
|
import type {
|
|
4
5
|
TelemetryArtifactRecord,
|
|
5
6
|
TelemetryArtifactStore,
|
|
6
7
|
TelemetryArtifactWrite,
|
|
8
|
+
TelemetryPruneArgs,
|
|
7
9
|
} from "../../domain/telemetry/TelemetryContracts";
|
|
8
10
|
import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
|
|
9
11
|
|
|
12
|
+
/** Payloads larger than this byte threshold are offloaded to BinaryStorage. */
|
|
13
|
+
const PAYLOAD_OFFLOAD_THRESHOLD_BYTES = 64_000;
|
|
14
|
+
|
|
10
15
|
@injectable()
|
|
11
16
|
export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
12
17
|
constructor(
|
|
@@ -14,11 +19,36 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
14
19
|
private readonly prisma: PrismaDatabaseClient,
|
|
15
20
|
@inject(OtelIdentityFactory)
|
|
16
21
|
private readonly otelIdentityFactory: OtelIdentityFactory,
|
|
22
|
+
@inject(CoreTokens.BinaryStorage)
|
|
23
|
+
private readonly binaryStorage: BinaryStorage,
|
|
17
24
|
) {}
|
|
18
25
|
|
|
19
26
|
async save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord> {
|
|
20
27
|
const artifactId = this.otelIdentityFactory.createArtifactId();
|
|
21
28
|
const createdAt = new Date().toISOString();
|
|
29
|
+
|
|
30
|
+
// Resolve inline vs offloaded payload
|
|
31
|
+
let payloadText: string | null = record.payloadText ?? null;
|
|
32
|
+
let payloadJson: string | null = record.payloadJson !== undefined ? JSON.stringify(record.payloadJson) : null;
|
|
33
|
+
let payloadStorageKey: string | null = null;
|
|
34
|
+
|
|
35
|
+
const payloadTextBytes = payloadText ? Buffer.byteLength(payloadText, "utf8") : 0;
|
|
36
|
+
const payloadJsonBytes = payloadJson ? Buffer.byteLength(payloadJson, "utf8") : 0;
|
|
37
|
+
|
|
38
|
+
if (payloadTextBytes > PAYLOAD_OFFLOAD_THRESHOLD_BYTES) {
|
|
39
|
+
const storageKey = `telemetry-artifacts/${artifactId}.txt`;
|
|
40
|
+
const body: BinaryBody = Buffer.from(payloadText!, "utf8");
|
|
41
|
+
await this.binaryStorage.write({ storageKey, body });
|
|
42
|
+
payloadStorageKey = storageKey;
|
|
43
|
+
payloadText = null;
|
|
44
|
+
} else if (payloadJsonBytes > PAYLOAD_OFFLOAD_THRESHOLD_BYTES) {
|
|
45
|
+
const storageKey = `telemetry-artifacts/${artifactId}.json`;
|
|
46
|
+
const body: BinaryBody = Buffer.from(payloadJson!, "utf8");
|
|
47
|
+
await this.binaryStorage.write({ storageKey, body });
|
|
48
|
+
payloadStorageKey = storageKey;
|
|
49
|
+
payloadJson = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
22
52
|
await this.prisma.telemetryArtifact.create({
|
|
23
53
|
data: {
|
|
24
54
|
artifactId,
|
|
@@ -32,8 +62,9 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
32
62
|
contentType: record.contentType,
|
|
33
63
|
previewText: record.previewText ?? null,
|
|
34
64
|
previewJson: record.previewJson !== undefined ? JSON.stringify(record.previewJson) : null,
|
|
35
|
-
payloadText
|
|
36
|
-
payloadJson
|
|
65
|
+
payloadText,
|
|
66
|
+
payloadJson,
|
|
67
|
+
payloadStorageKey,
|
|
37
68
|
bytes: record.bytes ?? null,
|
|
38
69
|
truncated: record.truncated ?? null,
|
|
39
70
|
createdAt,
|
|
@@ -53,8 +84,9 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
53
84
|
contentType: record.contentType,
|
|
54
85
|
previewText: record.previewText,
|
|
55
86
|
previewJson: record.previewJson,
|
|
56
|
-
payloadText:
|
|
57
|
-
payloadJson:
|
|
87
|
+
payloadText: payloadText ?? undefined,
|
|
88
|
+
payloadJson: payloadJson !== null ? JSON.parse(payloadJson) : undefined,
|
|
89
|
+
payloadStorageKey: payloadStorageKey ?? undefined,
|
|
58
90
|
bytes: record.bytes,
|
|
59
91
|
truncated: record.truncated,
|
|
60
92
|
createdAt,
|
|
@@ -82,6 +114,7 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
82
114
|
previewJson: this.parseJson(row.previewJson),
|
|
83
115
|
payloadText: row.payloadText ?? undefined,
|
|
84
116
|
payloadJson: this.parseJson(row.payloadJson),
|
|
117
|
+
payloadStorageKey: row.payloadStorageKey ?? undefined,
|
|
85
118
|
bytes: row.bytes ?? undefined,
|
|
86
119
|
truncated: row.truncated ?? undefined,
|
|
87
120
|
createdAt: row.createdAt,
|
|
@@ -90,7 +123,7 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
90
123
|
}));
|
|
91
124
|
}
|
|
92
125
|
|
|
93
|
-
async pruneExpired(args:
|
|
126
|
+
async pruneExpired(args: TelemetryPruneArgs): Promise<{ count: number; storageKeys: ReadonlyArray<string> }> {
|
|
94
127
|
const rows = await this.prisma.telemetryArtifact.findMany({
|
|
95
128
|
where: {
|
|
96
129
|
retentionExpiresAt: {
|
|
@@ -99,13 +132,15 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
99
132
|
},
|
|
100
133
|
select: {
|
|
101
134
|
artifactId: true,
|
|
135
|
+
payloadStorageKey: true,
|
|
102
136
|
},
|
|
103
137
|
orderBy: [{ retentionExpiresAt: "asc" }, { artifactId: "asc" }],
|
|
104
138
|
...(args.limit ? { take: args.limit } : {}),
|
|
105
139
|
});
|
|
106
140
|
if (rows.length === 0) {
|
|
107
|
-
return 0;
|
|
141
|
+
return { count: 0, storageKeys: [] };
|
|
108
142
|
}
|
|
143
|
+
const storageKeys = rows.flatMap((row) => (row.payloadStorageKey ? [row.payloadStorageKey] : []));
|
|
109
144
|
const result = await this.prisma.telemetryArtifact.deleteMany({
|
|
110
145
|
where: {
|
|
111
146
|
artifactId: {
|
|
@@ -113,7 +148,7 @@ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
|
|
|
113
148
|
},
|
|
114
149
|
},
|
|
115
150
|
});
|
|
116
|
-
return result.count;
|
|
151
|
+
return { count: result.count, storageKeys };
|
|
117
152
|
}
|
|
118
153
|
|
|
119
154
|
private parseJson(value: string | null): unknown {
|