@codemation/host 0.8.0 → 0.9.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 +37 -0
- package/dist/{ApiPaths-Dv1dcHu_.js → ApiPaths-DCvrlIjg.js} +12 -1
- package/dist/{ApiPaths-Dv1dcHu_.js.map → ApiPaths-DCvrlIjg.js.map} +1 -1
- package/dist/{AppConfigFactory-Cx4qQvRk.js → AppConfigFactory-D4LL1aOR.js} +77 -297
- package/dist/AppConfigFactory-D4LL1aOR.js.map +1 -0
- package/dist/{AppConfigFactory-BT0y0LVC.d.ts → AppConfigFactory-DncmwCD1.d.ts} +2918 -199
- package/dist/{AppContainerFactory-DRTjG7nG.js → AppContainerFactory-jpYXGZGe.js} +1724 -474
- package/dist/AppContainerFactory-jpYXGZGe.js.map +1 -0
- package/dist/{CodemationAppContext-CGFYVcSb.d.ts → CodemationAppContext-K51b7oXe.d.ts} +3 -3
- package/dist/{CodemationAuthoring.types-DiKKogum.d.ts → CodemationAuthoring.types-BXlXIl4K.d.ts} +4 -4
- package/dist/{CodemationConfigNormalizer-48f-T66P.d.ts → CodemationConfigNormalizer-B4rDYC9h.d.ts} +3 -3
- package/dist/{CodemationConsumerConfigLoader-_PIYqwVx.d.ts → CodemationConsumerConfigLoader-Dt4jyLx6.d.ts} +2 -2
- package/dist/{CodemationPluginListMerger-DP7djJ9S.d.ts → CodemationPluginListMerger-DS6I3Xe0.d.ts} +24 -12
- package/dist/{persistenceServer-C-hH4z6l.js → CodemationPostgresPrismaClientFactory-C7156Fe-.js} +2 -2
- package/dist/CodemationPostgresPrismaClientFactory-C7156Fe-.js.map +1 -0
- package/dist/CodemationPostgresPrismaClientFactory-CTNTPnDr.d.ts +9 -0
- package/dist/{CredentialContractsRegistry-Bq2bq28t.d.ts → CredentialContractsRegistry-Dgu-rEXi.d.ts} +16 -3
- package/dist/{CredentialServices-BLloBztI.d.ts → CredentialServices-B3wPyp2y.d.ts} +4 -4
- package/dist/{CredentialServices-Dk8yypeL.js → CredentialServices-Bios0dM8.js} +10 -4
- package/dist/CredentialServices-Bios0dM8.js.map +1 -0
- package/dist/{InternalHonoApiRouteRegistrar-c7t3KnV_.d.ts → InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts} +1 -1
- package/dist/{InternalPingRegistrar-DY3kSfxP.js → InternalPingRegistrar-BavAAnvk.js} +19 -16
- package/dist/InternalPingRegistrar-BavAAnvk.js.map +1 -0
- package/dist/{ItemsInputNormalizer-_RwIfRIQ.d.ts → ItemsInputNormalizer-CFkfNMLt.d.ts} +1434 -1225
- package/dist/PrismaMigrationDeployer-DdEcXXVi.d.ts +14 -0
- package/dist/{PublicFrontendBootstrapFactory-Dv04tJ-6.d.ts → PublicFrontendBootstrapFactory-ClEjZP74.d.ts} +2 -2
- package/dist/{PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts → PublicFrontendBootstrapJsonCodec-HNItQ7ol.d.ts} +6 -1
- package/dist/{TelemetryContracts-BtDx84Cp.d.ts → TelemetryContracts-DpZEODQM.d.ts} +2 -2
- package/dist/{WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts → WorkflowPolicyUiPresentationFactory-BNn2fvR_.d.ts} +2 -2
- package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js} +1 -1
- package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js.map} +1 -1
- package/dist/authoring.d.ts +4 -4
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/consumer.d.ts +5 -5
- package/dist/credentials.d.ts +5 -5
- package/dist/credentials.js +1 -1
- package/dist/devServerSidecar.d.ts +2 -2
- package/dist/dto.d.ts +5 -5
- package/dist/{index-DilAYwnH.d.ts → index-ChIfeWzk.d.ts} +71 -28
- package/dist/index.d.ts +17 -16
- package/dist/index.js +8 -8
- package/dist/infrastructure/persistence/PrismaMigrationOperations.d.ts +44 -0
- package/dist/infrastructure/persistence/PrismaMigrationOperations.js +302 -0
- package/dist/infrastructure/persistence/PrismaMigrationOperations.js.map +1 -0
- package/dist/mapping.d.ts +2 -2
- package/dist/mapping.js +1 -1
- package/dist/nextServer.d.ts +15 -13
- package/dist/nextServer.js +6 -6
- package/dist/pairing.d.ts +28 -9
- package/dist/pairing.js +19 -3
- package/dist/pairing.js.map +1 -0
- package/dist/{pairing.types-snfZ_OzB.d.ts → pairing.types-D9Bjn98U.d.ts} +1 -1
- package/dist/persistenceServer.d.ts +31 -7
- package/dist/persistenceServer.js +2 -2
- package/dist/{server-09PKasWR.d.ts → server-B5trn7y4.d.ts} +5 -5
- package/dist/{server-vtRCPgRJ.js → server-BlG9qV5S.js} +4 -4
- package/dist/{server-vtRCPgRJ.js.map → server-BlG9qV5S.js.map} +1 -1
- package/dist/server.d.ts +10 -10
- package/dist/server.js +8 -8
- package/package.json +10 -9
- package/playwright.config.ts +8 -2
- package/playwright.scaffolded-dev.config.ts +8 -2
- package/prisma/migrations/20260526120000_credential_material_pointer/migration.sql +18 -0
- package/prisma/migrations/20260527120000_add_human_task/migration.sql +32 -0
- package/prisma/migrations/20260527130000_add_hitl_state_json/migration.sql +6 -0
- package/prisma/migrations/20260527130000_add_hmac_nonce/migration.sql +12 -0
- package/prisma/migrations.sqlite/20260526120000_credential_material_pointer/migration.sql +13 -0
- package/prisma/migrations.sqlite/20260527120000_add_human_task/migration.sql +30 -0
- package/prisma/migrations.sqlite/20260527130000_add_hitl_state_json/migration.sql +6 -0
- package/prisma/migrations.sqlite/20260527130000_add_hmac_nonce/migration.sql +9 -0
- package/prisma/schema.postgresql.prisma +48 -0
- package/prisma/schema.sqlite.prisma +48 -0
- package/prisma-generated/prisma-postgresql-client/edge.js +40 -6
- package/prisma-generated/prisma-postgresql-client/index-browser.js +36 -2
- package/prisma-generated/prisma-postgresql-client/index.d.ts +3179 -163
- package/prisma-generated/prisma-postgresql-client/index.js +40 -6
- package/prisma-generated/prisma-postgresql-client/package.json +1 -1
- package/prisma-generated/prisma-postgresql-client/schema.prisma +48 -0
- package/prisma-generated/prisma-sqlite-client/edge.js +40 -6
- package/prisma-generated/prisma-sqlite-client/index-browser.js +36 -2
- package/prisma-generated/prisma-sqlite-client/index.d.ts +3175 -163
- package/prisma-generated/prisma-sqlite-client/index.js +40 -6
- package/prisma-generated/prisma-sqlite-client/package.json +1 -1
- package/prisma-generated/prisma-sqlite-client/schema.prisma +48 -0
- package/src/application/contracts/CredentialContractsRegistry.ts +15 -0
- package/src/application/credentials/AppGalleryProjector.ts +69 -0
- package/src/application/hitl/DecideHumanTaskCommandHandler.ts +149 -0
- package/src/application/hitl/DecisionSchemaValidator.ts +22 -0
- package/src/application/hitl/HitlCallbackHandler.ts +96 -0
- package/src/application/mapping/WorkflowDefinitionMapper.ts +1 -3
- package/src/application/queries/CredentialQueryHandlers.ts +2 -0
- package/src/application/queries/GetCredentialAppsQuery.ts +4 -0
- package/src/application/queries/GetCredentialAppsQueryHandler.ts +27 -0
- package/src/application/telemetry/ResumeTelemetryContextForRun.ts +53 -0
- package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +9 -8
- package/src/applicationTokens.ts +11 -1
- package/src/auth/managed/ManagedCorsMiddleware.ts +20 -5
- package/src/bootstrap/AppContainerFactory.ts +100 -0
- package/src/credentials/CachingCredentialMaterialProvider.ts +96 -0
- package/src/credentials/CompositeCredentialMaterialProvider.ts +47 -0
- package/src/credentials/ControlPlaneCatalogFetcher.ts +4 -24
- package/src/credentials/ControlPlaneCredentialMaterialProvider.ts +79 -0
- package/src/credentials/CredentialOAuth2MaterialReader.ts +2 -7
- package/src/credentials/InternalCredentialsBindingRegistrar.ts +83 -0
- package/src/credentials/LocalCredentialMaterialProvider.ts +92 -0
- package/src/domain/credentials/CredentialInstanceService.ts +5 -1
- package/src/domain/credentials/CredentialTypeRegistryImpl.ts +18 -4
- package/src/domain/workflows/WorkflowActivationPreflightRules.ts +7 -4
- package/src/dto.ts +2 -0
- package/src/hitl/ControlPlaneInboxChannel.ts +102 -0
- package/src/hitl/HitlResumeTokenSigner.ts +80 -0
- package/src/hitl/HitlTimeoutJobScheduler.ts +77 -0
- package/src/hitl/HitlTimeoutWorker.ts +138 -0
- package/src/hitl/InboxChannelResolver.ts +49 -0
- package/src/hitl/LocalInboxChannel.ts +37 -0
- package/src/infrastructure/persistence/PrismaCredentialStore.ts +10 -0
- package/src/infrastructure/persistence/PrismaHmacNonceStore.ts +29 -0
- package/src/infrastructure/persistence/PrismaHumanTaskStore.ts +156 -0
- package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +53 -383
- package/src/infrastructure/persistence/PrismaMigrationOperations.ts +401 -0
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +39 -0
- package/src/mcp/AgentMcpIntegrationImpl.ts +5 -1
- package/src/pairing/HmacNonceStore.ts +14 -0
- package/src/pairing/HmacNonceStoreToken.ts +4 -0
- package/src/pairing/HmacRequestSigner.ts +10 -1
- package/src/pairing/InMemoryHmacNonceStore.ts +24 -0
- package/src/pairing/IncomingHmacVerifier.ts +28 -12
- package/src/pairing/InternalHmacAuthMiddleware.ts +1 -1
- package/src/pairing/index.ts +3 -0
- package/src/presentation/http/ApiPaths.ts +14 -0
- package/src/presentation/http/hono/HonoHttpAnonymousRoutePolicyRegistry.ts +4 -0
- package/src/presentation/http/hono/registrars/CredentialHonoApiRouteRegistrar.ts +1 -0
- package/src/presentation/http/hono/registrars/HitlDecideHonoApiRouteRegistrar.ts +54 -0
- package/src/presentation/http/hono/registrars/HitlInternalCallbackHonoApiRouteRegistrar.ts +33 -0
- package/src/presentation/http/hono/registrars/HitlResumeHonoApiRouteRegistrar.ts +43 -0
- package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +9 -0
- package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +1 -1
- package/src/server.ts +7 -2
- package/src/workflows/InternalWorkflowTestRunRegistrar.ts +9 -0
- package/tsconfig.json +1 -0
- package/dist/AppConfigFactory-Cx4qQvRk.js.map +0 -1
- package/dist/AppContainerFactory-DRTjG7nG.js.map +0 -1
- package/dist/CredentialServices-Dk8yypeL.js.map +0 -1
- package/dist/InternalPingRegistrar-DY3kSfxP.js.map +0 -1
- package/dist/persistenceServer-B71RGvSj.d.ts +0 -30
- package/dist/persistenceServer-C-hH4z6l.js.map +0 -1
- package/src/credentials/catalogTypes.ts +0 -4
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import type { Client } from "@libsql/client";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import type { AppPersistenceConfig } from "../../presentation/config/AppConfig";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Contains all runtime logic for deploying Prisma migrations.
|
|
12
|
+
* This class is loaded lazily via `await import(...)` from PrismaMigrationDeployer
|
|
13
|
+
* so that the dynamic fs/path/createRequire operations never appear in the
|
|
14
|
+
* static module graph visible to the Turbopack / Next.js NFT tracer.
|
|
15
|
+
*/
|
|
16
|
+
export class PrismaMigrationOperations {
|
|
17
|
+
private static readonly normalizedRuntimeMigrationName = "20260407140000_run_normalized_persistence";
|
|
18
|
+
private readonly require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
async deployPersistence(persistence: AppPersistenceConfig, env?: Readonly<NodeJS.ProcessEnv>): Promise<void> {
|
|
21
|
+
if (persistence.kind === "none") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (persistence.kind === "postgresql") {
|
|
25
|
+
await this.deployPostgres({ databaseUrl: persistence.databaseUrl, env });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await this.deploySqlite({ databaseFilePath: persistence.databaseFilePath, env });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async deploy(args: Readonly<{ databaseUrl: string; env?: Readonly<NodeJS.ProcessEnv> }>): Promise<void> {
|
|
32
|
+
await this.deployPostgres(args);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
resolvePackageRoot(env: Readonly<NodeJS.ProcessEnv> = process.env): string {
|
|
36
|
+
const configuredRoot = env.CODEMATION_HOST_PACKAGE_ROOT;
|
|
37
|
+
if (configuredRoot) {
|
|
38
|
+
return configuredRoot;
|
|
39
|
+
}
|
|
40
|
+
let currentDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
42
|
+
if (existsSync(path.join(currentDirectory, "prisma", "schema.postgresql.prisma"))) {
|
|
43
|
+
return currentDirectory;
|
|
44
|
+
}
|
|
45
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
46
|
+
if (parentDirectory === currentDirectory) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
currentDirectory = parentDirectory;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Could not locate prisma/schema.postgresql.prisma near ${fileURLToPath(import.meta.url)}.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async deploySqlite(
|
|
55
|
+
args: Readonly<{ databaseFilePath: string; env?: Readonly<NodeJS.ProcessEnv> }>,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
await this.ensureSqliteParentDirectoryExists(args.databaseFilePath);
|
|
58
|
+
const databaseUrl = this.sqliteFilePathToDatabaseUrl(args.databaseFilePath);
|
|
59
|
+
try {
|
|
60
|
+
await this.deployWithProvider({
|
|
61
|
+
provider: "sqlite",
|
|
62
|
+
databaseUrl,
|
|
63
|
+
env: args.env,
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const recovered = await this.tryRecoverPartiallyAppliedNormalizedRuntimeMigration({
|
|
67
|
+
databaseFilePath: args.databaseFilePath,
|
|
68
|
+
databaseUrl,
|
|
69
|
+
env: args.env,
|
|
70
|
+
error,
|
|
71
|
+
});
|
|
72
|
+
if (!recovered) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await this.cleanupNormalizedRuntimeLegacyArtifacts(args.databaseFilePath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async deployPostgres(
|
|
80
|
+
args: Readonly<{ databaseUrl: string; env?: Readonly<NodeJS.ProcessEnv> }>,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
await this.deployWithProvider({
|
|
83
|
+
provider: "postgresql",
|
|
84
|
+
databaseUrl: args.databaseUrl,
|
|
85
|
+
env: args.env,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async deployWithProvider(
|
|
90
|
+
args: Readonly<{
|
|
91
|
+
provider: "postgresql" | "sqlite";
|
|
92
|
+
databaseUrl: string;
|
|
93
|
+
env?: Readonly<NodeJS.ProcessEnv>;
|
|
94
|
+
}>,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
await this.runPrismaCommand({
|
|
97
|
+
prismaArgs: ["migrate", "deploy"],
|
|
98
|
+
provider: args.provider,
|
|
99
|
+
databaseUrl: args.databaseUrl,
|
|
100
|
+
env: args.env,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async resolveAppliedMigration(
|
|
105
|
+
args: Readonly<{
|
|
106
|
+
provider: "postgresql" | "sqlite";
|
|
107
|
+
databaseUrl: string;
|
|
108
|
+
migrationName: string;
|
|
109
|
+
env?: Readonly<NodeJS.ProcessEnv>;
|
|
110
|
+
}>,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
await this.runPrismaCommand({
|
|
113
|
+
prismaArgs: ["migrate", "resolve", "--applied", args.migrationName],
|
|
114
|
+
provider: args.provider,
|
|
115
|
+
databaseUrl: args.databaseUrl,
|
|
116
|
+
env: args.env,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async runPrismaCommand(
|
|
121
|
+
args: Readonly<{
|
|
122
|
+
prismaArgs: string[];
|
|
123
|
+
provider: "postgresql" | "sqlite";
|
|
124
|
+
databaseUrl: string;
|
|
125
|
+
env?: Readonly<NodeJS.ProcessEnv>;
|
|
126
|
+
}>,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const resolverEnv = { ...process.env, ...(args.env ?? {}) };
|
|
129
|
+
const prismaConfigPath = this.resolveAbsolutePrismaConfigPath(resolverEnv);
|
|
130
|
+
await new Promise<void>((resolve, reject) => {
|
|
131
|
+
const command = spawn(
|
|
132
|
+
process.execPath,
|
|
133
|
+
[...[this.resolvePrismaCliPath(resolverEnv), ...args.prismaArgs], "--config", path.basename(prismaConfigPath)],
|
|
134
|
+
{
|
|
135
|
+
cwd: path.dirname(prismaConfigPath),
|
|
136
|
+
env: this.createProcessEnvironment(args.databaseUrl, args.provider, args.env),
|
|
137
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
let stdout = "";
|
|
141
|
+
let stderr = "";
|
|
142
|
+
command.stdout.on("data", (chunk: Buffer | string) => {
|
|
143
|
+
stdout += chunk.toString();
|
|
144
|
+
});
|
|
145
|
+
command.stderr.on("data", (chunk: Buffer | string) => {
|
|
146
|
+
stderr += chunk.toString();
|
|
147
|
+
});
|
|
148
|
+
command.once("error", (error) => {
|
|
149
|
+
reject(error);
|
|
150
|
+
});
|
|
151
|
+
command.once("close", (exitCode) => {
|
|
152
|
+
if (exitCode === 0) {
|
|
153
|
+
resolve();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
reject(this.createDeployError(exitCode, stdout, stderr));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async tryRecoverPartiallyAppliedNormalizedRuntimeMigration(
|
|
162
|
+
args: Readonly<{
|
|
163
|
+
databaseFilePath: string;
|
|
164
|
+
databaseUrl: string;
|
|
165
|
+
env?: Readonly<NodeJS.ProcessEnv>;
|
|
166
|
+
error: unknown;
|
|
167
|
+
}>,
|
|
168
|
+
): Promise<boolean> {
|
|
169
|
+
if (!this.isRecoverableNormalizedRuntimeMigrationError(args.error)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const repaired = await this.repairPartiallyAppliedNormalizedRuntimeSqliteDatabase(args.databaseFilePath);
|
|
173
|
+
if (!repaired) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
await this.resolveAppliedMigration({
|
|
177
|
+
provider: "sqlite",
|
|
178
|
+
databaseUrl: args.databaseUrl,
|
|
179
|
+
migrationName: PrismaMigrationOperations.normalizedRuntimeMigrationName,
|
|
180
|
+
env: args.env,
|
|
181
|
+
});
|
|
182
|
+
await this.deployWithProvider({
|
|
183
|
+
provider: "sqlite",
|
|
184
|
+
databaseUrl: args.databaseUrl,
|
|
185
|
+
env: args.env,
|
|
186
|
+
});
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private isRecoverableNormalizedRuntimeMigrationError(error: unknown): boolean {
|
|
191
|
+
if (!(error instanceof Error)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
return (
|
|
195
|
+
error.message.includes("Error: P3009") &&
|
|
196
|
+
error.message.includes(PrismaMigrationOperations.normalizedRuntimeMigrationName)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async repairPartiallyAppliedNormalizedRuntimeSqliteDatabase(databaseFilePath: string): Promise<boolean> {
|
|
201
|
+
// Lazy import: @libsql/client pulls in platform-specific native bindings that confuse the
|
|
202
|
+
// Next.js / Turbopack module tracer (forcing the whole project to be traced via NFT). This
|
|
203
|
+
// recovery path is rarely needed, so defer the load until it's actually invoked.
|
|
204
|
+
const { createClient } = await import("@libsql/client");
|
|
205
|
+
const client = createClient({ url: this.sqliteFilePathToDatabaseUrl(databaseFilePath) });
|
|
206
|
+
try {
|
|
207
|
+
const failedMigration = await this.hasActiveFailedMigrationRecord(
|
|
208
|
+
client,
|
|
209
|
+
PrismaMigrationOperations.normalizedRuntimeMigrationName,
|
|
210
|
+
);
|
|
211
|
+
if (!failedMigration) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const runColumns = await this.readSqliteTableColumns(client, "Run");
|
|
215
|
+
const hasNormalizedRunShape =
|
|
216
|
+
runColumns.has("finished_at") &&
|
|
217
|
+
runColumns.has("revision") &&
|
|
218
|
+
runColumns.has("outputs_by_node_json") &&
|
|
219
|
+
!runColumns.has("state_json");
|
|
220
|
+
if (!hasNormalizedRunShape) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
await this.ensureNormalizedRuntimeRepairArtifacts(client);
|
|
224
|
+
return true;
|
|
225
|
+
} finally {
|
|
226
|
+
client.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async hasActiveFailedMigrationRecord(client: Client, migrationName: string): Promise<boolean> {
|
|
231
|
+
const result = await client.execute({
|
|
232
|
+
sql: [
|
|
233
|
+
'SELECT 1 AS "has_failed"',
|
|
234
|
+
'FROM "_prisma_migrations"',
|
|
235
|
+
'WHERE "migration_name" = ?',
|
|
236
|
+
' AND "finished_at" IS NULL',
|
|
237
|
+
' AND "rolled_back_at" IS NULL',
|
|
238
|
+
"LIMIT 1",
|
|
239
|
+
].join(" "),
|
|
240
|
+
args: [migrationName],
|
|
241
|
+
});
|
|
242
|
+
return result.rows.length > 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private async readSqliteTableColumns(client: Client, tableName: string): Promise<Set<string>> {
|
|
246
|
+
const result = await client.execute(`PRAGMA table_info("${tableName}")`);
|
|
247
|
+
return new Set(result.rows.map((row) => String(row.name)));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async ensureNormalizedRuntimeRepairArtifacts(client: Client): Promise<void> {
|
|
251
|
+
await client.execute(`
|
|
252
|
+
CREATE TABLE IF NOT EXISTS "RunWorkItem" (
|
|
253
|
+
"work_item_id" TEXT NOT NULL PRIMARY KEY,
|
|
254
|
+
"run_id" TEXT NOT NULL,
|
|
255
|
+
"workflow_id" TEXT NOT NULL,
|
|
256
|
+
"status" TEXT NOT NULL,
|
|
257
|
+
"target_node_id" TEXT NOT NULL,
|
|
258
|
+
"batch_id" TEXT NOT NULL,
|
|
259
|
+
"queue_name" TEXT,
|
|
260
|
+
"claim_token" TEXT,
|
|
261
|
+
"claimed_by" TEXT,
|
|
262
|
+
"claimed_at" TEXT,
|
|
263
|
+
"available_at" TEXT NOT NULL,
|
|
264
|
+
"enqueued_at" TEXT NOT NULL,
|
|
265
|
+
"completed_at" TEXT,
|
|
266
|
+
"failed_at" TEXT,
|
|
267
|
+
"source_instance_id" TEXT,
|
|
268
|
+
"parent_instance_id" TEXT,
|
|
269
|
+
"items_in" INTEGER NOT NULL,
|
|
270
|
+
"inputs_by_port_json" TEXT NOT NULL,
|
|
271
|
+
"error_json" TEXT,
|
|
272
|
+
CONSTRAINT "RunWorkItem_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "Run"("run_id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
273
|
+
)
|
|
274
|
+
`);
|
|
275
|
+
await client.execute(`
|
|
276
|
+
CREATE INDEX IF NOT EXISTS "RunWorkItem_run_id_status_available_at_idx"
|
|
277
|
+
ON "RunWorkItem"("run_id", "status", "available_at")
|
|
278
|
+
`);
|
|
279
|
+
await client.execute(`
|
|
280
|
+
CREATE INDEX IF NOT EXISTS "RunWorkItem_run_id_target_node_id_batch_id_idx"
|
|
281
|
+
ON "RunWorkItem"("run_id", "target_node_id", "batch_id")
|
|
282
|
+
`);
|
|
283
|
+
await client.execute(`
|
|
284
|
+
CREATE TABLE IF NOT EXISTS "RunSlotProjection" (
|
|
285
|
+
"run_id" TEXT NOT NULL PRIMARY KEY,
|
|
286
|
+
"workflow_id" TEXT NOT NULL,
|
|
287
|
+
"revision" INTEGER NOT NULL,
|
|
288
|
+
"updated_at" TEXT NOT NULL,
|
|
289
|
+
"slot_states_json" TEXT NOT NULL,
|
|
290
|
+
CONSTRAINT "RunSlotProjection_run_id_fkey" FOREIGN KEY ("run_id") REFERENCES "Run"("run_id") ON DELETE CASCADE ON UPDATE CASCADE
|
|
291
|
+
)
|
|
292
|
+
`);
|
|
293
|
+
await client.execute(`
|
|
294
|
+
CREATE INDEX IF NOT EXISTS "RunSlotProjection_workflow_id_updated_at_idx"
|
|
295
|
+
ON "RunSlotProjection"("workflow_id", "updated_at")
|
|
296
|
+
`);
|
|
297
|
+
await client.execute(`
|
|
298
|
+
INSERT OR IGNORE INTO "RunSlotProjection" (
|
|
299
|
+
"run_id",
|
|
300
|
+
"workflow_id",
|
|
301
|
+
"revision",
|
|
302
|
+
"updated_at",
|
|
303
|
+
"slot_states_json"
|
|
304
|
+
)
|
|
305
|
+
SELECT
|
|
306
|
+
"run_id",
|
|
307
|
+
"workflow_id",
|
|
308
|
+
"revision",
|
|
309
|
+
"updated_at",
|
|
310
|
+
json_object('slotStatesByNodeId', json('{}'))
|
|
311
|
+
FROM "Run"
|
|
312
|
+
`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private async cleanupNormalizedRuntimeLegacyArtifacts(databaseFilePath: string): Promise<void> {
|
|
316
|
+
const { createClient } = await import("@libsql/client");
|
|
317
|
+
const client = createClient({ url: this.sqliteFilePathToDatabaseUrl(databaseFilePath) });
|
|
318
|
+
try {
|
|
319
|
+
const runColumns = await this.readSqliteTableColumns(client, "Run");
|
|
320
|
+
const hasNormalizedRunShape =
|
|
321
|
+
runColumns.has("finished_at") &&
|
|
322
|
+
runColumns.has("revision") &&
|
|
323
|
+
runColumns.has("outputs_by_node_json") &&
|
|
324
|
+
!runColumns.has("state_json");
|
|
325
|
+
if (!hasNormalizedRunShape) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const runSlotProjectionColumns = await this.readSqliteTableColumns(client, "RunSlotProjection");
|
|
329
|
+
await client.execute('DROP TABLE IF EXISTS "Run_legacy"');
|
|
330
|
+
if (runSlotProjectionColumns.size > 0) {
|
|
331
|
+
await client.execute('DROP TABLE IF EXISTS "RunProjection"');
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
client.close();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private sqliteFilePathToDatabaseUrl(databaseFilePath: string): string {
|
|
339
|
+
return `file:${path.resolve(databaseFilePath)}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private createProcessEnvironment(
|
|
343
|
+
databaseUrl: string,
|
|
344
|
+
provider: "postgresql" | "sqlite",
|
|
345
|
+
env?: Readonly<NodeJS.ProcessEnv>,
|
|
346
|
+
): NodeJS.ProcessEnv {
|
|
347
|
+
return {
|
|
348
|
+
...process.env,
|
|
349
|
+
...(env ?? {}),
|
|
350
|
+
DATABASE_URL: databaseUrl,
|
|
351
|
+
CODEMATION_PRISMA_PROVIDER: provider,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private resolvePrismaCliPath(env: Readonly<NodeJS.ProcessEnv>): string {
|
|
356
|
+
const configuredPath = env.CODEMATION_PRISMA_CLI_PATH;
|
|
357
|
+
if (configuredPath && existsSync(configuredPath)) {
|
|
358
|
+
return configuredPath;
|
|
359
|
+
}
|
|
360
|
+
const packageRoot = this.resolvePackageRoot(env);
|
|
361
|
+
const packageManagerCandidates = [
|
|
362
|
+
path.resolve(process.cwd(), "node_modules", "prisma", "build", "index.js"),
|
|
363
|
+
path.resolve(packageRoot, "node_modules", "prisma", "build", "index.js"),
|
|
364
|
+
];
|
|
365
|
+
for (const candidate of packageManagerCandidates) {
|
|
366
|
+
if (existsSync(candidate)) {
|
|
367
|
+
return candidate;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
return this.require.resolve("prisma/build/index.js", {
|
|
372
|
+
paths: [process.cwd(), packageRoot],
|
|
373
|
+
});
|
|
374
|
+
} catch {
|
|
375
|
+
throw new Error(
|
|
376
|
+
"Unable to resolve the Prisma CLI required for startup migrations. Ensure `prisma` is installed.",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private resolveAbsolutePrismaConfigPath(env: Readonly<NodeJS.ProcessEnv>): string {
|
|
382
|
+
const configuredPath = env.CODEMATION_PRISMA_CONFIG_PATH;
|
|
383
|
+
const packageRoot = this.resolvePackageRoot(env);
|
|
384
|
+
if (configuredPath) {
|
|
385
|
+
return path.isAbsolute(configuredPath) ? configuredPath : path.resolve(packageRoot, configuredPath);
|
|
386
|
+
}
|
|
387
|
+
return path.resolve(packageRoot, "prisma.config.ts");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async ensureSqliteParentDirectoryExists(databaseFilePath: string): Promise<void> {
|
|
391
|
+
await mkdir(path.dirname(databaseFilePath), { recursive: true });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private createDeployError(exitCode: number | null, stdout: string, stderr: string): Error {
|
|
395
|
+
const output = stderr.trim() || stdout.trim();
|
|
396
|
+
if (!output) {
|
|
397
|
+
return new Error(`Prisma migrate deploy failed during startup with exit code ${exitCode ?? "unknown"}.`);
|
|
398
|
+
}
|
|
399
|
+
return new Error(`Prisma migrate deploy failed during startup with exit code ${exitCode ?? "unknown"}.\n${output}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -173,6 +173,7 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
173
173
|
control: this.parseJson(row.controlJson),
|
|
174
174
|
workflowSnapshot: this.parseJson(row.workflowSnapshotJson),
|
|
175
175
|
mutableState: this.parseJson(row.mutableStateJson),
|
|
176
|
+
...this.loadHitlState(row.hitlStateJson),
|
|
176
177
|
policySnapshot: this.parseJson(row.policySnapshotJson),
|
|
177
178
|
engineCounters: this.parseJson(row.engineCountersJson),
|
|
178
179
|
status: row.status as PersistedRunState["status"],
|
|
@@ -416,6 +417,7 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
416
417
|
policySnapshotJson: state.policySnapshot ? JSON.stringify(state.policySnapshot) : null,
|
|
417
418
|
engineCountersJson: state.engineCounters ? JSON.stringify(state.engineCounters) : null,
|
|
418
419
|
mutableStateJson: state.mutableState ? JSON.stringify(state.mutableState) : null,
|
|
420
|
+
hitlStateJson: this.buildHitlStateJson(state),
|
|
419
421
|
outputsByNodeJson: JSON.stringify(this.buildPersistedOutputsByNode(state)),
|
|
420
422
|
},
|
|
421
423
|
});
|
|
@@ -596,6 +598,43 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
596
598
|
};
|
|
597
599
|
}
|
|
598
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Serialize HITL state fields into the dedicated `hitl_state_json` column.
|
|
603
|
+
* Returns null when none of the HITL fields are populated.
|
|
604
|
+
*/
|
|
605
|
+
private buildHitlStateJson(state: PersistedRunState): string | null {
|
|
606
|
+
const hasHitl =
|
|
607
|
+
(state.suspension && state.suspension.length > 0) ||
|
|
608
|
+
state.pendingResume !== undefined ||
|
|
609
|
+
state.reason !== undefined;
|
|
610
|
+
if (!hasHitl) return null;
|
|
611
|
+
return JSON.stringify({
|
|
612
|
+
...(state.suspension && state.suspension.length > 0 ? { suspension: state.suspension } : {}),
|
|
613
|
+
...(state.pendingResume !== undefined ? { pendingResume: state.pendingResume } : {}),
|
|
614
|
+
...(state.reason !== undefined ? { reason: state.reason } : {}),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Load HITL state (suspension array, pendingResume context, halt reason) from the
|
|
620
|
+
* dedicated `hitl_state_json` column.
|
|
621
|
+
*/
|
|
622
|
+
private loadHitlState(
|
|
623
|
+
hitlStateJson: string | null,
|
|
624
|
+
): Pick<PersistedRunState, "suspension" | "pendingResume" | "reason"> {
|
|
625
|
+
const parsed = this.parseJson<{
|
|
626
|
+
suspension?: PersistedRunState["suspension"];
|
|
627
|
+
pendingResume?: PersistedRunState["pendingResume"];
|
|
628
|
+
reason?: PersistedRunState["reason"];
|
|
629
|
+
}>(hitlStateJson);
|
|
630
|
+
if (!parsed) return {};
|
|
631
|
+
return {
|
|
632
|
+
suspension: parsed.suspension,
|
|
633
|
+
pendingResume: parsed.pendingResume,
|
|
634
|
+
reason: parsed.reason,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
599
638
|
private buildWorkItems(state: PersistedRunState, nowIso: string): Prisma.RunWorkItemCreateManyInput[] {
|
|
600
639
|
const rows: Prisma.RunWorkItemCreateManyInput[] = [];
|
|
601
640
|
for (const [index, entry] of (state.queue ?? []).entries()) {
|
|
@@ -102,7 +102,11 @@ export class AgentMcpIntegrationImpl implements AgentMcpIntegration {
|
|
|
102
102
|
* Looks up the credential binding for the MCP connection node and verifies the
|
|
103
103
|
* referenced credential instance still exists.
|
|
104
104
|
*/
|
|
105
|
-
private async resolveCredentialInstanceId(
|
|
105
|
+
private async resolveCredentialInstanceId(
|
|
106
|
+
workflowId: string,
|
|
107
|
+
agentNodeId: string,
|
|
108
|
+
serverId: string,
|
|
109
|
+
): Promise<string> {
|
|
106
110
|
const mcpNodeId = ConnectionNodeIdFactory.mcpConnectionNodeId(agentNodeId, serverId);
|
|
107
111
|
const binding = await this.credentialStore.getBinding({ workflowId, nodeId: mcpNodeId, slotKey: "credential" });
|
|
108
112
|
if (!binding) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seam for durable HMAC replay-protection nonce storage (T6 security fix).
|
|
3
|
+
*
|
|
4
|
+
* The default in-process store (InMemoryHmacNonceStore) clears on restart, allowing
|
|
5
|
+
* replay within the timestamp window. PrismaHmacNonceStore provides durability.
|
|
6
|
+
*/
|
|
7
|
+
export interface HmacNonceStore {
|
|
8
|
+
/**
|
|
9
|
+
* Atomically record a nonce if it has not been seen before.
|
|
10
|
+
* Returns `true` if the nonce was new (request should proceed),
|
|
11
|
+
* `false` if the nonce was already present (replay — reject).
|
|
12
|
+
*/
|
|
13
|
+
recordIfNew(nonce: string, expiresAt: Date): Promise<boolean>;
|
|
14
|
+
}
|
|
@@ -9,9 +9,18 @@ export interface SignedHeaders {
|
|
|
9
9
|
|
|
10
10
|
@injectable()
|
|
11
11
|
export class HmacRequestSigner {
|
|
12
|
-
constructor(@inject(PairingConfigToken) private readonly config: PairingConfig) {}
|
|
12
|
+
constructor(@inject(PairingConfigToken, { isOptional: true }) private readonly config: PairingConfig | null = null) {}
|
|
13
13
|
|
|
14
14
|
sign(method: string, urlOrPath: string, body: string): SignedHeaders {
|
|
15
|
+
if (this.config === null) {
|
|
16
|
+
// Should never happen in managed mode (PairingConfig is registered then). In non-managed
|
|
17
|
+
// mode this signer should never be called — callers like `PairedFetch` are themselves
|
|
18
|
+
// only reachable via `ControlPlaneCatalogFetcher` whose poll loop checks `pairingConfig`.
|
|
19
|
+
// If we land here, a CP-bound call escaped that guard.
|
|
20
|
+
throw new Error(
|
|
21
|
+
"HmacRequestSigner.sign called without a registered PairingConfig — workspace is not in managed mode.",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
15
24
|
const ts = Math.floor(Date.now() / 1000);
|
|
16
25
|
const nonce = randomBytes(16).toString("base64");
|
|
17
26
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { injectable } from "@codemation/core";
|
|
2
|
+
import type { HmacNonceStore } from "./HmacNonceStore";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* In-memory HMAC nonce store for unit tests and non-managed mode.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: Nonces are lost on process restart; this is intentional for non-managed
|
|
8
|
+
* mode where replay risk is low. Use PrismaHmacNonceStore in managed mode.
|
|
9
|
+
*/
|
|
10
|
+
@injectable()
|
|
11
|
+
export class InMemoryHmacNonceStore implements HmacNonceStore {
|
|
12
|
+
private readonly store = new Map<string, number>();
|
|
13
|
+
|
|
14
|
+
async recordIfNew(nonce: string, expiresAt: Date): Promise<boolean> {
|
|
15
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
16
|
+
// Prune expired entries on each call (mirrors original in-process behaviour)
|
|
17
|
+
for (const [key, expirySec] of this.store.entries()) {
|
|
18
|
+
if (expirySec <= nowSec) this.store.delete(key);
|
|
19
|
+
}
|
|
20
|
+
if (this.store.has(nonce)) return false;
|
|
21
|
+
this.store.set(nonce, Math.floor(expiresAt.getTime() / 1000));
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -2,19 +2,41 @@ import { createHmac, createHash, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { inject, injectable } from "@codemation/core";
|
|
3
3
|
import type { PairingConfig, PairingVerificationResult } from "./pairing.types";
|
|
4
4
|
import { PairingConfigToken } from "./PairingConfigToken";
|
|
5
|
+
import type { HmacNonceStore } from "./HmacNonceStore";
|
|
6
|
+
import { HmacNonceStoreToken } from "./HmacNonceStoreToken";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Verifies incoming HMAC-signed requests from the control plane.
|
|
8
10
|
* Mirrors the control-plane HmacVerifier — both sides follow docs/pairing-protocol.md.
|
|
11
|
+
*
|
|
12
|
+
* Security (T6): The nonce store is injected and defaults to PrismaHmacNonceStore in
|
|
13
|
+
* managed mode so replay protection survives process restarts within the 300-second
|
|
14
|
+
* timestamp window.
|
|
9
15
|
*/
|
|
10
16
|
@injectable()
|
|
11
17
|
export class IncomingHmacVerifier {
|
|
12
|
-
private readonly usedNonces = new Map<string, number>();
|
|
13
18
|
private readonly nonceTtlSeconds = 600; // 10 minutes
|
|
14
19
|
|
|
15
|
-
constructor(
|
|
20
|
+
constructor(
|
|
21
|
+
@inject(PairingConfigToken, { isOptional: true }) private readonly config: PairingConfig | null = null,
|
|
22
|
+
@inject(HmacNonceStoreToken) private readonly nonceStore: HmacNonceStore,
|
|
23
|
+
) {}
|
|
16
24
|
|
|
17
|
-
verify(
|
|
25
|
+
async verify(
|
|
26
|
+
method: string,
|
|
27
|
+
url: string,
|
|
28
|
+
body: string,
|
|
29
|
+
authHeader: string | null,
|
|
30
|
+
): Promise<PairingVerificationResult> {
|
|
31
|
+
if (this.config === null) {
|
|
32
|
+
// Same shape as HmacRequestSigner — verifier is reachable via the DI graph even in
|
|
33
|
+
// non-managed mode (lazy CodemationHonoApiApp construction pulls every registered handler).
|
|
34
|
+
// We accept construction without PairingConfig and throw only when verify() is actually
|
|
35
|
+
// called, which shouldn't happen in non-managed mode (internal HMAC routes aren't mounted).
|
|
36
|
+
throw new Error(
|
|
37
|
+
"IncomingHmacVerifier.verify called without a registered PairingConfig — workspace is not in managed mode.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
18
40
|
if (!this.config.pairingSecret || this.config.pairingSecret.trim().length === 0) {
|
|
19
41
|
throw new Error("IncomingHmacVerifier: pairingSecret is not configured — cannot verify HMAC requests.");
|
|
20
42
|
}
|
|
@@ -47,10 +69,10 @@ export class IncomingHmacVerifier {
|
|
|
47
69
|
return { failure: "signature" };
|
|
48
70
|
}
|
|
49
71
|
|
|
50
|
-
this.pruneExpiredNonces(nowSec);
|
|
51
72
|
const nonceKey = `${parts.workspaceId}:${parts.nonce}`;
|
|
52
|
-
|
|
53
|
-
this.
|
|
73
|
+
const nonceExpiresAt = new Date((nowSec + this.nonceTtlSeconds) * 1000);
|
|
74
|
+
const isNew = await this.nonceStore.recordIfNew(nonceKey, nonceExpiresAt);
|
|
75
|
+
if (!isNew) return { failure: "replay" };
|
|
54
76
|
|
|
55
77
|
return { workspaceId: parts.workspaceId };
|
|
56
78
|
}
|
|
@@ -73,10 +95,4 @@ export class IncomingHmacVerifier {
|
|
|
73
95
|
if (!v || !workspaceId || !ts || !nonce || !sig) return null;
|
|
74
96
|
return { v, workspaceId, ts: Number(ts), nonce, sig };
|
|
75
97
|
}
|
|
76
|
-
|
|
77
|
-
private pruneExpiredNonces(nowSec: number): void {
|
|
78
|
-
for (const [key, expiry] of this.usedNonces.entries()) {
|
|
79
|
-
if (expiry <= nowSec) this.usedNonces.delete(key);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
98
|
}
|
|
@@ -17,7 +17,7 @@ export class InternalHmacAuthMiddleware {
|
|
|
17
17
|
return async (c: Context, next: Next) => {
|
|
18
18
|
const body = c.req.method === "GET" || c.req.method === "HEAD" ? "" : await c.req.text();
|
|
19
19
|
|
|
20
|
-
const result = this.verifier.verify(c.req.method, c.req.url, body, c.req.header("authorization") ?? null);
|
|
20
|
+
const result = await this.verifier.verify(c.req.method, c.req.url, body, c.req.header("authorization") ?? null);
|
|
21
21
|
|
|
22
22
|
if ("failure" in result) {
|
|
23
23
|
return c.json({ error: "Unauthorized" }, 401);
|
package/src/pairing/index.ts
CHANGED
|
@@ -6,6 +6,9 @@ export { InternalHmacAuthMiddleware } from "./InternalHmacAuthMiddleware";
|
|
|
6
6
|
export { InternalPingRegistrar } from "./InternalPingRegistrar";
|
|
7
7
|
export { PairingConfigFactory } from "./PairingConfigFactory";
|
|
8
8
|
export { PairingConfigToken } from "./PairingConfigToken";
|
|
9
|
+
export type { HmacNonceStore } from "./HmacNonceStore";
|
|
10
|
+
export { HmacNonceStoreToken } from "./HmacNonceStoreToken";
|
|
11
|
+
export { InMemoryHmacNonceStore } from "./InMemoryHmacNonceStore";
|
|
9
12
|
export type {
|
|
10
13
|
PairingConfig,
|
|
11
14
|
PairingVerificationResult,
|
|
@@ -132,6 +132,10 @@ export class ApiPaths {
|
|
|
132
132
|
return `${this.credentialsBasePath}/instances`;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
static credentialApps(): string {
|
|
136
|
+
return `${this.credentialsBasePath}/apps`;
|
|
137
|
+
}
|
|
138
|
+
|
|
135
139
|
static credentialInstance(instanceId: string, withSecrets?: boolean): string {
|
|
136
140
|
const base = `${this.credentialInstances()}/${encodeURIComponent(instanceId)}`;
|
|
137
141
|
return withSecrets ? `${base}?withSecrets=1` : base;
|
|
@@ -279,4 +283,14 @@ export class ApiPaths {
|
|
|
279
283
|
static internalAuthBootstrap(): string {
|
|
280
284
|
return `${this.bootstrapBasePath}/auth/internal`;
|
|
281
285
|
}
|
|
286
|
+
|
|
287
|
+
/** Token-authenticated: resume a suspended HITL task. */
|
|
288
|
+
static hitlTaskResume(taskId: string): string {
|
|
289
|
+
return `${this.apiBasePath}/hitl/tasks/${encodeURIComponent(taskId)}/resume`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Session-authenticated: record a decision on a suspended HITL task. */
|
|
293
|
+
static hitlTaskDecide(taskId: string): string {
|
|
294
|
+
return `${this.apiBasePath}/hitl/tasks/${encodeURIComponent(taskId)}/decide`;
|
|
295
|
+
}
|
|
282
296
|
}
|
|
@@ -35,6 +35,10 @@ export class HonoHttpAnonymousRoutePolicy {
|
|
|
35
35
|
if (pathname === ApiPaths.whitelabelLogo()) {
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
+
// HITL token-authenticated resume endpoint — token is the auth, no session required
|
|
39
|
+
if (/^\/api\/hitl\/tasks\/[^/]+\/resume$/.test(pathname)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
38
42
|
return false;
|
|
39
43
|
}
|
|
40
44
|
}
|
|
@@ -8,6 +8,7 @@ export class CredentialHonoApiRouteRegistrar implements HonoApiRouteRegistrar {
|
|
|
8
8
|
constructor(@inject(CredentialHttpRouteHandler) private readonly handler: CredentialHttpRouteHandler) {}
|
|
9
9
|
|
|
10
10
|
register(app: Hono): void {
|
|
11
|
+
app.get("/credentials/apps", (_c) => this.handler.getCredentialApps());
|
|
11
12
|
app.get("/credentials/types", (_c) => this.handler.getCredentialTypes());
|
|
12
13
|
app.get("/credentials/env-status", (_c) => this.handler.getCredentialFieldEnvStatus());
|
|
13
14
|
app.get("/credentials/instances", (_c) => this.handler.getCredentialInstances());
|