@kici-dev/shared 0.1.14 → 0.1.16
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/README.md +13 -1
- package/dist/db-admin.d.ts +29 -0
- package/dist/db-admin.js +56 -5
- package/dist/db.d.ts +20 -1
- package/dist/db.js +34 -2
- package/dist/db.test.d.ts +2 -0
- package/dist/diagnostics/bundle-archive.d.ts +28 -0
- package/dist/diagnostics/bundle-archive.js +141 -0
- package/dist/diagnostics/bundle-archive.test.d.ts +2 -0
- package/dist/diagnostics/bundle-chunks.d.ts +46 -0
- package/dist/diagnostics/bundle-chunks.js +111 -0
- package/dist/diagnostics/bundle-chunks.test.d.ts +2 -0
- package/dist/graceful-shutdown.d.ts +13 -1
- package/dist/graceful-shutdown.js +23 -9
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/s3-client.js +4 -1
- package/dist/s3-client.test.d.ts +2 -0
- package/package.json +13 -7
- package/sbom.spdx.json +2338 -510
package/README.md
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# @kici-dev/shared
|
|
2
|
+
|
|
3
|
+
Shared utilities for the KiCI CI/CD stack — logging, zx setup, crypto, telemetry, health and metrics routes. No business logic. Re-exports `@kici-dev/core`.
|
|
4
|
+
|
|
5
|
+
This is an internal support library — other `@kici-dev` packages depend on it; it is not meant to be installed directly.
|
|
6
|
+
|
|
7
|
+
Part of [KiCI](https://kici.dev) — CI/CD workflows as TypeScript code: author them with full language power, dry-run them locally, and run them on your own infrastructure.
|
|
8
|
+
|
|
9
|
+
## Links
|
|
10
|
+
|
|
11
|
+
- Documentation: <https://docs.kici.dev/architecture/overview/>
|
|
12
|
+
- Source: <https://github.com/kici-dev/kici-public/tree/main/packages/shared>
|
|
13
|
+
- License: Apache-2.0
|
package/dist/db-admin.d.ts
CHANGED
|
@@ -187,6 +187,7 @@ export interface SeedEnvironmentOpts {
|
|
|
187
187
|
waitTimerSeconds?: number | null;
|
|
188
188
|
holdExpirySeconds?: number | null;
|
|
189
189
|
minimumTrust?: string | null;
|
|
190
|
+
globPattern?: string | null;
|
|
190
191
|
}
|
|
191
192
|
export interface SeedEnvironmentResult {
|
|
192
193
|
envId: string;
|
|
@@ -198,6 +199,24 @@ export interface SeedEnvironmentResult {
|
|
|
198
199
|
* are JSON-serialised server-side; pass them as plain arrays or objects.
|
|
199
200
|
*/
|
|
200
201
|
export declare function seedEnvironmentDirect(databaseUrl: string, opts: SeedEnvironmentOpts): Promise<SeedEnvironmentResult>;
|
|
202
|
+
export interface DeleteEnvironmentOpts {
|
|
203
|
+
orgId: string;
|
|
204
|
+
name: string;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Delete an environment keyed by (org_id, name). Returns whether a row was
|
|
208
|
+
* removed. The `environment_bindings`, `environment_variables`, and
|
|
209
|
+
* `environment_source_overrides` children all carry
|
|
210
|
+
* `FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE`,
|
|
211
|
+
* so a single DELETE on `environments` cascades to those children. The
|
|
212
|
+
* `held_runs` FK uses `ON DELETE SET NULL`, so terminal held-run history
|
|
213
|
+
* survives the delete with a null environment reference. Pending held runs
|
|
214
|
+
* still reference the environment, so this helper pre-checks their count and
|
|
215
|
+
* throws before issuing the DELETE — approve or reject them first.
|
|
216
|
+
*/
|
|
217
|
+
export declare function deleteEnvironmentDirect(databaseUrl: string, opts: DeleteEnvironmentOpts): Promise<{
|
|
218
|
+
deleted: boolean;
|
|
219
|
+
}>;
|
|
201
220
|
export interface SeedEnvironmentBindingOpts {
|
|
202
221
|
orgId: string;
|
|
203
222
|
envName: string;
|
|
@@ -219,6 +238,7 @@ export interface SetEnvironmentPolicyOpts {
|
|
|
219
238
|
holdExpirySeconds?: number | null;
|
|
220
239
|
minimumTrust?: string | null;
|
|
221
240
|
enabled?: boolean;
|
|
241
|
+
allowLocalExecution?: boolean;
|
|
222
242
|
}
|
|
223
243
|
/**
|
|
224
244
|
* UPDATE only the policy fields that were explicitly provided. Columns that
|
|
@@ -1462,5 +1482,14 @@ export declare function clearPeerCredentialsRevokedAtByIdsDirect(databaseUrl: st
|
|
|
1462
1482
|
export declare function countActivePeerCredentialsByInstanceDirect(databaseUrl: string, opts: {
|
|
1463
1483
|
instanceId: string;
|
|
1464
1484
|
}): Promise<number>;
|
|
1485
|
+
/**
|
|
1486
|
+
* Terminate every idle backend of the connecting user except our own
|
|
1487
|
+
* connection. Mirrors what a Postgres leader demotion does to idle pooled
|
|
1488
|
+
* connections — used by resilience tests to verify the pg pool error
|
|
1489
|
+
* handlers absorb the termination without a process restart.
|
|
1490
|
+
*
|
|
1491
|
+
* Returns the number of backends terminated.
|
|
1492
|
+
*/
|
|
1493
|
+
export declare function terminateIdleDbBackendsDirect(databaseUrl: string): Promise<number>;
|
|
1465
1494
|
export {};
|
|
1466
1495
|
//# sourceMappingURL=db-admin.d.ts.map
|
package/dist/db-admin.js
CHANGED
|
@@ -351,7 +351,8 @@ const ENV_POLICY_COLUMNS = new Set([
|
|
|
351
351
|
"wait_timer_seconds",
|
|
352
352
|
"hold_expiry_seconds",
|
|
353
353
|
"minimum_trust",
|
|
354
|
-
"enabled"
|
|
354
|
+
"enabled",
|
|
355
|
+
"allow_local_execution"
|
|
355
356
|
]);
|
|
356
357
|
/**
|
|
357
358
|
* Upsert an environment row keyed by (org_id, name). Returns the env id and
|
|
@@ -370,9 +371,9 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
370
371
|
const reviewersJson = opts.requiredReviewers === void 0 ? null : JSON.stringify(opts.requiredReviewers);
|
|
371
372
|
const row = (await pool.query(`INSERT INTO environments
|
|
372
373
|
(org_id, name, type, enabled, branch_restrictions, required_reviewers,
|
|
373
|
-
wait_timer_seconds, hold_expiry_seconds, minimum_trust)
|
|
374
|
+
wait_timer_seconds, hold_expiry_seconds, minimum_trust, glob_pattern)
|
|
374
375
|
VALUES ($1, $2, COALESCE($3, 'fixed'), COALESCE($4, true), $5::jsonb, $6::jsonb,
|
|
375
|
-
$7, COALESCE($8, 86400), $9)
|
|
376
|
+
$7, COALESCE($8, 86400), $9, $10)
|
|
376
377
|
ON CONFLICT (org_id, name) DO UPDATE SET
|
|
377
378
|
type = COALESCE(EXCLUDED.type, environments.type),
|
|
378
379
|
enabled = EXCLUDED.enabled,
|
|
@@ -381,6 +382,7 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
381
382
|
wait_timer_seconds = EXCLUDED.wait_timer_seconds,
|
|
382
383
|
hold_expiry_seconds = EXCLUDED.hold_expiry_seconds,
|
|
383
384
|
minimum_trust = EXCLUDED.minimum_trust,
|
|
385
|
+
glob_pattern = COALESCE(EXCLUDED.glob_pattern, environments.glob_pattern),
|
|
384
386
|
updated_at = now()
|
|
385
387
|
RETURNING id, (xmax = 0) AS inserted`, [
|
|
386
388
|
opts.orgId,
|
|
@@ -391,7 +393,8 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
391
393
|
reviewersJson,
|
|
392
394
|
opts.waitTimerSeconds ?? null,
|
|
393
395
|
opts.holdExpirySeconds ?? null,
|
|
394
|
-
opts.minimumTrust ?? null
|
|
396
|
+
opts.minimumTrust ?? null,
|
|
397
|
+
opts.globPattern ?? null
|
|
395
398
|
])).rows[0];
|
|
396
399
|
if (!row) throw new Error(`environment: upsert returned no row for ${opts.name}`);
|
|
397
400
|
return {
|
|
@@ -403,6 +406,33 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
403
406
|
}
|
|
404
407
|
}
|
|
405
408
|
/**
|
|
409
|
+
* Delete an environment keyed by (org_id, name). Returns whether a row was
|
|
410
|
+
* removed. The `environment_bindings`, `environment_variables`, and
|
|
411
|
+
* `environment_source_overrides` children all carry
|
|
412
|
+
* `FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE`,
|
|
413
|
+
* so a single DELETE on `environments` cascades to those children. The
|
|
414
|
+
* `held_runs` FK uses `ON DELETE SET NULL`, so terminal held-run history
|
|
415
|
+
* survives the delete with a null environment reference. Pending held runs
|
|
416
|
+
* still reference the environment, so this helper pre-checks their count and
|
|
417
|
+
* throws before issuing the DELETE — approve or reject them first.
|
|
418
|
+
*/
|
|
419
|
+
async function deleteEnvironmentDirect(databaseUrl, opts) {
|
|
420
|
+
const pool = new pg.Pool({
|
|
421
|
+
connectionString: databaseUrl,
|
|
422
|
+
max: 1
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
const pending = await pool.query(`SELECT count(*)::text AS count FROM held_runs hr
|
|
426
|
+
JOIN environments e ON e.id = hr.environment_id
|
|
427
|
+
WHERE e.org_id = $1 AND e.name = $2 AND hr.status = 'pending'`, [opts.orgId, opts.name]);
|
|
428
|
+
const pendingCount = Number(pending.rows[0]?.count ?? 0);
|
|
429
|
+
if (pendingCount > 0) throw new Error(`environment has ${pendingCount} pending held run(s) — approve or reject them first`);
|
|
430
|
+
return { deleted: (await pool.query(`DELETE FROM environments WHERE org_id = $1 AND name = $2 RETURNING id`, [opts.orgId, opts.name])).rows.length > 0 };
|
|
431
|
+
} finally {
|
|
432
|
+
await pool.end();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
406
436
|
* Upsert an `environment_bindings` row connecting `envName` to `scopePattern`.
|
|
407
437
|
* Throws if the environment does not exist.
|
|
408
438
|
*/
|
|
@@ -449,6 +479,7 @@ async function setEnvironmentPolicyDirect(databaseUrl, opts) {
|
|
|
449
479
|
if (opts.holdExpirySeconds !== void 0) addSet("hold_expiry_seconds", opts.holdExpirySeconds);
|
|
450
480
|
if (opts.minimumTrust !== void 0) addSet("minimum_trust", opts.minimumTrust);
|
|
451
481
|
if (opts.enabled !== void 0) addSet("enabled", opts.enabled);
|
|
482
|
+
if (opts.allowLocalExecution !== void 0) addSet("allow_local_execution", opts.allowLocalExecution);
|
|
452
483
|
if (setClauses.length === 0) throw new Error("environment: setEnvironmentPolicy requires at least one policy field");
|
|
453
484
|
const pool = new pg.Pool({
|
|
454
485
|
connectionString: databaseUrl,
|
|
@@ -2555,7 +2586,27 @@ async function countActivePeerCredentialsByInstanceDirect(databaseUrl, opts) {
|
|
|
2555
2586
|
await pool.end();
|
|
2556
2587
|
}
|
|
2557
2588
|
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Terminate every idle backend of the connecting user except our own
|
|
2591
|
+
* connection. Mirrors what a Postgres leader demotion does to idle pooled
|
|
2592
|
+
* connections — used by resilience tests to verify the pg pool error
|
|
2593
|
+
* handlers absorb the termination without a process restart.
|
|
2594
|
+
*
|
|
2595
|
+
* Returns the number of backends terminated.
|
|
2596
|
+
*/
|
|
2597
|
+
async function terminateIdleDbBackendsDirect(databaseUrl) {
|
|
2598
|
+
const pool = createPool(databaseUrl);
|
|
2599
|
+
try {
|
|
2600
|
+
return (await pool.query(`SELECT pg_terminate_backend(pid)
|
|
2601
|
+
FROM pg_stat_activity
|
|
2602
|
+
WHERE pid <> pg_backend_pid()
|
|
2603
|
+
AND usename = current_user
|
|
2604
|
+
AND state = 'idle'`)).rowCount ?? 0;
|
|
2605
|
+
} finally {
|
|
2606
|
+
await pool.end();
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2558
2609
|
//#endregion
|
|
2559
|
-
export { MIGRATION_HASH_TABLE, PROVIDER_HASH_KEY, REGISTERABLE_TRIGGER_TYPES, apiKeyExistsDirect, bumpRegistryVersionDirect, bumpRegistryVersionSimpleDirect, cleanupExecutionRowsDirect, clearDispatchQueueDirect, clearPeerCredentialsRevokedAtByIdsDirect, computeMigrationsHash, countActivePeerCredentialsByInstanceDirect, countCronLastFiredDirect, countHeldRunsByRunIdDirect, countWebhookSourcesByConnectionIdDirect, createDbRole, createEnvironmentTemplateDirect, createJoinTokenDirect, createReadOnlyDbUser, deleteCronLastFiredDirect, deleteCrossRepoTrustDirect, deleteExecutionRunsByWorkflowNameDirect, deleteGenericWebhookSourcesByNameDirect, deleteKiciEventsDirect, deleteOrgSettingsByCustomerIdDirect, deletePeerCredentialsByInstanceIdLikeDirect, deleteWorkflowRegistrationsDirect, describeTableColumnsDirect, dropAndCreateDatabase, dropDatabaseDirect, emitKiciEventDirect, ensureDatabase, ensureOrgOwnerMemberDirect, findAnyUserApiKeyIdDirect, getExecutionRunSecurityDirect, getGenericWebhookSourceByRoutingKeyDirect, getHeldRunByIdDirect, getPeerCredentialRevokedAtDirect, getRegistryVersionDirect, getWebhookSourceByRoutingKeyDirect, getWorkflowRegistrationByIdDirect, insertCronLastFiredNowDirect, insertCrossRepoTrustStrictDirect, insertKiciEventRawDirect, insertPeerCredentialExpiredDirect, insertWorkflowRegistrationRawDirect, insertWorkflowRegistrationStrictDirect, isSchemaCurrent, isSchemaCurrentFromFilesDirect, latestExecutionRunByStatusDirect, listActiveGenericWebhookSourcesDirect, listActivePeerCredentialsExcludingDirect, listCrossRepoTrustBySourceRoutingKeyDirect, listEnvironmentsDirect, listExecutionJobsDirect, listExecutionRunsDirect, listKiciEventsDirect, listQueueDirect, listRegistrationsByRoutingKeyDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, platformConnectionExistsDirect, pollKiciEventsDirect, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, readStoredMigrationContentHash, registerWorkflowManualDirect, resetRaftStateDirect, resolvePlatformWebhookSourceRoutingKeyDirect, restoreSoftDeletedGenericWebhookSourceDirect, seedApiKeyInlineDirect, seedCiSecurityFixturesDirect, seedCrossRepoTrustDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, seedGenericWebhookSourceDirect, seedSourcePrivateKeyDirect, seedSyntheticGithubSourceDirect, seedUniversalGitSourceDirect, seedWebhookSecretDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, showEnvironmentDirect, showExecutionRunDirect, showKiciEventDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, storeMigrationContentHashInTableDirect, tableExistsDirect, updateGenericWebhookVerificationConfigDirect, updateOrgSettingsDeniedReposDirect, updateSourceRoutingKeyDirect, updateWorkflowRegistrationCommitShaDirect, upsertCronLastFiredDirect, upsertOrgSettingsGlobalWorkflowsDirect, verifyKiciEventNotifyDirect, waitForEventLogRowByDeliveryIdDirect, waitForExecutionRunStatusSinceDirect, waitForLatestExecutionJobStatusDirect, waitForPlatformEventLogDistinctRoutedDirect, waitForPlatformExecutionRunStatusDirect, waitForPlatformRegistrationsDirect, waitForPostgresDirect, waitForRegistrationsByRoutingKeyDirect, waitForRegistrationsUpdatedAtAdvanceDirect, waitForRunCompletionDirect };
|
|
2610
|
+
export { MIGRATION_HASH_TABLE, PROVIDER_HASH_KEY, REGISTERABLE_TRIGGER_TYPES, apiKeyExistsDirect, bumpRegistryVersionDirect, bumpRegistryVersionSimpleDirect, cleanupExecutionRowsDirect, clearDispatchQueueDirect, clearPeerCredentialsRevokedAtByIdsDirect, computeMigrationsHash, countActivePeerCredentialsByInstanceDirect, countCronLastFiredDirect, countHeldRunsByRunIdDirect, countWebhookSourcesByConnectionIdDirect, createDbRole, createEnvironmentTemplateDirect, createJoinTokenDirect, createReadOnlyDbUser, deleteCronLastFiredDirect, deleteCrossRepoTrustDirect, deleteEnvironmentDirect, deleteExecutionRunsByWorkflowNameDirect, deleteGenericWebhookSourcesByNameDirect, deleteKiciEventsDirect, deleteOrgSettingsByCustomerIdDirect, deletePeerCredentialsByInstanceIdLikeDirect, deleteWorkflowRegistrationsDirect, describeTableColumnsDirect, dropAndCreateDatabase, dropDatabaseDirect, emitKiciEventDirect, ensureDatabase, ensureOrgOwnerMemberDirect, findAnyUserApiKeyIdDirect, getExecutionRunSecurityDirect, getGenericWebhookSourceByRoutingKeyDirect, getHeldRunByIdDirect, getPeerCredentialRevokedAtDirect, getRegistryVersionDirect, getWebhookSourceByRoutingKeyDirect, getWorkflowRegistrationByIdDirect, insertCronLastFiredNowDirect, insertCrossRepoTrustStrictDirect, insertKiciEventRawDirect, insertPeerCredentialExpiredDirect, insertWorkflowRegistrationRawDirect, insertWorkflowRegistrationStrictDirect, isSchemaCurrent, isSchemaCurrentFromFilesDirect, latestExecutionRunByStatusDirect, listActiveGenericWebhookSourcesDirect, listActivePeerCredentialsExcludingDirect, listCrossRepoTrustBySourceRoutingKeyDirect, listEnvironmentsDirect, listExecutionJobsDirect, listExecutionRunsDirect, listKiciEventsDirect, listQueueDirect, listRegistrationsByRoutingKeyDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, platformConnectionExistsDirect, pollKiciEventsDirect, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, readStoredMigrationContentHash, registerWorkflowManualDirect, resetRaftStateDirect, resolvePlatformWebhookSourceRoutingKeyDirect, restoreSoftDeletedGenericWebhookSourceDirect, seedApiKeyInlineDirect, seedCiSecurityFixturesDirect, seedCrossRepoTrustDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, seedGenericWebhookSourceDirect, seedSourcePrivateKeyDirect, seedSyntheticGithubSourceDirect, seedUniversalGitSourceDirect, seedWebhookSecretDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, showEnvironmentDirect, showExecutionRunDirect, showKiciEventDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, storeMigrationContentHashInTableDirect, tableExistsDirect, terminateIdleDbBackendsDirect, updateGenericWebhookVerificationConfigDirect, updateOrgSettingsDeniedReposDirect, updateSourceRoutingKeyDirect, updateWorkflowRegistrationCommitShaDirect, upsertCronLastFiredDirect, upsertOrgSettingsGlobalWorkflowsDirect, verifyKiciEventNotifyDirect, waitForEventLogRowByDeliveryIdDirect, waitForExecutionRunStatusSinceDirect, waitForLatestExecutionJobStatusDirect, waitForPlatformEventLogDistinctRoutedDirect, waitForPlatformExecutionRunStatusDirect, waitForPlatformRegistrationsDirect, waitForPostgresDirect, waitForRegistrationsByRoutingKeyDirect, waitForRegistrationsUpdatedAtAdvanceDirect, waitForRunCompletionDirect };
|
|
2560
2611
|
|
|
2561
2612
|
//# sourceMappingURL=db-admin.js.map
|
package/dist/db.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
2
|
import { Kysely } from 'kysely';
|
|
3
|
+
/** Where a pg connection error surfaced. */
|
|
4
|
+
export type PgPoolErrorSource = 'idle-pool' | 'client';
|
|
5
|
+
export interface CreatePoolOptions {
|
|
6
|
+
/** Extra pg.Pool config merged over the connection string (e.g. max, connectionTimeoutMillis). */
|
|
7
|
+
config?: Omit<pg.PoolConfig, 'connectionString'>;
|
|
8
|
+
/**
|
|
9
|
+
* Optional hook invoked after the built-in log line on every absorbed
|
|
10
|
+
* connection error (e.g. to increment a metrics counter). Additive — it
|
|
11
|
+
* never replaces the log.
|
|
12
|
+
*/
|
|
13
|
+
onError?: (err: Error, source: PgPoolErrorSource) => void;
|
|
14
|
+
}
|
|
3
15
|
/**
|
|
4
16
|
* Create PostgreSQL connection pool.
|
|
17
|
+
*
|
|
18
|
+
* Always attaches error handlers for both idle pooled clients (the pool's
|
|
19
|
+
* own 'error' event) and checked-out clients (per-client 'error' via the
|
|
20
|
+
* 'connect' hook). Without them, a terminated backend — e.g. a Postgres
|
|
21
|
+
* leader switchover — escalates to an uncaughtException and a full process
|
|
22
|
+
* restart. The broken connection is logged and discarded; pg replaces it on
|
|
23
|
+
* the next acquire. In-flight query failures still reject to their callers.
|
|
5
24
|
*/
|
|
6
|
-
export declare function createPool(databaseUrl: string): pg.Pool;
|
|
25
|
+
export declare function createPool(databaseUrl: string, options?: CreatePoolOptions): pg.Pool;
|
|
7
26
|
/**
|
|
8
27
|
* Create Kysely database instance (PostgreSQL only).
|
|
9
28
|
*
|
package/dist/db.js
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
import "./chunk-gOLHoazu.js";
|
|
2
2
|
import pg from "pg";
|
|
3
3
|
import { Kysely, PostgresDialect } from "kysely";
|
|
4
|
+
import { createLogger } from "@kici-dev/core";
|
|
4
5
|
//#region src/db.ts
|
|
6
|
+
let poolLogger;
|
|
7
|
+
function getPoolLogger() {
|
|
8
|
+
poolLogger ??= createLogger({ prefix: "pg-pool" });
|
|
9
|
+
return poolLogger;
|
|
10
|
+
}
|
|
5
11
|
/**
|
|
6
12
|
* Create PostgreSQL connection pool.
|
|
13
|
+
*
|
|
14
|
+
* Always attaches error handlers for both idle pooled clients (the pool's
|
|
15
|
+
* own 'error' event) and checked-out clients (per-client 'error' via the
|
|
16
|
+
* 'connect' hook). Without them, a terminated backend — e.g. a Postgres
|
|
17
|
+
* leader switchover — escalates to an uncaughtException and a full process
|
|
18
|
+
* restart. The broken connection is logged and discarded; pg replaces it on
|
|
19
|
+
* the next acquire. In-flight query failures still reject to their callers.
|
|
7
20
|
*/
|
|
8
|
-
function createPool(databaseUrl) {
|
|
9
|
-
|
|
21
|
+
function createPool(databaseUrl, options) {
|
|
22
|
+
const pool = new pg.Pool({
|
|
23
|
+
connectionString: databaseUrl,
|
|
24
|
+
...options?.config
|
|
25
|
+
});
|
|
26
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
27
|
+
const handle = (err, source) => {
|
|
28
|
+
if (seen.has(err)) return;
|
|
29
|
+
seen.add(err);
|
|
30
|
+
getPoolLogger().warn("Discarded broken pg connection", {
|
|
31
|
+
source,
|
|
32
|
+
error: err.message,
|
|
33
|
+
stack: err.stack
|
|
34
|
+
});
|
|
35
|
+
options?.onError?.(err, source);
|
|
36
|
+
};
|
|
37
|
+
pool.on("error", (err) => handle(err, "idle-pool"));
|
|
38
|
+
pool.on("connect", (client) => {
|
|
39
|
+
client.on("error", (err) => handle(err, "client"));
|
|
40
|
+
});
|
|
41
|
+
return pool;
|
|
10
42
|
}
|
|
11
43
|
/**
|
|
12
44
|
* Create Kysely database instance (PostgreSQL only).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared debug-bundle archive primitives.
|
|
3
|
+
*
|
|
4
|
+
* Allowlist config redaction and windowed log-file archiving, used by the
|
|
5
|
+
* orchestrator's in-process bundle writer, the kici-admin CLI's local bundle,
|
|
6
|
+
* and the agent's fleet mini-bundle assembler. Keeping one copy means a node
|
|
7
|
+
* cannot drift from the redaction posture of its peers.
|
|
8
|
+
*/
|
|
9
|
+
import type archiver from 'archiver';
|
|
10
|
+
/** Maximum total log bytes to include in bundle (50MB). */
|
|
11
|
+
export declare const MAX_LOG_BYTES: number;
|
|
12
|
+
/**
|
|
13
|
+
* Redact config values using allowlist approach.
|
|
14
|
+
* Only known-safe fields are preserved; everything else becomes "****".
|
|
15
|
+
*/
|
|
16
|
+
export declare function redactConfig(obj: unknown, parentKey?: string): unknown;
|
|
17
|
+
/**
|
|
18
|
+
* Add log files from logDir to the archive, respecting MAX_LOG_BYTES cap
|
|
19
|
+
* and the logWindow time filter. Matches any `*.log` file in the directory,
|
|
20
|
+
* so the per-instance filename pattern produced by
|
|
21
|
+
* `buildLogFilename()` is picked up without additional configuration.
|
|
22
|
+
*
|
|
23
|
+
* Exported for reuse by the `kici-admin debug-bundle` CLI command, which
|
|
24
|
+
* runs outside the orchestrator process but still needs to include the
|
|
25
|
+
* same log files in its locally-assembled bundle.
|
|
26
|
+
*/
|
|
27
|
+
export declare function addLogsToArchive(archive: archiver.Archiver, logDir: string, logWindowHours: number): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=bundle-archive.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import "../chunk-gOLHoazu.js";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
//#region src/diagnostics/bundle-archive.ts
|
|
5
|
+
/**
|
|
6
|
+
* Shared debug-bundle archive primitives.
|
|
7
|
+
*
|
|
8
|
+
* Allowlist config redaction and windowed log-file archiving, used by the
|
|
9
|
+
* orchestrator's in-process bundle writer, the kici-admin CLI's local bundle,
|
|
10
|
+
* and the agent's fleet mini-bundle assembler. Keeping one copy means a node
|
|
11
|
+
* cannot drift from the redaction posture of its peers.
|
|
12
|
+
*/
|
|
13
|
+
/** Maximum total log bytes to include in bundle (50MB). */
|
|
14
|
+
const MAX_LOG_BYTES = 50 * 1024 * 1024;
|
|
15
|
+
/**
|
|
16
|
+
* Config field names that are safe to include unredacted.
|
|
17
|
+
* Everything else gets replaced with "****".
|
|
18
|
+
*/
|
|
19
|
+
const SAFE_CONFIG_KEYS = new Set([
|
|
20
|
+
"mode",
|
|
21
|
+
"host",
|
|
22
|
+
"port",
|
|
23
|
+
"logLevel",
|
|
24
|
+
"region",
|
|
25
|
+
"environment",
|
|
26
|
+
"name",
|
|
27
|
+
"label",
|
|
28
|
+
"labels",
|
|
29
|
+
"enabled",
|
|
30
|
+
"disabled",
|
|
31
|
+
"timeout",
|
|
32
|
+
"interval",
|
|
33
|
+
"maxRetries",
|
|
34
|
+
"retries",
|
|
35
|
+
"workers",
|
|
36
|
+
"concurrency",
|
|
37
|
+
"maxConcurrency",
|
|
38
|
+
"batchSize",
|
|
39
|
+
"bufferSize",
|
|
40
|
+
"warmPool",
|
|
41
|
+
"cooldown",
|
|
42
|
+
"type",
|
|
43
|
+
"provider",
|
|
44
|
+
"scaler",
|
|
45
|
+
"driver",
|
|
46
|
+
"backend",
|
|
47
|
+
"protocol",
|
|
48
|
+
"scheme",
|
|
49
|
+
"path",
|
|
50
|
+
"basePath",
|
|
51
|
+
"metricsPath",
|
|
52
|
+
"healthPath",
|
|
53
|
+
"logFormat",
|
|
54
|
+
"logFile",
|
|
55
|
+
"logDir",
|
|
56
|
+
"dataDir",
|
|
57
|
+
"version",
|
|
58
|
+
"debug",
|
|
59
|
+
"verbose",
|
|
60
|
+
"quiet",
|
|
61
|
+
"tls",
|
|
62
|
+
"cors",
|
|
63
|
+
"rateLimiting",
|
|
64
|
+
"maxConnections",
|
|
65
|
+
"poolSize",
|
|
66
|
+
"minPool",
|
|
67
|
+
"maxPool",
|
|
68
|
+
"idleTimeout",
|
|
69
|
+
"connectTimeout",
|
|
70
|
+
"requestTimeout",
|
|
71
|
+
"shutdownTimeout",
|
|
72
|
+
"gracefulShutdown"
|
|
73
|
+
]);
|
|
74
|
+
/**
|
|
75
|
+
* Redact config values using allowlist approach.
|
|
76
|
+
* Only known-safe fields are preserved; everything else becomes "****".
|
|
77
|
+
*/
|
|
78
|
+
function redactConfig(obj, parentKey) {
|
|
79
|
+
if (obj === null || obj === void 0) return obj;
|
|
80
|
+
if (Array.isArray(obj)) return obj.map((item) => redactConfig(item, parentKey));
|
|
81
|
+
if (typeof obj === "object") {
|
|
82
|
+
const result = {};
|
|
83
|
+
for (const [key, value] of Object.entries(obj)) result[key] = redactConfig(value, key);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
if (typeof obj === "string" && parentKey && !SAFE_CONFIG_KEYS.has(parentKey)) return "****";
|
|
87
|
+
if (typeof obj === "number" || typeof obj === "boolean") return obj;
|
|
88
|
+
if (typeof obj === "string" && parentKey && SAFE_CONFIG_KEYS.has(parentKey)) return obj;
|
|
89
|
+
if (typeof obj === "string") return "****";
|
|
90
|
+
return obj;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Add log files from logDir to the archive, respecting MAX_LOG_BYTES cap
|
|
94
|
+
* and the logWindow time filter. Matches any `*.log` file in the directory,
|
|
95
|
+
* so the per-instance filename pattern produced by
|
|
96
|
+
* `buildLogFilename()` is picked up without additional configuration.
|
|
97
|
+
*
|
|
98
|
+
* Exported for reuse by the `kici-admin debug-bundle` CLI command, which
|
|
99
|
+
* runs outside the orchestrator process but still needs to include the
|
|
100
|
+
* same log files in its locally-assembled bundle.
|
|
101
|
+
*/
|
|
102
|
+
async function addLogsToArchive(archive, logDir, logWindowHours) {
|
|
103
|
+
const cutoff = Date.now() - logWindowHours * 60 * 60 * 1e3;
|
|
104
|
+
const entries = fs.readdirSync(logDir).filter((f) => {
|
|
105
|
+
if (!f.endsWith(".log")) return false;
|
|
106
|
+
return fs.statSync(path.join(logDir, f)).mtimeMs >= cutoff;
|
|
107
|
+
});
|
|
108
|
+
entries.sort((a, b) => {
|
|
109
|
+
const aStat = fs.statSync(path.join(logDir, a));
|
|
110
|
+
return fs.statSync(path.join(logDir, b)).mtimeMs - aStat.mtimeMs;
|
|
111
|
+
});
|
|
112
|
+
let totalBytes = 0;
|
|
113
|
+
let totalLines = 0;
|
|
114
|
+
let errors = 0;
|
|
115
|
+
let warnings = 0;
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const filePath = path.join(logDir, entry);
|
|
118
|
+
const stat = fs.statSync(filePath);
|
|
119
|
+
if (totalBytes + stat.size > 52428800) break;
|
|
120
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
121
|
+
archive.append(content, { name: `logs/${entry}` });
|
|
122
|
+
totalBytes += stat.size;
|
|
123
|
+
const lines = content.split("\n");
|
|
124
|
+
totalLines += lines.length;
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
if (/\berror\b/i.test(line)) errors++;
|
|
127
|
+
if (/\bwarn(ing)?\b/i.test(line)) warnings++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const summary = {
|
|
131
|
+
totalLines,
|
|
132
|
+
errors,
|
|
133
|
+
warnings,
|
|
134
|
+
totalBytes
|
|
135
|
+
};
|
|
136
|
+
archive.append(JSON.stringify(summary, null, 2), { name: "logs/summary.json" });
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
export { MAX_LOG_BYTES, addLogsToArchive, redactConfig };
|
|
140
|
+
|
|
141
|
+
//# sourceMappingURL=bundle-archive.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-agnostic chunked transfer for fleet bundle ZIPs.
|
|
3
|
+
*
|
|
4
|
+
* The WS frame cap (WS_MAX_PAYLOAD_BYTES = 25 MiB) forbids shipping a whole
|
|
5
|
+
* bundle in one frame, so a bundle Buffer is split into ordered base64 frames
|
|
6
|
+
* (~85 KiB raw each, matching the webhook-relay frame size) and reassembled by
|
|
7
|
+
* the receiver. Used on both the orchestrator-agent and peer channels.
|
|
8
|
+
*/
|
|
9
|
+
/** Raw bytes per chunk before base64 (matches webhook-relay's ~85 KiB frames). */
|
|
10
|
+
export declare const FLEET_CHUNK_BYTES: number;
|
|
11
|
+
export interface BundleChunkFrame {
|
|
12
|
+
seq: number;
|
|
13
|
+
dataB64: string;
|
|
14
|
+
isLast: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Split a Buffer into ordered base64 frames. An empty buffer yields one final frame. */
|
|
17
|
+
export declare function chunkBuffer(buf: Buffer, chunkBytes?: number): BundleChunkFrame[];
|
|
18
|
+
/**
|
|
19
|
+
* Correlation core for any "send a request, await a chunked response" channel.
|
|
20
|
+
*
|
|
21
|
+
* Key-agnostic so both the orchestrator-agent channel (keyed by requestId) and
|
|
22
|
+
* the peer channel (keyed by messageId) share one implementation. Pending
|
|
23
|
+
* requests reject on timeout, on an error frame, or on disconnect. No
|
|
24
|
+
* orchestrator-initiated request/response primitive existed before fleet
|
|
25
|
+
* collection; this is it.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ChunkRequestWaiter {
|
|
28
|
+
private pending;
|
|
29
|
+
/** Register a pending request `id` that rejects after `timeoutMs`. */
|
|
30
|
+
add(id: string, timeoutMs: number): Promise<Buffer>;
|
|
31
|
+
/** Accumulate a chunk; resolves the pending request on the final frame. */
|
|
32
|
+
onChunk(id: string, seq: number, dataB64: string, isLast: boolean): void;
|
|
33
|
+
/** Reject the pending request `id` with `message`. */
|
|
34
|
+
onError(id: string, message: string): void;
|
|
35
|
+
/** Reject every pending request — used when the underlying connection drops. */
|
|
36
|
+
rejectAll(reason: string): void;
|
|
37
|
+
}
|
|
38
|
+
/** Reassembles ordered frames into a Buffer. Throws on gaps/reordering. */
|
|
39
|
+
export declare class BundleChunkAssembler {
|
|
40
|
+
private parts;
|
|
41
|
+
private next;
|
|
42
|
+
private done;
|
|
43
|
+
/** Returns the assembled Buffer on the final frame, otherwise undefined. */
|
|
44
|
+
accept(seq: number, dataB64: string, isLast: boolean): Buffer | undefined;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=bundle-chunks.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import "../chunk-gOLHoazu.js";
|
|
2
|
+
//#region src/diagnostics/bundle-chunks.ts
|
|
3
|
+
/**
|
|
4
|
+
* Channel-agnostic chunked transfer for fleet bundle ZIPs.
|
|
5
|
+
*
|
|
6
|
+
* The WS frame cap (WS_MAX_PAYLOAD_BYTES = 25 MiB) forbids shipping a whole
|
|
7
|
+
* bundle in one frame, so a bundle Buffer is split into ordered base64 frames
|
|
8
|
+
* (~85 KiB raw each, matching the webhook-relay frame size) and reassembled by
|
|
9
|
+
* the receiver. Used on both the orchestrator-agent and peer channels.
|
|
10
|
+
*/
|
|
11
|
+
/** Raw bytes per chunk before base64 (matches webhook-relay's ~85 KiB frames). */
|
|
12
|
+
const FLEET_CHUNK_BYTES = 85 * 1024;
|
|
13
|
+
/** Split a Buffer into ordered base64 frames. An empty buffer yields one final frame. */
|
|
14
|
+
function chunkBuffer(buf, chunkBytes = FLEET_CHUNK_BYTES) {
|
|
15
|
+
const frames = [];
|
|
16
|
+
if (buf.length === 0) return [{
|
|
17
|
+
seq: 0,
|
|
18
|
+
dataB64: "",
|
|
19
|
+
isLast: true
|
|
20
|
+
}];
|
|
21
|
+
for (let offset = 0, seq = 0; offset < buf.length; offset += chunkBytes, seq++) {
|
|
22
|
+
const slice = buf.subarray(offset, Math.min(offset + chunkBytes, buf.length));
|
|
23
|
+
frames.push({
|
|
24
|
+
seq,
|
|
25
|
+
dataB64: slice.toString("base64"),
|
|
26
|
+
isLast: offset + chunkBytes >= buf.length
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return frames;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Correlation core for any "send a request, await a chunked response" channel.
|
|
33
|
+
*
|
|
34
|
+
* Key-agnostic so both the orchestrator-agent channel (keyed by requestId) and
|
|
35
|
+
* the peer channel (keyed by messageId) share one implementation. Pending
|
|
36
|
+
* requests reject on timeout, on an error frame, or on disconnect. No
|
|
37
|
+
* orchestrator-initiated request/response primitive existed before fleet
|
|
38
|
+
* collection; this is it.
|
|
39
|
+
*/
|
|
40
|
+
var ChunkRequestWaiter = class {
|
|
41
|
+
pending = /* @__PURE__ */ new Map();
|
|
42
|
+
/** Register a pending request `id` that rejects after `timeoutMs`. */
|
|
43
|
+
add(id, timeoutMs) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
this.pending.delete(id);
|
|
47
|
+
reject(/* @__PURE__ */ new Error(`chunk request ${id} timed out after ${timeoutMs}ms`));
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
this.pending.set(id, {
|
|
50
|
+
asm: new BundleChunkAssembler(),
|
|
51
|
+
resolve,
|
|
52
|
+
reject,
|
|
53
|
+
timer
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** Accumulate a chunk; resolves the pending request on the final frame. */
|
|
58
|
+
onChunk(id, seq, dataB64, isLast) {
|
|
59
|
+
const p = this.pending.get(id);
|
|
60
|
+
if (!p) return;
|
|
61
|
+
try {
|
|
62
|
+
const done = p.asm.accept(seq, dataB64, isLast);
|
|
63
|
+
if (done) {
|
|
64
|
+
clearTimeout(p.timer);
|
|
65
|
+
this.pending.delete(id);
|
|
66
|
+
p.resolve(done);
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
clearTimeout(p.timer);
|
|
70
|
+
this.pending.delete(id);
|
|
71
|
+
p.reject(err instanceof Error ? err : new Error(String(err)));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Reject the pending request `id` with `message`. */
|
|
75
|
+
onError(id, message) {
|
|
76
|
+
const p = this.pending.get(id);
|
|
77
|
+
if (!p) return;
|
|
78
|
+
clearTimeout(p.timer);
|
|
79
|
+
this.pending.delete(id);
|
|
80
|
+
p.reject(new Error(message));
|
|
81
|
+
}
|
|
82
|
+
/** Reject every pending request — used when the underlying connection drops. */
|
|
83
|
+
rejectAll(reason) {
|
|
84
|
+
for (const [, p] of this.pending) {
|
|
85
|
+
clearTimeout(p.timer);
|
|
86
|
+
p.reject(new Error(reason));
|
|
87
|
+
}
|
|
88
|
+
this.pending.clear();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/** Reassembles ordered frames into a Buffer. Throws on gaps/reordering. */
|
|
92
|
+
var BundleChunkAssembler = class {
|
|
93
|
+
parts = [];
|
|
94
|
+
next = 0;
|
|
95
|
+
done = false;
|
|
96
|
+
/** Returns the assembled Buffer on the final frame, otherwise undefined. */
|
|
97
|
+
accept(seq, dataB64, isLast) {
|
|
98
|
+
if (this.done) throw new Error("bundle chunk received after final frame");
|
|
99
|
+
if (seq !== this.next) throw new Error(`out-of-order bundle chunk: expected ${this.next}, got ${seq}`);
|
|
100
|
+
this.next++;
|
|
101
|
+
this.parts.push(Buffer.from(dataB64, "base64"));
|
|
102
|
+
if (isLast) {
|
|
103
|
+
this.done = true;
|
|
104
|
+
return Buffer.concat(this.parts);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
export { BundleChunkAssembler, ChunkRequestWaiter, FLEET_CHUNK_BYTES, chunkBuffer };
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=bundle-chunks.js.map
|
|
@@ -50,9 +50,21 @@ export interface ShutdownHandle {
|
|
|
50
50
|
/**
|
|
51
51
|
* Wire up SIGTERM / SIGINT (and optionally uncaughtException /
|
|
52
52
|
* unhandledRejection) handlers that execute the provided teardown
|
|
53
|
-
* steps sequentially, then
|
|
53
|
+
* steps sequentially, then exit.
|
|
54
|
+
*
|
|
55
|
+
* Exit codes: signal-triggered and programmatic shutdowns exit 0;
|
|
56
|
+
* shutdowns triggered by uncaughtException / unhandledRejection exit 1 so
|
|
57
|
+
* `Restart=on-failure`-style supervisor policies and exit-code alerting
|
|
58
|
+
* see the fatal cause. Escalation is sticky — a fatal trigger arriving
|
|
59
|
+
* while a clean shutdown is already in progress still raises the final
|
|
60
|
+
* exit code to 1.
|
|
54
61
|
*
|
|
55
62
|
* A force-exit timer ensures the process terminates even if a step hangs.
|
|
63
|
+
* The force-exit honors the same sticky exit code as a clean completion: a
|
|
64
|
+
* slow SIGTERM/SIGINT stop that merely overran the grace period still exits 0,
|
|
65
|
+
* so systemd records an intentional stop rather than marking the unit `failed`
|
|
66
|
+
* (which would break `systemctl restart` recovery). Only a fatal trigger
|
|
67
|
+
* (uncaughtException / unhandledRejection) raises the force-exit code to 1.
|
|
56
68
|
*/
|
|
57
69
|
export declare function setupGracefulShutdown(options: GracefulShutdownOptions): ShutdownHandle;
|
|
58
70
|
//# sourceMappingURL=graceful-shutdown.d.ts.map
|