@zintrust/core 0.1.41 → 0.1.42
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/package.json +17 -1
- package/src/boot/bootstrap.js +27 -11
- package/src/boot/registry/runtime.d.ts.map +1 -1
- package/src/boot/registry/runtime.js +11 -0
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +12 -0
- package/src/cli/commands/ConfigCommand.d.ts.map +1 -1
- package/src/cli/commands/ConfigCommand.js +3 -5
- package/src/cli/commands/D1LearnCommand.d.ts +9 -0
- package/src/cli/commands/D1LearnCommand.d.ts.map +1 -0
- package/src/cli/commands/D1LearnCommand.js +143 -0
- package/src/cli/commands/D1MigrateCommand.d.ts.map +1 -1
- package/src/cli/commands/D1MigrateCommand.js +55 -16
- package/src/cli/commands/InitContainerCommand.d.ts.map +1 -1
- package/src/cli/commands/InitContainerCommand.js +21 -6
- package/src/cli/commands/InitEcosystemCommand.d.ts +6 -0
- package/src/cli/commands/InitEcosystemCommand.d.ts.map +1 -0
- package/src/cli/commands/InitEcosystemCommand.js +51 -0
- package/src/cli/commands/MigrateCommand.d.ts.map +1 -1
- package/src/cli/commands/MigrateCommand.js +78 -36
- package/src/cli/commands/MigrateWorkerCommand.d.ts.map +1 -1
- package/src/cli/commands/MigrateWorkerCommand.js +36 -2
- package/src/cli/commands/PutCommand.d.ts +6 -0
- package/src/cli/commands/PutCommand.d.ts.map +1 -0
- package/src/cli/commands/PutCommand.js +173 -0
- package/src/cli/commands/QueueRecoveryCommand.d.ts.map +1 -1
- package/src/cli/commands/QueueRecoveryCommand.js +113 -14
- package/src/cli/commands/ScheduleListCommand.d.ts +6 -0
- package/src/cli/commands/ScheduleListCommand.d.ts.map +1 -0
- package/src/cli/commands/ScheduleListCommand.js +62 -0
- package/src/cli/commands/ScheduleRunCommand.d.ts +6 -0
- package/src/cli/commands/ScheduleRunCommand.d.ts.map +1 -0
- package/src/cli/commands/ScheduleRunCommand.js +32 -0
- package/src/cli/commands/ScheduleStartCommand.d.ts +6 -0
- package/src/cli/commands/ScheduleStartCommand.d.ts.map +1 -0
- package/src/cli/commands/ScheduleStartCommand.js +40 -0
- package/src/cli/commands/SecretsCommand.d.ts.map +1 -1
- package/src/cli/commands/SecretsCommand.js +2 -2
- package/src/cli/commands/schedule/ScheduleCliSupport.d.ts +6 -0
- package/src/cli/commands/schedule/ScheduleCliSupport.d.ts.map +1 -0
- package/src/cli/commands/schedule/ScheduleCliSupport.js +55 -0
- package/src/cli/config/ConfigManager.d.ts.map +1 -1
- package/src/cli/config/ConfigManager.js +8 -1
- package/src/cli/d1/D1SqlMigrations.d.ts.map +1 -1
- package/src/cli/d1/D1SqlMigrations.js +11 -1
- package/src/cli/d1/WranglerConfig.d.ts.map +1 -1
- package/src/cli/d1/WranglerConfig.js +34 -2
- package/src/cli/services/VersionChecker.d.ts.map +1 -1
- package/src/cli/services/VersionChecker.js +5 -1
- package/src/cli/utils/DatabaseCliUtils.d.ts.map +1 -1
- package/src/cli/utils/DatabaseCliUtils.js +6 -1
- package/src/cli/utils/EnvFileLoader.d.ts.map +1 -1
- package/src/cli/utils/EnvFileLoader.js +33 -14
- package/src/cli.d.ts +5 -0
- package/src/cli.d.ts.map +1 -0
- package/src/cli.js +4 -0
- package/src/collections/index.d.ts +2 -2
- package/src/collections/index.d.ts.map +1 -1
- package/src/collections/index.js +1 -1
- package/src/common/RemoteSignedJson.d.ts.map +1 -1
- package/src/common/RemoteSignedJson.js +49 -23
- package/src/common/utility.d.ts.map +1 -1
- package/src/common/utility.js +2 -6
- package/src/config/cloudflare.d.ts.map +1 -1
- package/src/config/cloudflare.js +19 -8
- package/src/config/env.js +2 -2
- package/src/helper/index.d.ts +225 -0
- package/src/helper/index.d.ts.map +1 -0
- package/src/helper/index.js +347 -0
- package/src/index.d.ts +3 -6
- package/src/index.d.ts.map +1 -1
- package/src/index.js +7 -9
- package/src/migrations/MigrationDiscovery.d.ts.map +1 -1
- package/src/migrations/MigrationDiscovery.js +2 -1
- package/src/orm/DatabaseAdapter.d.ts +1 -0
- package/src/orm/DatabaseAdapter.d.ts.map +1 -1
- package/src/orm/SchemaStatemenWriter.d.ts +15 -0
- package/src/orm/SchemaStatemenWriter.d.ts.map +1 -0
- package/src/orm/SchemaStatemenWriter.js +78 -0
- package/src/orm/adapters/D1Adapter.d.ts.map +1 -1
- package/src/orm/adapters/D1Adapter.js +52 -2
- package/src/orm/adapters/D1RemoteAdapter.d.ts.map +1 -1
- package/src/orm/adapters/D1RemoteAdapter.js +137 -89
- package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/MySQLProxyAdapter.js +100 -81
- package/src/orm/adapters/PostgreSQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/PostgreSQLProxyAdapter.js +26 -10
- package/src/orm/adapters/SqlProxyAdapterUtils.d.ts.map +1 -1
- package/src/orm/adapters/SqlProxyAdapterUtils.js +2 -1
- package/src/orm/adapters/SqlProxyRegistryMode.d.ts +12 -0
- package/src/orm/adapters/SqlProxyRegistryMode.d.ts.map +1 -0
- package/src/orm/adapters/SqlProxyRegistryMode.js +24 -0
- package/src/orm/adapters/SqlServerProxyAdapter.d.ts +3 -0
- package/src/orm/adapters/SqlServerProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/SqlServerProxyAdapter.js +125 -117
- package/src/orm/migrations/MigrationStore.js +1 -1
- package/src/proxy/ProxyRequestParsing.d.ts +9 -0
- package/src/proxy/ProxyRequestParsing.d.ts.map +1 -0
- package/src/proxy/ProxyRequestParsing.js +16 -0
- package/src/proxy/RequestValidator.d.ts.map +1 -1
- package/src/proxy/RequestValidator.js +2 -1
- package/src/proxy/SigningService.js +2 -2
- package/src/proxy/SqlProxyDbOverrides.d.ts +17 -0
- package/src/proxy/SqlProxyDbOverrides.d.ts.map +1 -0
- package/src/proxy/SqlProxyDbOverrides.js +1 -0
- package/src/proxy/SqlProxyServerDeps.d.ts +12 -0
- package/src/proxy/SqlProxyServerDeps.d.ts.map +1 -0
- package/src/proxy/SqlProxyServerDeps.js +9 -0
- package/src/proxy/StatementPayloadValidator.d.ts +13 -0
- package/src/proxy/StatementPayloadValidator.d.ts.map +1 -0
- package/src/proxy/StatementPayloadValidator.js +18 -0
- package/src/proxy/StatementRegistryLoader.d.ts +2 -0
- package/src/proxy/StatementRegistryLoader.d.ts.map +1 -0
- package/src/proxy/StatementRegistryLoader.js +36 -0
- package/src/proxy/StatementRegistryResolver.d.ts +15 -0
- package/src/proxy/StatementRegistryResolver.d.ts.map +1 -0
- package/src/proxy/StatementRegistryResolver.js +34 -0
- package/src/proxy/d1/ZintrustD1Proxy.d.ts +2 -1
- package/src/proxy/d1/ZintrustD1Proxy.d.ts.map +1 -1
- package/src/proxy/d1/ZintrustD1Proxy.js +2 -1
- package/src/proxy/isMutatingSql.d.ts +2 -0
- package/src/proxy/isMutatingSql.d.ts.map +1 -0
- package/src/proxy/isMutatingSql.js +12 -0
- package/src/proxy/kv/ZintrustKvProxy.d.ts +2 -1
- package/src/proxy/kv/ZintrustKvProxy.d.ts.map +1 -1
- package/src/proxy/kv/ZintrustKvProxy.js +2 -1
- package/src/proxy/mysql/MySqlProxyServer.d.ts +2 -8
- package/src/proxy/mysql/MySqlProxyServer.d.ts.map +1 -1
- package/src/proxy/mysql/MySqlProxyServer.js +84 -51
- package/src/proxy/postgres/PostgresProxyServer.d.ts +2 -8
- package/src/proxy/postgres/PostgresProxyServer.d.ts.map +1 -1
- package/src/proxy/postgres/PostgresProxyServer.js +86 -48
- package/src/proxy/smtp/SmtpProxyServer.d.ts.map +1 -1
- package/src/proxy/smtp/SmtpProxyServer.js +6 -5
- package/src/proxy/sqlserver/SqlServerProxyServer.d.ts +2 -8
- package/src/proxy/sqlserver/SqlServerProxyServer.d.ts.map +1 -1
- package/src/proxy/sqlserver/SqlServerProxyServer.js +84 -49
- package/src/proxy.d.ts +4 -0
- package/src/proxy.d.ts.map +1 -0
- package/src/proxy.js +3 -0
- package/src/scheduler/Schedule.d.ts +36 -0
- package/src/scheduler/Schedule.d.ts.map +1 -0
- package/src/scheduler/Schedule.js +197 -0
- package/src/scheduler/ScheduleHttpGateway.d.ts +8 -0
- package/src/scheduler/ScheduleHttpGateway.d.ts.map +1 -0
- package/src/scheduler/ScheduleHttpGateway.js +196 -0
- package/src/scheduler/ScheduleRunner.d.ts +6 -0
- package/src/scheduler/ScheduleRunner.d.ts.map +1 -1
- package/src/scheduler/ScheduleRunner.js +166 -29
- package/src/scheduler/SchedulerRuntime.d.ts +15 -0
- package/src/scheduler/SchedulerRuntime.d.ts.map +1 -0
- package/src/scheduler/SchedulerRuntime.js +79 -0
- package/src/scheduler/cron/Cron.d.ts +19 -0
- package/src/scheduler/cron/Cron.d.ts.map +1 -0
- package/src/scheduler/cron/Cron.js +200 -0
- package/src/scheduler/leader/SchedulerLeader.d.ts +14 -0
- package/src/scheduler/leader/SchedulerLeader.d.ts.map +1 -0
- package/src/scheduler/leader/SchedulerLeader.js +187 -0
- package/src/scheduler/state/ScheduleStateStore.d.ts +27 -0
- package/src/scheduler/state/ScheduleStateStore.d.ts.map +1 -0
- package/src/scheduler/state/ScheduleStateStore.js +27 -0
- package/src/scheduler/types.d.ts +10 -0
- package/src/scheduler/types.d.ts.map +1 -1
- package/src/schedules/index.d.ts +1 -0
- package/src/schedules/index.d.ts.map +1 -1
- package/src/schedules/index.js +1 -0
- package/src/schedules/job-tracking-cleanup.d.ts +4 -0
- package/src/schedules/job-tracking-cleanup.d.ts.map +1 -0
- package/src/schedules/job-tracking-cleanup.js +116 -0
- package/src/schedules/log-cleanup.d.ts +1 -2
- package/src/schedules/log-cleanup.d.ts.map +1 -1
- package/src/schedules/log-cleanup.js +12 -15
- package/src/security/Sanitizer.d.ts.map +1 -1
- package/src/security/Sanitizer.js +1 -9
- package/src/security/SignedRequest.d.ts.map +1 -1
- package/src/security/SignedRequest.js +2 -2
- package/src/templates/docker/docker-compose.ecosystem.yml.tpl +301 -0
- package/src/templates/docker/docker-compose.schedules.yml.tpl +84 -0
- package/src/templates/project/basic/app/Schedules/index.ts.tpl +0 -0
- package/src/templates/project/basic/config/database.ts.tpl +1 -1
- package/src/toolkit/Secrets/Manifest.d.ts.map +1 -1
- package/src/toolkit/Secrets/Manifest.js +5 -7
- package/src/tools/mail/drivers/Smtp.d.ts.map +1 -1
- package/src/tools/mail/drivers/Smtp.js +7 -1
- package/src/tools/queue/JobReconciliationRunner.d.ts.map +1 -1
- package/src/tools/queue/JobReconciliationRunner.js +7 -39
- package/src/tools/queue/JobRecoveryDaemon.d.ts.map +1 -1
- package/src/tools/queue/JobRecoveryDaemon.js +116 -18
- package/src/tools/queue/JobStateTracker.d.ts +10 -1
- package/src/tools/queue/JobStateTracker.d.ts.map +1 -1
- package/src/tools/queue/JobStateTracker.js +24 -2
- package/src/tools/queue/JobStateTrackerDbPersistence.d.ts.map +1 -1
- package/src/tools/queue/JobStateTrackerDbPersistence.js +93 -2
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Logger } from '../../config/logger.js';
|
|
3
|
-
import { ErrorHandler } from '../ErrorHandler.js';
|
|
4
|
-
import { createProxyServer } from '../ProxyServer.js';
|
|
5
|
-
import { resolveBaseConfig, resolveBaseSigningConfig, verifyRequestSignature, } from '../ProxyServerUtils.js';
|
|
6
|
-
import { RequestValidator } from '../RequestValidator.js';
|
|
7
|
-
import { validateSqlPayload } from '../SqlPayloadValidator.js';
|
|
1
|
+
import * as Deps from '../SqlProxyServerDeps.js';
|
|
8
2
|
const resolveDatabaseConfig = (overrides = {}) => {
|
|
9
|
-
const dbHost = overrides.dbHost ?? Env.get('DB_HOST_MSSQL', Env.get('DB_HOST', '127.0.0.1'));
|
|
10
|
-
const dbPort = overrides.dbPort ?? Env.getInt('DB_PORT_MSSQL', 1433);
|
|
11
|
-
const dbName = overrides.dbName ?? Env.get('DB_DATABASE_MSSQL', 'zintrust');
|
|
12
|
-
const dbUser = overrides.dbUser ?? Env.get('DB_USERNAME_MSSQL', 'sa');
|
|
13
|
-
const dbPass = overrides.dbPass ?? Env.get('DB_PASSWORD_MSSQL', '');
|
|
14
|
-
const connectionLimit = overrides.connectionLimit ?? Env.getInt('SQLSERVER_PROXY_POOL_LIMIT', 10);
|
|
3
|
+
const dbHost = overrides.dbHost ?? Deps.Env.get('DB_HOST_MSSQL', Deps.Env.get('DB_HOST', '127.0.0.1'));
|
|
4
|
+
const dbPort = overrides.dbPort ?? Deps.Env.getInt('DB_PORT_MSSQL', 1433);
|
|
5
|
+
const dbName = overrides.dbName ?? Deps.Env.get('DB_DATABASE_MSSQL', 'zintrust');
|
|
6
|
+
const dbUser = overrides.dbUser ?? Deps.Env.get('DB_USERNAME_MSSQL', 'sa');
|
|
7
|
+
const dbPass = overrides.dbPass ?? Deps.Env.get('DB_PASSWORD_MSSQL', '');
|
|
8
|
+
const connectionLimit = overrides.connectionLimit ?? Deps.Env.getInt('SQLSERVER_PROXY_POOL_LIMIT', 10);
|
|
15
9
|
return { dbHost, dbPort, dbName, dbUser, dbPass, connectionLimit };
|
|
16
10
|
};
|
|
17
11
|
const resolveConfig = (overrides = {}) => {
|
|
18
|
-
const proxyConfig = resolveBaseConfig(overrides, 'SQLSERVER');
|
|
12
|
+
const proxyConfig = Deps.resolveBaseConfig(overrides, 'SQLSERVER');
|
|
19
13
|
const dbConfig = resolveDatabaseConfig(overrides);
|
|
20
|
-
const signingConfig = resolveBaseSigningConfig(overrides, 'SQLSERVER');
|
|
14
|
+
const signingConfig = Deps.resolveBaseSigningConfig(overrides, 'SQLSERVER');
|
|
21
15
|
const poolConfig = {
|
|
22
16
|
server: dbConfig.dbHost,
|
|
23
17
|
port: dbConfig.dbPort,
|
|
@@ -45,10 +39,11 @@ const resolveConfig = (overrides = {}) => {
|
|
|
45
39
|
require: signingConfig.requireSigning,
|
|
46
40
|
windowMs: signingConfig.signingWindowMs,
|
|
47
41
|
},
|
|
42
|
+
statements: Deps.loadStatementRegistry('SQLSERVER'),
|
|
48
43
|
};
|
|
49
44
|
};
|
|
50
45
|
const validateQueryPayload = (payload) => {
|
|
51
|
-
const base = validateSqlPayload(payload);
|
|
46
|
+
const base = Deps.validateSqlPayload(payload);
|
|
52
47
|
if (!base.valid) {
|
|
53
48
|
return { valid: false, error: base.error };
|
|
54
49
|
}
|
|
@@ -95,42 +90,82 @@ const handleEndpoint = (path, result) => {
|
|
|
95
90
|
},
|
|
96
91
|
};
|
|
97
92
|
}
|
|
98
|
-
|
|
93
|
+
if (path === '/zin/sqlserver/statement') {
|
|
94
|
+
// Statement endpoint returns query-shaped response for non-mutating SQL.
|
|
95
|
+
return { status: 200, body: { rows, rowCount: rowsAffected[0] ?? 0 } };
|
|
96
|
+
}
|
|
97
|
+
return Deps.ErrorHandler.toProxyError(404, 'NOT_FOUND', 'Unknown endpoint');
|
|
98
|
+
};
|
|
99
|
+
const toMutatingStatementResponse = (result) => {
|
|
100
|
+
const rowsAffected = result['rowsAffected'];
|
|
101
|
+
return {
|
|
102
|
+
status: 200,
|
|
103
|
+
body: {
|
|
104
|
+
ok: true,
|
|
105
|
+
meta: { changes: rowsAffected[0] ?? 0 },
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const handleStatementRequest = async (pool, statements, requestPath, payload) => {
|
|
110
|
+
const resolved = Deps.resolveStatementOrError(statements, payload);
|
|
111
|
+
if (!resolved.ok)
|
|
112
|
+
return resolved.response;
|
|
113
|
+
try {
|
|
114
|
+
const result = await executeQuery(pool, resolved.value.sql, resolved.value.params);
|
|
115
|
+
if (!resolved.value.mutating)
|
|
116
|
+
return handleEndpoint('/zin/sqlserver/statement', result);
|
|
117
|
+
return toMutatingStatementResponse(result);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
Deps.Logger.error('[SqlServerProxyServer] Statement execution failed', {
|
|
121
|
+
path: requestPath,
|
|
122
|
+
statementId: resolved.value.statementId,
|
|
123
|
+
mutating: resolved.value.mutating,
|
|
124
|
+
paramsCount: resolved.value.params.length,
|
|
125
|
+
error: error instanceof Error ? error.message : String(error),
|
|
126
|
+
});
|
|
127
|
+
return Deps.ErrorHandler.toProxyError(500, 'SQLSERVER_ERROR', String(error));
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const handleSqlRequest = async (pool, requestPath, payload) => {
|
|
131
|
+
const sqlValidation = validateQueryPayload(payload);
|
|
132
|
+
if (!sqlValidation.valid) {
|
|
133
|
+
const error = sqlValidation.error ?? {
|
|
134
|
+
code: 'VALIDATION_ERROR',
|
|
135
|
+
message: 'Invalid SQL payload',
|
|
136
|
+
};
|
|
137
|
+
return Deps.ErrorHandler.toProxyError(400, error.code, error.message);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const result = await executeQuery(pool, sqlValidation.sql ?? '', sqlValidation.params ?? []);
|
|
141
|
+
return handleEndpoint(requestPath, result);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return Deps.ErrorHandler.toProxyError(500, 'SQLSERVER_ERROR', String(error));
|
|
145
|
+
}
|
|
99
146
|
};
|
|
100
|
-
const
|
|
147
|
+
const handleProxyRequest = async (pool, statements, request) => {
|
|
148
|
+
const validationError = Deps.validateProxyRequest(request);
|
|
149
|
+
if (validationError !== null)
|
|
150
|
+
return validationError;
|
|
151
|
+
const parsed = Deps.parseJsonBody(request.body);
|
|
152
|
+
if ('status' in parsed)
|
|
153
|
+
return parsed;
|
|
154
|
+
if (request.path === '/zin/sqlserver/statement') {
|
|
155
|
+
return handleStatementRequest(pool, statements, request.path, parsed.value);
|
|
156
|
+
}
|
|
157
|
+
return handleSqlRequest(pool, request.path, parsed.value);
|
|
158
|
+
};
|
|
159
|
+
const createBackend = (pool, statements) => ({
|
|
101
160
|
name: 'sqlserver',
|
|
102
|
-
handle: async (request) =>
|
|
103
|
-
const methodError = RequestValidator.requirePost(request.method);
|
|
104
|
-
if (methodError) {
|
|
105
|
-
return ErrorHandler.toProxyError(405, methodError.code, methodError.message);
|
|
106
|
-
}
|
|
107
|
-
const parsed = RequestValidator.parseJson(request.body);
|
|
108
|
-
if (!parsed.ok) {
|
|
109
|
-
return ErrorHandler.toProxyError(400, parsed.error.code, parsed.error.message);
|
|
110
|
-
}
|
|
111
|
-
const sqlValidation = validateQueryPayload(parsed.value);
|
|
112
|
-
if (!sqlValidation.valid) {
|
|
113
|
-
const error = sqlValidation.error ?? {
|
|
114
|
-
code: 'VALIDATION_ERROR',
|
|
115
|
-
message: 'Invalid SQL payload',
|
|
116
|
-
};
|
|
117
|
-
return ErrorHandler.toProxyError(400, error.code, error.message);
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
const result = await executeQuery(pool, sqlValidation.sql ?? '', sqlValidation.params ?? []);
|
|
121
|
-
return handleEndpoint(request.path, result);
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
return ErrorHandler.toProxyError(500, 'SQLSERVER_ERROR', String(error));
|
|
125
|
-
}
|
|
126
|
-
},
|
|
161
|
+
handle: async (request) => handleProxyRequest(pool, statements, request),
|
|
127
162
|
health: async () => {
|
|
128
163
|
try {
|
|
129
164
|
await executeQuery(pool, 'SELECT 1', []);
|
|
130
165
|
return { status: 200, body: { status: 'healthy' } };
|
|
131
166
|
}
|
|
132
167
|
catch (error) {
|
|
133
|
-
return ErrorHandler.toProxyError(503, 'UNHEALTHY', String(error));
|
|
168
|
+
return Deps.ErrorHandler.toProxyError(503, 'UNHEALTHY', String(error));
|
|
134
169
|
}
|
|
135
170
|
},
|
|
136
171
|
shutdown: async () => {
|
|
@@ -142,20 +177,20 @@ export const SqlServerProxyServer = Object.freeze({
|
|
|
142
177
|
async start(overrides = {}) {
|
|
143
178
|
const config = resolveConfig(overrides);
|
|
144
179
|
try {
|
|
145
|
-
Logger.info(`SQL Server proxy config: proxyHost=${config.host} proxyPort=${config.port} dbHost=${String(config.poolConfig['server'])} dbPort=${String(config.poolConfig['port'])} dbName=${String(config.poolConfig['database'])} dbUser=${String(config.poolConfig['user'])}`);
|
|
180
|
+
Deps.Logger.info(`SQL Server proxy config: proxyHost=${config.host} proxyPort=${config.port} dbHost=${String(config.poolConfig['server'])} dbPort=${String(config.poolConfig['port'])} dbName=${String(config.poolConfig['database'])} dbUser=${String(config.poolConfig['user'])}`);
|
|
146
181
|
}
|
|
147
182
|
catch {
|
|
148
183
|
// noop - logging must not block startup
|
|
149
184
|
}
|
|
150
185
|
const pool = await createPool(config.poolConfig);
|
|
151
|
-
const backend = createBackend(pool);
|
|
152
|
-
const proxy = createProxyServer({
|
|
186
|
+
const backend = createBackend(pool, config.statements);
|
|
187
|
+
const proxy = Deps.createProxyServer({
|
|
153
188
|
host: config.host,
|
|
154
189
|
port: config.port,
|
|
155
190
|
maxBodyBytes: config.maxBodyBytes,
|
|
156
191
|
backend,
|
|
157
192
|
verify: async (req, body) => {
|
|
158
|
-
const verified = await verifyRequestSignature(req, body, config, 'SqlServerProxyServer');
|
|
193
|
+
const verified = await Deps.verifyRequestSignature(req, body, config, 'SqlServerProxyServer');
|
|
159
194
|
if (!verified.ok && verified.error) {
|
|
160
195
|
return { ok: false, status: verified.error.status, message: verified.error.message };
|
|
161
196
|
}
|
|
@@ -163,6 +198,6 @@ export const SqlServerProxyServer = Object.freeze({
|
|
|
163
198
|
},
|
|
164
199
|
});
|
|
165
200
|
await proxy.start();
|
|
166
|
-
Logger.info(`✓ SQL Server proxy listening on ${config.host}:${config.port}`);
|
|
201
|
+
Deps.Logger.info(`✓ SQL Server proxy listening on ${config.host}:${config.port}`);
|
|
167
202
|
},
|
|
168
203
|
});
|
package/src/proxy.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
|
package/src/proxy.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ISchedule, IScheduleBackoffPolicy } from './types';
|
|
2
|
+
type CronOptions = {
|
|
3
|
+
timezone?: string;
|
|
4
|
+
};
|
|
5
|
+
type WithoutOverlappingOptions = {
|
|
6
|
+
provider?: string;
|
|
7
|
+
ttlMs?: number;
|
|
8
|
+
key?: string;
|
|
9
|
+
};
|
|
10
|
+
export type ScheduleBuilderApi = Readonly<{
|
|
11
|
+
everyMinute: () => ScheduleBuilderApi;
|
|
12
|
+
everyMinutes: (minutes: number) => ScheduleBuilderApi;
|
|
13
|
+
everyHour: () => ScheduleBuilderApi;
|
|
14
|
+
everyHours: (hours: number) => ScheduleBuilderApi;
|
|
15
|
+
intervalMs: (ms: number) => ScheduleBuilderApi;
|
|
16
|
+
cron: (expr: string, options?: CronOptions) => ScheduleBuilderApi;
|
|
17
|
+
timezone: (tz: string) => ScheduleBuilderApi;
|
|
18
|
+
jitterMs: (ms: number) => ScheduleBuilderApi;
|
|
19
|
+
backoff: (policy: IScheduleBackoffPolicy) => ScheduleBuilderApi;
|
|
20
|
+
leaderOnly: () => ScheduleBuilderApi;
|
|
21
|
+
runOnStart: () => ScheduleBuilderApi;
|
|
22
|
+
enabledWhen: (value: boolean) => ScheduleBuilderApi;
|
|
23
|
+
withoutOverlapping: (options?: WithoutOverlappingOptions) => ScheduleBuilderApi;
|
|
24
|
+
build: () => ISchedule;
|
|
25
|
+
}>;
|
|
26
|
+
export declare const Schedule: Readonly<{
|
|
27
|
+
define(name: string, handler: ISchedule["handler"]): ScheduleBuilderApi;
|
|
28
|
+
}>;
|
|
29
|
+
export declare const ScheduleBuilder: Readonly<{
|
|
30
|
+
create(input: {
|
|
31
|
+
name: string;
|
|
32
|
+
handler: ISchedule["handler"];
|
|
33
|
+
}): ScheduleBuilderApi;
|
|
34
|
+
}>;
|
|
35
|
+
export default Schedule;
|
|
36
|
+
//# sourceMappingURL=Schedule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Schedule.d.ts","sourceRoot":"","sources":["../../../src/scheduler/Schedule.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,KAAK,WAAW,GAAG;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,yBAAyB,GAAG;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,WAAW,EAAE,MAAM,kBAAkB,CAAC;IACtC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,kBAAkB,CAAC;IACtD,SAAS,EAAE,MAAM,kBAAkB,CAAC;IACpC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAClD,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAC/C,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,kBAAkB,CAAC;IAClE,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAC7C,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,kBAAkB,CAAC;IAC7C,OAAO,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,kBAAkB,CAAC;IAChE,UAAU,EAAE,MAAM,kBAAkB,CAAC;IACrC,UAAU,EAAE,MAAM,kBAAkB,CAAC;IACrC,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,kBAAkB,CAAC;IACpD,kBAAkB,EAAE,CAAC,OAAO,CAAC,EAAE,yBAAyB,KAAK,kBAAkB,CAAC;IAChF,KAAK,EAAE,MAAM,SAAS,CAAC;CACxB,CAAC,CAAC;AA4HH,eAAO,MAAM,QAAQ;iBACN,MAAM,WAAW,SAAS,CAAC,SAAS,CAAC,GAAG,kBAAkB;EAGvE,CAAC;AAqFH,eAAO,MAAM,eAAe;kBACZ;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;KAAE,GAAG,kBAAkB;EAQlF,CAAC;AAEH,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Env } from '../config/env.js';
|
|
2
|
+
import { Logger } from '../config/logger.js';
|
|
3
|
+
import { ZintrustLang } from '../lang/lang.js';
|
|
4
|
+
import { createAdvancedQueue } from '../tools/queue/AdvancedQueue.js';
|
|
5
|
+
import { getLockProvider } from '../tools/queue/LockProvider.js';
|
|
6
|
+
const toIntervalMs = (ms) => {
|
|
7
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
8
|
+
return 0;
|
|
9
|
+
return Math.floor(ms);
|
|
10
|
+
};
|
|
11
|
+
const normalizeOptionalString = (value) => {
|
|
12
|
+
const s = typeof value === 'string' ? value.trim() : '';
|
|
13
|
+
return s.length > 0 ? s : undefined;
|
|
14
|
+
};
|
|
15
|
+
const toPositiveInt = (value) => {
|
|
16
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
17
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
return Math.floor(n);
|
|
20
|
+
};
|
|
21
|
+
const resolveLockProvider = (providerName) => {
|
|
22
|
+
const name = providerName.trim().toLowerCase();
|
|
23
|
+
if (name.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
// Ensure provider is registered. createAdvancedQueue triggers lock-provider registration.
|
|
26
|
+
createAdvancedQueue({
|
|
27
|
+
name: ZintrustLang.CLI_LOCKS,
|
|
28
|
+
connection: undefined,
|
|
29
|
+
defaultDedupTtl: Env.getInt('SCHEDULE_OVERLAP_LOCK_TTL_MS', 300000),
|
|
30
|
+
lockProvider: name,
|
|
31
|
+
});
|
|
32
|
+
return getLockProvider(name);
|
|
33
|
+
};
|
|
34
|
+
const normalizeBackoffFactor = (factor) => {
|
|
35
|
+
if (factor === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
return Number.isFinite(factor) ? factor : undefined;
|
|
38
|
+
};
|
|
39
|
+
const wrapWithoutOverlapping = (scheduleName, handler, options) => {
|
|
40
|
+
const providerName = (options.provider ?? 'redis').trim();
|
|
41
|
+
const lockKey = (options.key ?? `schedule:${scheduleName}`).trim();
|
|
42
|
+
const ttlMs = Env.getInt('SCHEDULE_OVERLAP_LOCK_TTL_MS', options.ttlMs ?? 300000);
|
|
43
|
+
const acquireTimeoutMs = Env.getInt('SCHEDULE_OVERLAP_LOCK_ACQUIRE_TIMEOUT_MS', 2000);
|
|
44
|
+
const withTimeout = async (promise, timeoutMs) => {
|
|
45
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
46
|
+
return promise;
|
|
47
|
+
let timeoutId;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
promise,
|
|
51
|
+
new Promise((_, reject) => {
|
|
52
|
+
timeoutId = globalThis.setTimeout(() => {
|
|
53
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
54
|
+
reject(new Error('Lock acquire timed out'));
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
}),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (timeoutId !== undefined)
|
|
61
|
+
globalThis.clearTimeout(timeoutId);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return async (kernel) => {
|
|
65
|
+
const provider = resolveLockProvider(providerName);
|
|
66
|
+
if (!provider) {
|
|
67
|
+
Logger.warn('Schedule withoutOverlapping requested but lock provider not available; running anyway', {
|
|
68
|
+
schedule: scheduleName,
|
|
69
|
+
provider: providerName,
|
|
70
|
+
});
|
|
71
|
+
await handler(kernel);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let lock;
|
|
75
|
+
try {
|
|
76
|
+
lock = await withTimeout(provider.acquire(lockKey, { ttl: ttlMs }), acquireTimeoutMs);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
Logger.warn('Schedule lock acquire failed; running anyway', {
|
|
80
|
+
schedule: scheduleName,
|
|
81
|
+
provider: providerName,
|
|
82
|
+
timeoutMs: acquireTimeoutMs,
|
|
83
|
+
message: error instanceof Error ? error.message : String(error),
|
|
84
|
+
});
|
|
85
|
+
await handler(kernel);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!lock.acquired) {
|
|
89
|
+
Logger.info(`Skipping overlapping run for schedule: ${scheduleName}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await handler(kernel);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
try {
|
|
97
|
+
await provider.release(lock);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// best-effort
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
export const Schedule = Object.freeze({
|
|
106
|
+
define(name, handler) {
|
|
107
|
+
return ScheduleBuilder.create({ name, handler });
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const createScheduleApi = (state) => {
|
|
111
|
+
const api = Object.freeze({
|
|
112
|
+
everyMinute: () => api.everyMinutes(1),
|
|
113
|
+
everyMinutes: (minutes) => {
|
|
114
|
+
const resolved = Math.max(1, Math.floor(minutes));
|
|
115
|
+
state.intervalMs = resolved * 60_000;
|
|
116
|
+
return api;
|
|
117
|
+
},
|
|
118
|
+
everyHour: () => api.everyHours(1),
|
|
119
|
+
everyHours: (hours) => {
|
|
120
|
+
const resolved = Math.max(1, Math.floor(hours));
|
|
121
|
+
state.intervalMs = resolved * 3_600_000;
|
|
122
|
+
return api;
|
|
123
|
+
},
|
|
124
|
+
intervalMs: (ms) => {
|
|
125
|
+
state.intervalMs = toIntervalMs(ms);
|
|
126
|
+
return api;
|
|
127
|
+
},
|
|
128
|
+
cron: (expr, options) => {
|
|
129
|
+
state.cron = normalizeOptionalString(expr);
|
|
130
|
+
if (options?.timezone !== undefined) {
|
|
131
|
+
state.timezone = normalizeOptionalString(options.timezone);
|
|
132
|
+
}
|
|
133
|
+
return api;
|
|
134
|
+
},
|
|
135
|
+
timezone: (tz) => {
|
|
136
|
+
state.timezone = normalizeOptionalString(tz);
|
|
137
|
+
return api;
|
|
138
|
+
},
|
|
139
|
+
jitterMs: (ms) => {
|
|
140
|
+
state.jitterMs = toPositiveInt(ms);
|
|
141
|
+
return api;
|
|
142
|
+
},
|
|
143
|
+
backoff: (policy) => {
|
|
144
|
+
state.backoff = {
|
|
145
|
+
initialMs: toPositiveInt(policy.initialMs) ?? 0,
|
|
146
|
+
maxMs: toPositiveInt(policy.maxMs) ?? 0,
|
|
147
|
+
factor: normalizeBackoffFactor(policy.factor),
|
|
148
|
+
};
|
|
149
|
+
return api;
|
|
150
|
+
},
|
|
151
|
+
leaderOnly: () => {
|
|
152
|
+
state.leaderOnly = true;
|
|
153
|
+
return api;
|
|
154
|
+
},
|
|
155
|
+
runOnStart: () => {
|
|
156
|
+
state.runOnStart = true;
|
|
157
|
+
return api;
|
|
158
|
+
},
|
|
159
|
+
enabledWhen: (value) => {
|
|
160
|
+
state.enabled = value;
|
|
161
|
+
return api;
|
|
162
|
+
},
|
|
163
|
+
withoutOverlapping: (options) => {
|
|
164
|
+
state.overlap = options ?? {};
|
|
165
|
+
return api;
|
|
166
|
+
},
|
|
167
|
+
build: () => {
|
|
168
|
+
const handler = state.overlap === undefined
|
|
169
|
+
? state.handler
|
|
170
|
+
: wrapWithoutOverlapping(state.name, state.handler, state.overlap);
|
|
171
|
+
const schedule = {
|
|
172
|
+
name: state.name,
|
|
173
|
+
intervalMs: state.intervalMs,
|
|
174
|
+
cron: state.cron,
|
|
175
|
+
timezone: state.timezone,
|
|
176
|
+
jitterMs: state.jitterMs,
|
|
177
|
+
backoff: state.backoff,
|
|
178
|
+
leaderOnly: state.leaderOnly,
|
|
179
|
+
handler,
|
|
180
|
+
enabled: state.enabled,
|
|
181
|
+
runOnStart: state.runOnStart,
|
|
182
|
+
};
|
|
183
|
+
return schedule;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return api;
|
|
187
|
+
};
|
|
188
|
+
export const ScheduleBuilder = Object.freeze({
|
|
189
|
+
create(input) {
|
|
190
|
+
const state = {
|
|
191
|
+
name: input.name,
|
|
192
|
+
handler: input.handler,
|
|
193
|
+
};
|
|
194
|
+
return createScheduleApi(state);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
export default Schedule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScheduleHttpGateway.d.ts","sourceRoot":"","sources":["../../../src/scheduler/ScheduleHttpGateway.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AA8QnD,eAAO,MAAM,mBAAmB;cACpB;QAAE,cAAc,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE;EAWvD,CAAC;AAEH,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Env } from '../config/env.js';
|
|
2
|
+
import { Logger } from '../config/logger.js';
|
|
3
|
+
import { Router } from '../routes/Router.js';
|
|
4
|
+
import { ErrorFactory } from '../exceptions/ZintrustError.js';
|
|
5
|
+
import { SchedulerRuntime } from './SchedulerRuntime.js';
|
|
6
|
+
import { SignedRequest } from '../security/SignedRequest.js';
|
|
7
|
+
const nonces = new Map();
|
|
8
|
+
const nowMs = () => Date.now();
|
|
9
|
+
const normalizePath = (value) => {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (trimmed === '')
|
|
12
|
+
return '/api/_sys/schedule/rpc';
|
|
13
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
14
|
+
};
|
|
15
|
+
const parseMiddleware = (value) => value
|
|
16
|
+
.split(',')
|
|
17
|
+
.map((entry) => entry.trim())
|
|
18
|
+
.filter((entry) => entry.length > 0);
|
|
19
|
+
const readSettings = () => {
|
|
20
|
+
const configuredSecret = Env.get('SCHEDULE_HTTP_PROXY_KEY', '').trim();
|
|
21
|
+
const secret = configuredSecret === '' ? Env.APP_KEY : configuredSecret;
|
|
22
|
+
return {
|
|
23
|
+
basePath: normalizePath(Env.get('SCHEDULE_HTTP_PROXY_PATH', '/api/_sys/schedule/rpc')),
|
|
24
|
+
keyId: Env.get('SCHEDULE_HTTP_PROXY_KEY_ID', Env.APP_NAME || 'zintrust').trim(),
|
|
25
|
+
secret,
|
|
26
|
+
signingWindowMs: Env.getInt('SCHEDULE_HTTP_PROXY_MAX_SKEW_MS', 60000),
|
|
27
|
+
nonceTtlMs: Env.getInt('SCHEDULE_HTTP_PROXY_NONCE_TTL_MS', 120000),
|
|
28
|
+
middleware: parseMiddleware(Env.get('SCHEDULE_HTTP_PROXY_MIDDLEWARE', '')),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const cleanupExpiredNonces = () => {
|
|
32
|
+
const current = nowMs();
|
|
33
|
+
for (const [nonceKey, expiresAt] of nonces.entries()) {
|
|
34
|
+
if (expiresAt <= current)
|
|
35
|
+
nonces.delete(nonceKey);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
39
|
+
const storeNonce = async (keyId, nonce, ttlMs) => {
|
|
40
|
+
cleanupExpiredNonces();
|
|
41
|
+
const nonceKey = `${keyId}:${nonce}`;
|
|
42
|
+
if (nonces.has(nonceKey))
|
|
43
|
+
return false;
|
|
44
|
+
nonces.set(nonceKey, nowMs() + Math.max(ttlMs, 1));
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
const getBodyRecord = (req) => {
|
|
48
|
+
const body = req.getBody?.() ?? req.body;
|
|
49
|
+
if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
|
|
50
|
+
return body;
|
|
51
|
+
}
|
|
52
|
+
return {};
|
|
53
|
+
};
|
|
54
|
+
const getRawBody = (req) => {
|
|
55
|
+
const rawText = req.context?.['rawBodyText'];
|
|
56
|
+
if (typeof rawText === 'string')
|
|
57
|
+
return rawText;
|
|
58
|
+
return JSON.stringify(getBodyRecord(req));
|
|
59
|
+
};
|
|
60
|
+
const toIncomingHeaders = (req) => {
|
|
61
|
+
const headers = req.getHeaders();
|
|
62
|
+
const normalize = (value) => {
|
|
63
|
+
if (Array.isArray(value))
|
|
64
|
+
return value.join(',');
|
|
65
|
+
return value;
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
'x-zt-key-id': normalize(headers['x-zt-key-id']),
|
|
69
|
+
'x-zt-nonce': normalize(headers['x-zt-nonce']),
|
|
70
|
+
'x-zt-timestamp': normalize(headers['x-zt-timestamp']),
|
|
71
|
+
'x-zt-body-sha256': normalize(headers['x-zt-body-sha256']),
|
|
72
|
+
'x-zt-signature': normalize(headers['x-zt-signature']),
|
|
73
|
+
'content-type': normalize(headers['content-type']),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const parseRequest = (req) => {
|
|
77
|
+
const body = getBodyRecord(req);
|
|
78
|
+
const action = String(body['action'] ?? '')
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
const requestId = String(body['requestId'] ?? '').trim();
|
|
82
|
+
if (requestId.length === 0) {
|
|
83
|
+
throw ErrorFactory.createValidationError('requestId is required');
|
|
84
|
+
}
|
|
85
|
+
if (action !== 'list' && action !== 'run') {
|
|
86
|
+
throw ErrorFactory.createValidationError('Invalid action');
|
|
87
|
+
}
|
|
88
|
+
const payload = typeof body['payload'] === 'object' && body['payload'] !== null
|
|
89
|
+
? body['payload']
|
|
90
|
+
: undefined;
|
|
91
|
+
return {
|
|
92
|
+
action: action,
|
|
93
|
+
requestId,
|
|
94
|
+
payload,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const ok = (requestId, result) => ({
|
|
98
|
+
ok: true,
|
|
99
|
+
requestId,
|
|
100
|
+
result,
|
|
101
|
+
error: null,
|
|
102
|
+
});
|
|
103
|
+
const fail = (requestId, code, message, details) => ({
|
|
104
|
+
ok: false,
|
|
105
|
+
requestId,
|
|
106
|
+
result: null,
|
|
107
|
+
error: { code, message, details },
|
|
108
|
+
});
|
|
109
|
+
const listSchedules = async () => {
|
|
110
|
+
const rows = await SchedulerRuntime.listWithState();
|
|
111
|
+
return rows.map(({ schedule, state }) => ({
|
|
112
|
+
name: schedule.name,
|
|
113
|
+
intervalMs: schedule.intervalMs,
|
|
114
|
+
cron: schedule.cron,
|
|
115
|
+
timezone: schedule.timezone,
|
|
116
|
+
enabled: schedule.enabled,
|
|
117
|
+
runOnStart: schedule.runOnStart,
|
|
118
|
+
state,
|
|
119
|
+
}));
|
|
120
|
+
};
|
|
121
|
+
const verifyRequest = async (req, bodyText, settings) => {
|
|
122
|
+
if (settings.keyId.trim() === '' || settings.secret.trim() === '') {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
status: 500,
|
|
126
|
+
code: 'CONFIG_ERROR',
|
|
127
|
+
message: 'Schedule HTTP gateway signing credentials are not configured',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const url = new URL(req.getPath(), 'http://localhost');
|
|
131
|
+
const verifyResult = await SignedRequest.verify({
|
|
132
|
+
method: req.getMethod(),
|
|
133
|
+
url,
|
|
134
|
+
body: bodyText,
|
|
135
|
+
headers: toIncomingHeaders(req),
|
|
136
|
+
nowMs: nowMs(),
|
|
137
|
+
windowMs: settings.signingWindowMs,
|
|
138
|
+
verifyNonce: async (keyId, nonce) => storeNonce(keyId, nonce, settings.nonceTtlMs),
|
|
139
|
+
getSecretForKeyId: (keyId) => {
|
|
140
|
+
if (keyId === settings.keyId)
|
|
141
|
+
return settings.secret;
|
|
142
|
+
return undefined;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (verifyResult.ok === true)
|
|
146
|
+
return { ok: true };
|
|
147
|
+
const errorCode = 'code' in verifyResult ? verifyResult.code : 'INVALID_SIGNATURE';
|
|
148
|
+
const errorMessage = 'message' in verifyResult ? verifyResult.message : 'Invalid signature';
|
|
149
|
+
const status = errorCode === 'EXPIRED' || errorCode === 'REPLAYED' ? 401 : 403;
|
|
150
|
+
return { ok: false, status, code: errorCode, message: errorMessage };
|
|
151
|
+
};
|
|
152
|
+
const handleRpc = async (req, res) => {
|
|
153
|
+
const settings = readSettings();
|
|
154
|
+
const rawBody = getRawBody(req);
|
|
155
|
+
const body = getBodyRecord(req);
|
|
156
|
+
const requestId = typeof body['requestId'] === 'string' && String(body['requestId']).trim() !== ''
|
|
157
|
+
? String(body['requestId'])
|
|
158
|
+
: 'unknown';
|
|
159
|
+
const auth = await verifyRequest(req, rawBody, settings);
|
|
160
|
+
if (auth.ok === false) {
|
|
161
|
+
res.status(auth.status).json(fail(requestId, auth.code, auth.message));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const parsed = parseRequest(req);
|
|
165
|
+
try {
|
|
166
|
+
if (parsed.action === 'list') {
|
|
167
|
+
res.json(ok(parsed.requestId, await listSchedules()));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const name = String(parsed.payload?.name ?? '').trim();
|
|
171
|
+
if (name.length === 0) {
|
|
172
|
+
res.status(400).json(fail(parsed.requestId, 'VALIDATION_ERROR', 'payload.name is required'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
await SchedulerRuntime.runOnce(name);
|
|
176
|
+
res.json(ok(parsed.requestId, { ran: true, name }));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
Logger.error('Schedule RPC failed', error);
|
|
180
|
+
res.status(500).json(fail(parsed.requestId, 'INTERNAL_ERROR', 'Schedule RPC failed', {
|
|
181
|
+
message: error instanceof Error ? error.message : String(error),
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
export const ScheduleHttpGateway = Object.freeze({
|
|
186
|
+
create() {
|
|
187
|
+
const settings = readSettings();
|
|
188
|
+
const options = settings.middleware.length > 0 ? { middleware: settings.middleware } : undefined;
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
registerRoutes(router) {
|
|
191
|
+
Router.post(router, settings.basePath, handleRpc, options);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
export default ScheduleHttpGateway;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* ScheduleRunner
|
|
3
3
|
* Lightweight, zero-dependency schedule runner for long-running runtimes
|
|
4
4
|
*/
|
|
5
|
+
import { type ScheduleRunState } from './state/ScheduleStateStore';
|
|
5
6
|
import type { ISchedule, IScheduleKernel } from './types';
|
|
6
7
|
type ScheduleRunner = {
|
|
7
8
|
register: (schedule: ISchedule) => void;
|
|
@@ -9,6 +10,11 @@ type ScheduleRunner = {
|
|
|
9
10
|
stop: (timeoutMs?: number) => Promise<void>;
|
|
10
11
|
list: () => ISchedule[];
|
|
11
12
|
runOnce: (name: string, kernel?: IScheduleKernel) => Promise<void>;
|
|
13
|
+
getState: (name: string) => Promise<ScheduleRunState | null>;
|
|
14
|
+
listStates: () => Promise<Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
state: ScheduleRunState;
|
|
17
|
+
}>>;
|
|
12
18
|
};
|
|
13
19
|
export declare const create: () => Readonly<ScheduleRunner>;
|
|
14
20
|
declare const _default: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScheduleRunner.d.ts","sourceRoot":"","sources":["../../../src/scheduler/ScheduleRunner.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"ScheduleRunner.d.ts","sourceRoot":"","sources":["../../../src/scheduler/ScheduleRunner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAkBnE,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC;IACxC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;IAC1C,IAAI,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,SAAS,EAAE,CAAC;IACxB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC7D,UAAU,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,gBAAgB,CAAA;KAAE,CAAC,CAAC,CAAC;CAC7E,CAAC;AA6VF,eAAO,MAAM,MAAM,QAAO,QAAQ,CAAC,cAAc,CA6BhD,CAAC;;kBA7BwB,QAAQ,CAAC,cAAc,CAAC;;AA+BlD,wBAA0B"}
|