@kici-dev/shared 0.1.13 → 0.1.15

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.
Files changed (47) hide show
  1. package/README.md +13 -1
  2. package/dist/cold-store/chunk-encoder.js +1 -1
  3. package/dist/cold-store/chunk-id.js +1 -1
  4. package/dist/cold-store/cold-store.js +1 -1
  5. package/dist/db-admin.d.ts +39 -0
  6. package/dist/db-admin.js +60 -5
  7. package/dist/db.d.ts +20 -1
  8. package/dist/db.js +34 -2
  9. package/dist/db.test.d.ts +2 -0
  10. package/dist/env/define-env.js +27 -6
  11. package/dist/env/logger-env.d.ts +1 -1
  12. package/dist/graceful-shutdown.d.ts +13 -1
  13. package/dist/graceful-shutdown.js +24 -10
  14. package/dist/idempotency.d.ts +1 -67
  15. package/dist/idempotency.js +2 -48
  16. package/dist/index.d.ts +3 -9
  17. package/dist/index.js +3 -9
  18. package/dist/package-manager-types.d.ts +1 -21
  19. package/dist/package-manager-types.js +2 -34
  20. package/dist/package-manager.d.ts +1 -51
  21. package/dist/package-manager.js +2 -130
  22. package/dist/s3-client.js +4 -1
  23. package/dist/s3-client.test.d.ts +2 -0
  24. package/dist/ts-loader-hook.d.ts +1 -25
  25. package/dist/ts-loader-hook.js +2 -47
  26. package/package.json +12 -7
  27. package/sbom.spdx.json +60 -5
  28. package/dist/crypto.d.ts +0 -33
  29. package/dist/crypto.js +0 -67
  30. package/dist/error.d.ts +0 -16
  31. package/dist/error.js +0 -58
  32. package/dist/error.test.d.ts +0 -2
  33. package/dist/format-bytes.d.ts +0 -5
  34. package/dist/format-bytes.js +0 -15
  35. package/dist/format-bytes.test.d.ts +0 -2
  36. package/dist/format-duration.d.ts +0 -11
  37. package/dist/format-duration.js +0 -32
  38. package/dist/format-duration.test.d.ts +0 -2
  39. package/dist/idempotency.test.d.ts +0 -2
  40. package/dist/logger.d.ts +0 -60
  41. package/dist/logger.js +0 -181
  42. package/dist/logger.test.d.ts +0 -2
  43. package/dist/package-manager.test.d.ts +0 -2
  44. package/dist/request-context.d.ts +0 -42
  45. package/dist/request-context.js +0 -37
  46. package/dist/zx.d.ts +0 -8
  47. package/dist/zx.js +0 -78
package/README.md CHANGED
@@ -1 +1,13 @@
1
- TBD
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
@@ -1,5 +1,5 @@
1
1
  import "../chunk-gOLHoazu.js";
2
- import { sha256 } from "../crypto.js";
2
+ import { sha256 } from "@kici-dev/core";
3
3
  import { gzipSync } from "node:zlib";
4
4
  //#region src/cold-store/chunk-encoder.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import "../chunk-gOLHoazu.js";
2
- import { sha256 } from "../crypto.js";
2
+ import { sha256 } from "@kici-dev/core";
3
3
  //#region src/cold-store/chunk-id.ts
4
4
  /**
5
5
  * Deterministic chunk ID computation.
@@ -1,5 +1,4 @@
1
1
  import "../chunk-gOLHoazu.js";
2
- import { sha256 } from "../crypto.js";
3
2
  import { createS3Client } from "../s3-client.js";
4
3
  import { chunkObjectKey, encodeKeySegment, tablePrefix, tenantDayPrefix } from "./key.js";
5
4
  import { coldDaysToBucket, isLongerColdRetention } from "./bucket.js";
@@ -7,6 +6,7 @@ import { computeChunkId } from "./chunk-id.js";
7
6
  import { decodeChunk, encodeChunk } from "./chunk-encoder.js";
8
7
  import { parseManifest, serializeManifest } from "./manifest.js";
9
8
  import { coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./metrics.js";
9
+ import { sha256 } from "@kici-dev/core";
10
10
  import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
11
11
  //#region src/cold-store/cold-store.ts
12
12
  /**
@@ -60,6 +60,16 @@ export interface EnsureDatabaseOpts {
60
60
  * grant is a no-op).
61
61
  */
62
62
  revokeConnectFromPublic?: boolean;
63
+ /**
64
+ * After creating (or finding) the database — and after the optional
65
+ * `REVOKE CONNECT … FROM PUBLIC` — `GRANT CONNECT ON DATABASE "<name>"
66
+ * TO "<role>"` for each role here. Pairs with `revokeConnectFromPublic`
67
+ * to re-grant CONNECT to the specific non-PUBLIC roles that legitimately
68
+ * need it once the default PUBLIC grant is revoked. Idempotent (GRANT on
69
+ * an already-present grant is a no-op). Each name is validated as a SQL
70
+ * identifier before interpolation.
71
+ */
72
+ grantConnectToRoles?: string[];
63
73
  }
64
74
  /**
65
75
  * CREATE DATABASE IF NOT EXISTS (idempotent). With no `opts`, the URL's
@@ -177,6 +187,7 @@ export interface SeedEnvironmentOpts {
177
187
  waitTimerSeconds?: number | null;
178
188
  holdExpirySeconds?: number | null;
179
189
  minimumTrust?: string | null;
190
+ globPattern?: string | null;
180
191
  }
181
192
  export interface SeedEnvironmentResult {
182
193
  envId: string;
@@ -188,6 +199,24 @@ export interface SeedEnvironmentResult {
188
199
  * are JSON-serialised server-side; pass them as plain arrays or objects.
189
200
  */
190
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
+ }>;
191
220
  export interface SeedEnvironmentBindingOpts {
192
221
  orgId: string;
193
222
  envName: string;
@@ -209,6 +238,7 @@ export interface SetEnvironmentPolicyOpts {
209
238
  holdExpirySeconds?: number | null;
210
239
  minimumTrust?: string | null;
211
240
  enabled?: boolean;
241
+ allowLocalExecution?: boolean;
212
242
  }
213
243
  /**
214
244
  * UPDATE only the policy fields that were explicitly provided. Columns that
@@ -1452,5 +1482,14 @@ export declare function clearPeerCredentialsRevokedAtByIdsDirect(databaseUrl: st
1452
1482
  export declare function countActivePeerCredentialsByInstanceDirect(databaseUrl: string, opts: {
1453
1483
  instanceId: string;
1454
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>;
1455
1494
  export {};
1456
1495
  //# sourceMappingURL=db-admin.d.ts.map
package/dist/db-admin.js CHANGED
@@ -111,6 +111,10 @@ async function ensureDatabase(databaseUrl, opts = {}) {
111
111
  outcome = "created";
112
112
  }
113
113
  if (opts.revokeConnectFromPublic) await pool.query(`REVOKE CONNECT ON DATABASE "${dbName}" FROM PUBLIC`);
114
+ for (const role of opts.grantConnectToRoles ?? []) {
115
+ assertValidIdentifier(role, "grant-connect role");
116
+ await pool.query(`GRANT CONNECT ON DATABASE "${dbName}" TO "${role}"`);
117
+ }
114
118
  return outcome;
115
119
  });
116
120
  }
@@ -347,7 +351,8 @@ const ENV_POLICY_COLUMNS = new Set([
347
351
  "wait_timer_seconds",
348
352
  "hold_expiry_seconds",
349
353
  "minimum_trust",
350
- "enabled"
354
+ "enabled",
355
+ "allow_local_execution"
351
356
  ]);
352
357
  /**
353
358
  * Upsert an environment row keyed by (org_id, name). Returns the env id and
@@ -366,9 +371,9 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
366
371
  const reviewersJson = opts.requiredReviewers === void 0 ? null : JSON.stringify(opts.requiredReviewers);
367
372
  const row = (await pool.query(`INSERT INTO environments
368
373
  (org_id, name, type, enabled, branch_restrictions, required_reviewers,
369
- wait_timer_seconds, hold_expiry_seconds, minimum_trust)
374
+ wait_timer_seconds, hold_expiry_seconds, minimum_trust, glob_pattern)
370
375
  VALUES ($1, $2, COALESCE($3, 'fixed'), COALESCE($4, true), $5::jsonb, $6::jsonb,
371
- $7, COALESCE($8, 86400), $9)
376
+ $7, COALESCE($8, 86400), $9, $10)
372
377
  ON CONFLICT (org_id, name) DO UPDATE SET
373
378
  type = COALESCE(EXCLUDED.type, environments.type),
374
379
  enabled = EXCLUDED.enabled,
@@ -377,6 +382,7 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
377
382
  wait_timer_seconds = EXCLUDED.wait_timer_seconds,
378
383
  hold_expiry_seconds = EXCLUDED.hold_expiry_seconds,
379
384
  minimum_trust = EXCLUDED.minimum_trust,
385
+ glob_pattern = COALESCE(EXCLUDED.glob_pattern, environments.glob_pattern),
380
386
  updated_at = now()
381
387
  RETURNING id, (xmax = 0) AS inserted`, [
382
388
  opts.orgId,
@@ -387,7 +393,8 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
387
393
  reviewersJson,
388
394
  opts.waitTimerSeconds ?? null,
389
395
  opts.holdExpirySeconds ?? null,
390
- opts.minimumTrust ?? null
396
+ opts.minimumTrust ?? null,
397
+ opts.globPattern ?? null
391
398
  ])).rows[0];
392
399
  if (!row) throw new Error(`environment: upsert returned no row for ${opts.name}`);
393
400
  return {
@@ -399,6 +406,33 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
399
406
  }
400
407
  }
401
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
+ /**
402
436
  * Upsert an `environment_bindings` row connecting `envName` to `scopePattern`.
403
437
  * Throws if the environment does not exist.
404
438
  */
@@ -445,6 +479,7 @@ async function setEnvironmentPolicyDirect(databaseUrl, opts) {
445
479
  if (opts.holdExpirySeconds !== void 0) addSet("hold_expiry_seconds", opts.holdExpirySeconds);
446
480
  if (opts.minimumTrust !== void 0) addSet("minimum_trust", opts.minimumTrust);
447
481
  if (opts.enabled !== void 0) addSet("enabled", opts.enabled);
482
+ if (opts.allowLocalExecution !== void 0) addSet("allow_local_execution", opts.allowLocalExecution);
448
483
  if (setClauses.length === 0) throw new Error("environment: setEnvironmentPolicy requires at least one policy field");
449
484
  const pool = new pg.Pool({
450
485
  connectionString: databaseUrl,
@@ -2551,7 +2586,27 @@ async function countActivePeerCredentialsByInstanceDirect(databaseUrl, opts) {
2551
2586
  await pool.end();
2552
2587
  }
2553
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
+ }
2554
2609
  //#endregion
2555
- 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 };
2556
2611
 
2557
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
- return new pg.Pool({ connectionString: databaseUrl });
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=db.test.d.ts.map
@@ -60,9 +60,11 @@ function extractDefault(field) {
60
60
  const def = t.def;
61
61
  if (!def) return void 0;
62
62
  if (def.type === "default" && def.defaultValue !== void 0) {
63
- const v = typeof def.defaultValue === "function" ? "<computed>" : def.defaultValue;
64
- if (v === "" || v === void 0) return void 0;
65
- return JSON.stringify(v);
63
+ const probe1 = def.defaultValue;
64
+ const probe2 = def.defaultValue;
65
+ if (typeof probe1 === "function" || probe1 !== probe2) return "\"<computed>\"";
66
+ if (probe1 === "" || probe1 === void 0) return void 0;
67
+ return JSON.stringify(probe1);
66
68
  }
67
69
  if (def.in) {
68
70
  t = def.in;
@@ -80,7 +82,7 @@ function isOptional(field) {
80
82
  for (let i = 0; i < 6; i++) {
81
83
  const def = t.def;
82
84
  if (!def) return false;
83
- if (def.type === "optional" || def.type === "default") return true;
85
+ if (def.type === "optional" || def.type === "default" || def.type === "prefault") return true;
84
86
  if (def.in) {
85
87
  t = def.in;
86
88
  continue;
@@ -93,6 +95,26 @@ function isOptional(field) {
93
95
  }
94
96
  return false;
95
97
  }
98
+ /**
99
+ * Walk through `.optional()` / `.default(...)` / `.prefault(...)` wrappers to
100
+ * find a nested ZodObject's `.shape`. Returns undefined if `field` does not
101
+ * (eventually) wrap a ZodObject. Mirrors the unwrap loop in
102
+ * `extractDefault` / `isOptional` so that a nested object can be wrapped in
103
+ * any of the common compositional modifiers and still be walked for docs.
104
+ */
105
+ function findInnerShape(field) {
106
+ let t = field;
107
+ for (let i = 0; i < 8; i++) {
108
+ const node = t;
109
+ if (node.shape) return node.shape;
110
+ if (node.def?.shape) return node.def.shape;
111
+ if (node.def?.innerType) {
112
+ t = node.def.innerType;
113
+ continue;
114
+ }
115
+ return;
116
+ }
117
+ }
96
118
  function describeFieldRecursive(shape, envMap, fieldPath, descriptions, out) {
97
119
  for (const [name, field] of Object.entries(shape)) {
98
120
  const path = fieldPath ? `${fieldPath}.${name}` : name;
@@ -113,8 +135,7 @@ function describeFieldRecursive(shape, envMap, fieldPath, descriptions, out) {
113
135
  description: explicitDesc ?? zodDesc ?? def2?.description
114
136
  });
115
137
  } else {
116
- const nested = field;
117
- const innerShape = nested.shape ?? nested.def?.shape;
138
+ const innerShape = findInnerShape(field);
118
139
  if (innerShape) describeFieldRecursive(innerShape, mapping, path, descriptions, out);
119
140
  }
120
141
  }
@@ -20,9 +20,9 @@ export declare const LoggerEnvSchema: z.ZodObject<{
20
20
  KICI_LOG_MAX_SIZE: z.ZodDefault<z.ZodString>;
21
21
  KICI_LOG_RETENTION_DAYS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
22
22
  KICI_LOG_FORMAT: z.ZodDefault<z.ZodEnum<{
23
- plain: "plain";
24
23
  json: "json";
25
24
  auto: "auto";
25
+ plain: "plain";
26
26
  }>>;
27
27
  KICI_CLUSTER_INSTANCE_ID: z.ZodOptional<z.ZodString>;
28
28
  KICI_AGENT_ID: z.ZodOptional<z.ZodString>;
@@ -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 `process.exit(0)`.
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
@@ -1,17 +1,31 @@
1
1
  import "./chunk-gOLHoazu.js";
2
- import { toErrorMessage } from "./error.js";
2
+ import { toErrorMessage } from "@kici-dev/core";
3
3
  //#region src/graceful-shutdown.ts
4
4
  /**
5
5
  * Wire up SIGTERM / SIGINT (and optionally uncaughtException /
6
6
  * unhandledRejection) handlers that execute the provided teardown
7
- * steps sequentially, then `process.exit(0)`.
7
+ * steps sequentially, then exit.
8
+ *
9
+ * Exit codes: signal-triggered and programmatic shutdowns exit 0;
10
+ * shutdowns triggered by uncaughtException / unhandledRejection exit 1 so
11
+ * `Restart=on-failure`-style supervisor policies and exit-code alerting
12
+ * see the fatal cause. Escalation is sticky — a fatal trigger arriving
13
+ * while a clean shutdown is already in progress still raises the final
14
+ * exit code to 1.
8
15
  *
9
16
  * A force-exit timer ensures the process terminates even if a step hangs.
17
+ * The force-exit honors the same sticky exit code as a clean completion: a
18
+ * slow SIGTERM/SIGINT stop that merely overran the grace period still exits 0,
19
+ * so systemd records an intentional stop rather than marking the unit `failed`
20
+ * (which would break `systemctl restart` recovery). Only a fatal trigger
21
+ * (uncaughtException / unhandledRejection) raises the force-exit code to 1.
10
22
  */
11
23
  function setupGracefulShutdown(options) {
12
24
  const { logger, steps, timeoutMs = 3e4, onForceExit, skipErrorHandlers = false } = options;
13
25
  let isShuttingDown = false;
14
- async function gracefulShutdown(signal) {
26
+ let exitCode = 0;
27
+ async function gracefulShutdown(signal, code = 0) {
28
+ exitCode = Math.max(exitCode, code);
15
29
  if (isShuttingDown) {
16
30
  logger.warn("Shutdown already in progress, ignoring signal", { signal });
17
31
  return;
@@ -19,8 +33,8 @@ function setupGracefulShutdown(options) {
19
33
  isShuttingDown = true;
20
34
  logger.info(`Received ${signal}, starting graceful shutdown...`);
21
35
  const forceExitTimeout = setTimeout(() => {
22
- logger.error(`Graceful shutdown timed out after ${timeoutMs / 1e3}s, forcing exit`);
23
- if (onForceExit?.() !== true) process.exit(1);
36
+ logger.error(`Graceful shutdown timed out after ${timeoutMs / 1e3}s, forcing exit`, { exitCode });
37
+ if (onForceExit?.() !== true) process.exit(exitCode);
24
38
  }, timeoutMs);
25
39
  try {
26
40
  for (const step of steps) try {
@@ -34,8 +48,8 @@ function setupGracefulShutdown(options) {
34
48
  });
35
49
  }
36
50
  clearTimeout(forceExitTimeout);
37
- logger.info("Graceful shutdown complete");
38
- process.exit(0);
51
+ logger.info("Graceful shutdown complete", { exitCode });
52
+ process.exit(exitCode);
39
53
  } catch (error) {
40
54
  logger.error("Error during graceful shutdown", {
41
55
  error: toErrorMessage(error),
@@ -53,17 +67,17 @@ function setupGracefulShutdown(options) {
53
67
  error: error.message,
54
68
  stack: error.stack
55
69
  });
56
- gracefulShutdown("uncaughtException");
70
+ gracefulShutdown("uncaughtException", 1);
57
71
  });
58
72
  process.on("unhandledRejection", (reason) => {
59
73
  logger.error("Unhandled rejection", {
60
74
  reason: toErrorMessage(reason),
61
75
  stack: reason instanceof Error ? reason.stack : void 0
62
76
  });
63
- gracefulShutdown("unhandledRejection");
77
+ gracefulShutdown("unhandledRejection", 1);
64
78
  });
65
79
  }
66
- return { shutdown: gracefulShutdown };
80
+ return { shutdown: (signal) => gracefulShutdown(signal) };
67
81
  }
68
82
  //#endregion
69
83
  export { setupGracefulShutdown };
@@ -1,68 +1,2 @@
1
- /**
2
- * Idempotent-step primitive: a check / prompt / apply runner that is
3
- * UI-agnostic and embeddable across the product.
4
- *
5
- * The pattern: every destructive operation on shared state (prod infra,
6
- * npm, git remotes, DNS, TF state, workflow step side effects) is wrapped
7
- * as an IdempotentStep whose check() returns a typed drift value or null.
8
- * Null means the system is already in the desired state — the runner
9
- * silently skips, optionally invoking whenInSync() to surface the
10
- * already-satisfied resource (e.g. an existing resource id). A non-null
11
- * drift means apply() would change state — the runner asks the caller's
12
- * confirm() before invoking apply(), unless yes or dryRun overrides are
13
- * set. apply() returns the typed result of the change for the caller.
14
- *
15
- * The runner has no UI dependency. CLI consumers pass an inquirer-backed
16
- * confirm; future SDK / agent consumers pass their own policy function.
17
- * See `.claude/rules/idempotency.md` for the full rule and adopters.
18
- */
19
- export interface IdempotentStep<TDrift, TInSync = void, TApplied = void> {
20
- /** Human-readable name; appears in logs and the confirm prompt. */
21
- name: string;
22
- /** Read-only inspection. Returns drift value if apply() would change
23
- * state, or null if the system is already in the desired state. */
24
- check: () => Promise<TDrift | null>;
25
- /** Multi-line description of what apply() would do, given drift. */
26
- summarize: (drift: TDrift) => string;
27
- /** Destructive action that brings the system into the desired state.
28
- * Its return value is surfaced in StepResult.result on the 'applied'
29
- * outcome. */
30
- apply: (drift: TDrift) => Promise<TApplied>;
31
- /** Optional: runs when check() returns null. Use this to fetch the
32
- * already-satisfied resource (e.g. read the existing id when a
33
- * create-if-missing was already done). Return value is surfaced in
34
- * StepResult.result on the 'skipped' outcome. */
35
- whenInSync?: () => Promise<TInSync>;
36
- }
37
- export type ConfirmFn = (message: string) => Promise<boolean>;
38
- export interface RunOptions {
39
- /** Pluggable confirm. Required unless `yes` or `dryRun` is set. */
40
- confirm?: ConfirmFn;
41
- /** Breakglass: skip prompts, apply on drift. The CALLER prints any
42
- * loud "auto-confirm" banner before invoking the runner. */
43
- yes?: boolean;
44
- /** Report only; bypass confirm; never call apply(). */
45
- dryRun?: boolean;
46
- /** Sink for `name`-prefixed status lines. Defaults to console.log. */
47
- log?: (line: string) => void;
48
- }
49
- export type StepOutcome = 'skipped' | 'applied' | 'declined' | 'dry-run';
50
- export type StepResult<TDrift, TInSync = void, TApplied = void> = {
51
- outcome: 'skipped';
52
- drift: null;
53
- result: TInSync;
54
- } | {
55
- outcome: 'applied';
56
- drift: TDrift;
57
- result: TApplied;
58
- } | {
59
- outcome: 'declined';
60
- drift: TDrift;
61
- result: undefined;
62
- } | {
63
- outcome: 'dry-run';
64
- drift: TDrift;
65
- result: undefined;
66
- };
67
- export declare function runIdempotentStep<TDrift, TInSync = void, TApplied = void>(step: IdempotentStep<TDrift, TInSync, TApplied>, opts?: RunOptions): Promise<StepResult<TDrift, TInSync, TApplied>>;
1
+ export * from '@kici-dev/core/idempotency';
68
2
  //# sourceMappingURL=idempotency.d.ts.map
@@ -1,49 +1,3 @@
1
1
  import "./chunk-gOLHoazu.js";
2
- //#region src/idempotency.ts
3
- async function runIdempotentStep(step, opts = {}) {
4
- const log = opts.log ?? ((line) => console.log(line));
5
- const drift = await step.check();
6
- if (drift === null) {
7
- log(`✓ ${step.name} — in sync, skipping`);
8
- return {
9
- outcome: "skipped",
10
- drift: null,
11
- result: step.whenInSync ? await step.whenInSync() : void 0
12
- };
13
- }
14
- log(`! ${step.name} — drift detected:`);
15
- for (const line of step.summarize(drift).split("\n")) log(` ${line}`);
16
- if (opts.dryRun) {
17
- log(` (dry-run; would apply)`);
18
- return {
19
- outcome: "dry-run",
20
- drift,
21
- result: void 0
22
- };
23
- }
24
- let approved;
25
- if (opts.yes) approved = true;
26
- else {
27
- if (!opts.confirm) throw new Error(`runIdempotentStep(${step.name}): drift detected but no confirm callback provided and yes/dryRun not set. Pass opts.confirm, opts.yes, or opts.dryRun.`);
28
- approved = await opts.confirm(`Apply ${step.name}?`);
29
- }
30
- if (!approved) {
31
- log(` declined; skipping`);
32
- return {
33
- outcome: "declined",
34
- drift,
35
- result: void 0
36
- };
37
- }
38
- const appliedResult = await step.apply(drift);
39
- log(`✓ ${step.name} — applied`);
40
- return {
41
- outcome: "applied",
42
- drift,
43
- result: appliedResult
44
- };
45
- }
46
- //#endregion
47
- export { runIdempotentStep };
48
-
49
- //# sourceMappingURL=idempotency.js.map
2
+ export * from "@kici-dev/core/idempotency";
3
+ export {};