@kici-dev/shared 0.1.3 → 0.1.5
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/dist/db-admin.d.ts +25 -3
- package/dist/db-admin.js +20 -11
- package/dist/db-collation.d.ts +60 -0
- package/dist/db-collation.js +52 -0
- package/dist/db-collation.test.d.ts +2 -0
- package/dist/env/allowlist.d.ts +6 -6
- package/dist/env/allowlist.js +7 -7
- package/dist/env/logger-env.d.ts +6 -1
- package/dist/env/logger-env.js +21 -0
- package/dist/idempotency-env-diff.d.ts +83 -0
- package/dist/idempotency-env-diff.js +163 -0
- package/dist/idempotency-env-diff.test.d.ts +2 -0
- package/dist/idempotency-files.d.ts +71 -0
- package/dist/idempotency-files.js +73 -6
- package/dist/idempotency-yaml-diff.d.ts +74 -0
- package/dist/idempotency-yaml-diff.js +241 -0
- package/dist/idempotency-yaml-diff.test.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/logger.d.ts +4 -1
- package/dist/logger.js +23 -17
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.js +2 -2
- package/dist/telemetry/init.d.ts +10 -0
- package/dist/telemetry/init.js +32 -1
- package/dist/tool-check.js +31 -11
- package/package.json +14 -1
- package/sbom.spdx.json +10 -5
package/dist/db-admin.d.ts
CHANGED
|
@@ -41,10 +41,32 @@ export declare function dropDatabaseDirect(databaseUrl: string): Promise<void>;
|
|
|
41
41
|
* existing backend connections so the DROP doesn't block.
|
|
42
42
|
*/
|
|
43
43
|
export declare function dropAndCreateDatabase(databaseUrl: string): Promise<void>;
|
|
44
|
+
export interface EnsureDatabaseOpts {
|
|
45
|
+
/**
|
|
46
|
+
* DB owner role. Defaults to the URL's username. Pass when the admin
|
|
47
|
+
* connection user is privileged (e.g. Patroni superuser) but the new
|
|
48
|
+
* database should be owned by a separate, non-privileged role — the
|
|
49
|
+
* cross-owner case for provisioning shared-cluster databases like
|
|
50
|
+
* Keycloak's on the Platform Patroni cluster.
|
|
51
|
+
*/
|
|
52
|
+
owner?: string;
|
|
53
|
+
/**
|
|
54
|
+
* After creating (or finding) the database, run
|
|
55
|
+
* `REVOKE CONNECT ON DATABASE "<name>" FROM PUBLIC`. Recommended for
|
|
56
|
+
* shared Patroni clusters where multiple unrelated databases coexist
|
|
57
|
+
* (Platform + Keycloak today, more tomorrow) — the default `PUBLIC`
|
|
58
|
+
* CONNECT grant otherwise lets any role with LOGIN on the cluster
|
|
59
|
+
* reach the new database. Idempotent (REVOKE on an already-revoked
|
|
60
|
+
* grant is a no-op).
|
|
61
|
+
*/
|
|
62
|
+
revokeConnectFromPublic?: boolean;
|
|
63
|
+
}
|
|
44
64
|
/**
|
|
45
|
-
* CREATE DATABASE IF NOT EXISTS (idempotent).
|
|
65
|
+
* CREATE DATABASE IF NOT EXISTS (idempotent). With no `opts`, the URL's
|
|
66
|
+
* username is the owner. Pass `owner` to override and
|
|
67
|
+
* `revokeConnectFromPublic` to lock down the default CONNECT grant.
|
|
46
68
|
*/
|
|
47
|
-
export declare function ensureDatabase(databaseUrl: string): Promise<'created' | 'exists'>;
|
|
69
|
+
export declare function ensureDatabase(databaseUrl: string, opts?: EnsureDatabaseOpts): Promise<'created' | 'exists'>;
|
|
48
70
|
/**
|
|
49
71
|
* CREATE ROLE ... LOGIN [CREATEDB] (idempotent — updates password if role
|
|
50
72
|
* already exists).
|
|
@@ -1350,7 +1372,7 @@ export declare function resolvePlatformWebhookSourceRoutingKeyDirect(platformDbU
|
|
|
1350
1372
|
*/
|
|
1351
1373
|
export declare function ensureOrgOwnerMemberDirect(platformDbUrl: string, opts: {
|
|
1352
1374
|
orgId: string;
|
|
1353
|
-
|
|
1375
|
+
idpSub: string;
|
|
1354
1376
|
email: string;
|
|
1355
1377
|
displayName: string;
|
|
1356
1378
|
}): Promise<{
|
package/dist/db-admin.js
CHANGED
|
@@ -93,16 +93,25 @@ async function dropAndCreateDatabase(databaseUrl) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
/**
|
|
96
|
-
* CREATE DATABASE IF NOT EXISTS (idempotent).
|
|
96
|
+
* CREATE DATABASE IF NOT EXISTS (idempotent). With no `opts`, the URL's
|
|
97
|
+
* username is the owner. Pass `owner` to override and
|
|
98
|
+
* `revokeConnectFromPublic` to lock down the default CONNECT grant.
|
|
97
99
|
*/
|
|
98
|
-
async function ensureDatabase(databaseUrl) {
|
|
100
|
+
async function ensureDatabase(databaseUrl, opts = {}) {
|
|
99
101
|
const { adminUrl, dbName, dbOwner } = parseDatabaseUrl(databaseUrl);
|
|
102
|
+
const finalOwner = opts.owner ?? dbOwner;
|
|
100
103
|
assertValidIdentifier(dbName, "database name");
|
|
101
|
-
assertValidIdentifier(
|
|
104
|
+
assertValidIdentifier(finalOwner, "database owner");
|
|
102
105
|
return withAdminPool(adminUrl, async (pool) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
const check = await pool.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]);
|
|
107
|
+
let outcome;
|
|
108
|
+
if (check.rows.length > 0) outcome = "exists";
|
|
109
|
+
else {
|
|
110
|
+
await pool.query(`CREATE DATABASE "${dbName}" OWNER "${finalOwner}"`);
|
|
111
|
+
outcome = "created";
|
|
112
|
+
}
|
|
113
|
+
if (opts.revokeConnectFromPublic) await pool.query(`REVOKE CONNECT ON DATABASE "${dbName}" FROM PUBLIC`);
|
|
114
|
+
return outcome;
|
|
106
115
|
});
|
|
107
116
|
}
|
|
108
117
|
/**
|
|
@@ -2402,19 +2411,19 @@ async function ensureOrgOwnerMemberDirect(platformDbUrl, opts) {
|
|
|
2402
2411
|
const ownerRole = await pool.query(`SELECT id FROM roles WHERE org_id = $1 AND is_owner = true LIMIT 1`, [opts.orgId]);
|
|
2403
2412
|
if (ownerRole.rows.length === 0) throw new Error(`ensureOrgOwnerMemberDirect: no owner role found for org ${opts.orgId}`);
|
|
2404
2413
|
const ownerRoleId = ownerRole.rows[0].id;
|
|
2405
|
-
await pool.query(`INSERT INTO users (
|
|
2414
|
+
await pool.query(`INSERT INTO users (idp_sub, email, display_name)
|
|
2406
2415
|
VALUES ($1, $2, $3)
|
|
2407
|
-
ON CONFLICT (
|
|
2408
|
-
opts.
|
|
2416
|
+
ON CONFLICT (idp_sub) DO NOTHING`, [
|
|
2417
|
+
opts.idpSub,
|
|
2409
2418
|
opts.email,
|
|
2410
2419
|
opts.displayName
|
|
2411
2420
|
]);
|
|
2412
|
-
await pool.query(`INSERT INTO org_members (org_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [opts.orgId, opts.
|
|
2421
|
+
await pool.query(`INSERT INTO org_members (org_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [opts.orgId, opts.idpSub]);
|
|
2413
2422
|
await pool.query(`INSERT INTO role_assignments (org_id, user_id, role_id, assigned_by)
|
|
2414
2423
|
VALUES ($1, $2, $3, $2)
|
|
2415
2424
|
ON CONFLICT (org_id, user_id, role_id) DO NOTHING`, [
|
|
2416
2425
|
opts.orgId,
|
|
2417
|
-
opts.
|
|
2426
|
+
opts.idpSub,
|
|
2418
2427
|
ownerRoleId
|
|
2419
2428
|
]);
|
|
2420
2429
|
return { ownerRoleId };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
/**
|
|
3
|
+
* Collation-drift helpers shared between `kici-admin` (orchestrator DB) and
|
|
4
|
+
* `kici-platform-admin` (Platform DB).
|
|
5
|
+
*
|
|
6
|
+
* Background: `pg_database.datcollversion` records the libc collation version
|
|
7
|
+
* at database-bootstrap time. B-tree indexes on text columns were built under
|
|
8
|
+
* those collation rules. When the running Postgres process's libc collation
|
|
9
|
+
* version differs (commonly after a container image rebuild on a newer libc
|
|
10
|
+
* base), indexes can silently misindex non-ASCII data — `LIKE 'foo%'` may miss
|
|
11
|
+
* rows, `ORDER BY` becomes unstable, rule-equivalent duplicates can sneak past
|
|
12
|
+
* unique constraints.
|
|
13
|
+
*
|
|
14
|
+
* Detection compares `datcollversion` (the version stamped at bootstrap)
|
|
15
|
+
* against `pg_database_collation_actual_version(oid)` (the version the running
|
|
16
|
+
* libc reports). Healing is a two-step operator action:
|
|
17
|
+
*
|
|
18
|
+
* 1. `REINDEX DATABASE CONCURRENTLY <name>` — rebuild every index under the
|
|
19
|
+
* running libc rules. Non-blocking (short locks per index); needs ~2× temp
|
|
20
|
+
* disk while parallel indexes coexist.
|
|
21
|
+
* 2. `ALTER DATABASE <name> REFRESH COLLATION VERSION` — bump the metadata
|
|
22
|
+
* stamp so future probes report clean.
|
|
23
|
+
*
|
|
24
|
+
* All callers pass a `pg.Pool`. The helpers do not own pool lifecycle — the
|
|
25
|
+
* CLI / probe layer creates and ends pools using the standard
|
|
26
|
+
* `createPool` / `pool.end()` pattern from `db.js`.
|
|
27
|
+
*/
|
|
28
|
+
export interface CollationDrift {
|
|
29
|
+
/** Version stamped into pg_database.datcollversion at bootstrap. */
|
|
30
|
+
stamped: string;
|
|
31
|
+
/** Version the running libc reports via pg_database_collation_actual_version. */
|
|
32
|
+
actual: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read `pg_database.datcollversion` and
|
|
36
|
+
* `pg_database_collation_actual_version(oid)` for `dbName` and return drift
|
|
37
|
+
* details when they differ. Returns `null` when stamped === actual OR when
|
|
38
|
+
* stamped is null (Postgres marks template0-style locked databases that way —
|
|
39
|
+
* benign, no drift to report).
|
|
40
|
+
*/
|
|
41
|
+
export declare function getDatabaseCollationDrift(pool: pg.Pool, dbName: string): Promise<CollationDrift | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Issue `REINDEX DATABASE CONCURRENTLY <quoted_db>`. Rebuilds every index in
|
|
44
|
+
* the database under the running libc collation rules.
|
|
45
|
+
*
|
|
46
|
+
* Identifier escaping uses `pg.escapeIdentifier` so a database name containing
|
|
47
|
+
* a double-quote (legal in Postgres) is quoted correctly.
|
|
48
|
+
*
|
|
49
|
+
* REINDEX DATABASE CONCURRENTLY refuses to run inside a transaction block, so
|
|
50
|
+
* the helper issues the query directly via `pool.query` (no explicit BEGIN);
|
|
51
|
+
* `node-postgres` does not start an implicit transaction.
|
|
52
|
+
*/
|
|
53
|
+
export declare function reindexDatabaseConcurrently(pool: pg.Pool, dbName: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Issue `ALTER DATABASE <quoted_db> REFRESH COLLATION VERSION`. Updates
|
|
56
|
+
* `pg_database.datcollversion` to match the running libc's reported version.
|
|
57
|
+
* Metadata-only; safe to run any time after a REINDEX has rebuilt the indexes.
|
|
58
|
+
*/
|
|
59
|
+
export declare function refreshDatabaseCollationVersion(pool: pg.Pool, dbName: string): Promise<void>;
|
|
60
|
+
//# sourceMappingURL=db-collation.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "./chunk-gOLHoazu.js";
|
|
2
|
+
import pg from "pg";
|
|
3
|
+
//#region src/db-collation.ts
|
|
4
|
+
/**
|
|
5
|
+
* Read `pg_database.datcollversion` and
|
|
6
|
+
* `pg_database_collation_actual_version(oid)` for `dbName` and return drift
|
|
7
|
+
* details when they differ. Returns `null` when stamped === actual OR when
|
|
8
|
+
* stamped is null (Postgres marks template0-style locked databases that way —
|
|
9
|
+
* benign, no drift to report).
|
|
10
|
+
*/
|
|
11
|
+
async function getDatabaseCollationDrift(pool, dbName) {
|
|
12
|
+
const row = (await pool.query(`SELECT datcollversion AS stamped,
|
|
13
|
+
pg_database_collation_actual_version(oid) AS actual
|
|
14
|
+
FROM pg_database
|
|
15
|
+
WHERE datname = $1`, [dbName])).rows[0];
|
|
16
|
+
if (!row) throw new Error(`getDatabaseCollationDrift: database not found: ${dbName}`);
|
|
17
|
+
if (row.stamped === null) return null;
|
|
18
|
+
if (row.actual === null) throw new Error(`getDatabaseCollationDrift: pg_database_collation_actual_version returned null for ${dbName}`);
|
|
19
|
+
if (row.stamped === row.actual) return null;
|
|
20
|
+
return {
|
|
21
|
+
stamped: row.stamped,
|
|
22
|
+
actual: row.actual
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Issue `REINDEX DATABASE CONCURRENTLY <quoted_db>`. Rebuilds every index in
|
|
27
|
+
* the database under the running libc collation rules.
|
|
28
|
+
*
|
|
29
|
+
* Identifier escaping uses `pg.escapeIdentifier` so a database name containing
|
|
30
|
+
* a double-quote (legal in Postgres) is quoted correctly.
|
|
31
|
+
*
|
|
32
|
+
* REINDEX DATABASE CONCURRENTLY refuses to run inside a transaction block, so
|
|
33
|
+
* the helper issues the query directly via `pool.query` (no explicit BEGIN);
|
|
34
|
+
* `node-postgres` does not start an implicit transaction.
|
|
35
|
+
*/
|
|
36
|
+
async function reindexDatabaseConcurrently(pool, dbName) {
|
|
37
|
+
const quoted = pg.escapeIdentifier(dbName);
|
|
38
|
+
await pool.query(`REINDEX DATABASE CONCURRENTLY ${quoted}`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Issue `ALTER DATABASE <quoted_db> REFRESH COLLATION VERSION`. Updates
|
|
42
|
+
* `pg_database.datcollversion` to match the running libc's reported version.
|
|
43
|
+
* Metadata-only; safe to run any time after a REINDEX has rebuilt the indexes.
|
|
44
|
+
*/
|
|
45
|
+
async function refreshDatabaseCollationVersion(pool, dbName) {
|
|
46
|
+
const quoted = pg.escapeIdentifier(dbName);
|
|
47
|
+
await pool.query(`ALTER DATABASE ${quoted} REFRESH COLLATION VERSION`);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { getDatabaseCollationDrift, refreshDatabaseCollationVersion, reindexDatabaseConcurrently };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=db-collation.js.map
|
package/dist/env/allowlist.d.ts
CHANGED
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
* - Forgejo dev server — FORGEJO_URL, FORGEJO_CONTAINER (read by the
|
|
54
54
|
* staging Forgejo bootstrap script; conventional
|
|
55
55
|
* names owned by Forgejo, not KiCI).
|
|
56
|
-
* -
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
56
|
+
* - Keycloak admin CLI — KEYCLOAK_* (Keycloak's own admin tooling and
|
|
57
|
+
* docs use unprefixed `KEYCLOAK_BASE_URL`,
|
|
58
|
+
* `KEYCLOAK_ADMIN_CLIENT_ID`,
|
|
59
|
+
* `KEYCLOAK_ADMIN_CLIENT_SECRET`, etc. — the
|
|
60
|
+
* E2E admin helpers honour those names as a
|
|
61
|
+
* fallback after `KICI_KEYCLOAK_*`).
|
|
62
62
|
* - Vite client config — VITE_* (Vite reserves this prefix for env
|
|
63
63
|
* vars exposed to client bundles; `VITE_BASE`
|
|
64
64
|
* and `VITE_DOCS_BASE_URL` flow through this
|
package/dist/env/allowlist.js
CHANGED
|
@@ -55,12 +55,12 @@ import "../chunk-gOLHoazu.js";
|
|
|
55
55
|
* - Forgejo dev server — FORGEJO_URL, FORGEJO_CONTAINER (read by the
|
|
56
56
|
* staging Forgejo bootstrap script; conventional
|
|
57
57
|
* names owned by Forgejo, not KiCI).
|
|
58
|
-
* -
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
58
|
+
* - Keycloak admin CLI — KEYCLOAK_* (Keycloak's own admin tooling and
|
|
59
|
+
* docs use unprefixed `KEYCLOAK_BASE_URL`,
|
|
60
|
+
* `KEYCLOAK_ADMIN_CLIENT_ID`,
|
|
61
|
+
* `KEYCLOAK_ADMIN_CLIENT_SECRET`, etc. — the
|
|
62
|
+
* E2E admin helpers honour those names as a
|
|
63
|
+
* fallback after `KICI_KEYCLOAK_*`).
|
|
64
64
|
* - Vite client config — VITE_* (Vite reserves this prefix for env
|
|
65
65
|
* vars exposed to client bundles; `VITE_BASE`
|
|
66
66
|
* and `VITE_DOCS_BASE_URL` flow through this
|
|
@@ -70,7 +70,7 @@ import "../chunk-gOLHoazu.js";
|
|
|
70
70
|
* the dev proxy), HEADED (Playwright convention
|
|
71
71
|
* for `--headed` runs).
|
|
72
72
|
*/
|
|
73
|
-
const OS_SDK_ALLOWLIST_REGEX = /^(KICI_.*|NODE_ENV|HOME|PATH|TZ|LANG|TMPDIR|USER|USERNAME|SHELL|COMSPEC|PWD|OLDPWD|HOSTNAME|COLUMNS|LINES|TERM|COLORTERM|DISPLAY|WAYLAND_DISPLAY|LOCALAPPDATA|XDG_CACHE_HOME|XDG_CONFIG_HOME|XDG_DATA_HOME|XDG_RUNTIME_DIR|XDG_STATE_HOME|INIT_CWD|npm_.*|SSH_.*|CI|GITHUB_ACTIONS|GITHUB_ENV|GITHUB_OUTPUT|GITHUB_PATH|GITHUB_STEP_SUMMARY|GITLAB_CI|AWS_.*|REDIS_.*|OTEL_.*|STRIPE_.*|DOCKER_.*|CONTAINER_HOST|container|PGHOST|PGPORT|PGUSER|PGPASSWORD|PGDATABASE|PGSERVICEFILE|PGSSLMODE|FORGEJO_URL|FORGEJO_CONTAINER|
|
|
73
|
+
const OS_SDK_ALLOWLIST_REGEX = /^(KICI_.*|NODE_ENV|HOME|PATH|TZ|LANG|TMPDIR|USER|USERNAME|SHELL|COMSPEC|PWD|OLDPWD|HOSTNAME|COLUMNS|LINES|TERM|COLORTERM|DISPLAY|WAYLAND_DISPLAY|LOCALAPPDATA|XDG_CACHE_HOME|XDG_CONFIG_HOME|XDG_DATA_HOME|XDG_RUNTIME_DIR|XDG_STATE_HOME|INIT_CWD|npm_.*|SSH_.*|CI|GITHUB_ACTIONS|GITHUB_ENV|GITHUB_OUTPUT|GITHUB_PATH|GITHUB_STEP_SUMMARY|GITLAB_CI|AWS_.*|REDIS_.*|OTEL_.*|STRIPE_.*|DOCKER_.*|CONTAINER_HOST|container|PGHOST|PGPORT|PGUSER|PGPASSWORD|PGDATABASE|PGSERVICEFILE|PGSSLMODE|FORGEJO_URL|FORGEJO_CONTAINER|KEYCLOAK_.*|VITE_.*|PLAYWRIGHT|HEADED)$/;
|
|
74
74
|
/**
|
|
75
75
|
* Returns true when `name` is allowed under the KiCI env-var convention:
|
|
76
76
|
* it matches the OS/SDK allowlist regex (which includes the `KICI_*`
|
package/dist/env/logger-env.d.ts
CHANGED
|
@@ -19,12 +19,17 @@ export declare const LoggerEnvSchema: z.ZodObject<{
|
|
|
19
19
|
KICI_LOG_DIR: z.ZodOptional<z.ZodString>;
|
|
20
20
|
KICI_LOG_MAX_SIZE: z.ZodDefault<z.ZodString>;
|
|
21
21
|
KICI_LOG_RETENTION_DAYS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
22
|
+
KICI_LOG_FORMAT: z.ZodDefault<z.ZodEnum<{
|
|
23
|
+
plain: "plain";
|
|
24
|
+
json: "json";
|
|
25
|
+
auto: "auto";
|
|
26
|
+
}>>;
|
|
22
27
|
KICI_CLUSTER_INSTANCE_ID: z.ZodOptional<z.ZodString>;
|
|
23
28
|
KICI_AGENT_ID: z.ZodOptional<z.ZodString>;
|
|
24
29
|
KICI_PLATFORM_INSTANCE_ID: z.ZodOptional<z.ZodString>;
|
|
25
30
|
}, z.core.$strip>;
|
|
26
31
|
/** All env vars the logger reads, for the unknown-KICI-var scanner. */
|
|
27
|
-
export declare const LOGGER_ENV_VARS: readonly ["KICI_LOG_DIR", "KICI_LOG_MAX_SIZE", "KICI_LOG_RETENTION_DAYS", "KICI_CLUSTER_INSTANCE_ID", "KICI_AGENT_ID", "KICI_PLATFORM_INSTANCE_ID"];
|
|
32
|
+
export declare const LOGGER_ENV_VARS: readonly ["KICI_LOG_DIR", "KICI_LOG_MAX_SIZE", "KICI_LOG_RETENTION_DAYS", "KICI_LOG_FORMAT", "KICI_CLUSTER_INSTANCE_ID", "KICI_AGENT_ID", "KICI_PLATFORM_INSTANCE_ID"];
|
|
28
33
|
/** Doc-friendly description map (consumed by the env-reference generator). */
|
|
29
34
|
export declare const LOGGER_ENV_FIELD_SPECS: EnvFieldSpec[];
|
|
30
35
|
//# sourceMappingURL=logger-env.d.ts.map
|
package/dist/env/logger-env.js
CHANGED
|
@@ -20,6 +20,17 @@ const LoggerEnvSchema = z.object({
|
|
|
20
20
|
KICI_LOG_DIR: z.string().optional(),
|
|
21
21
|
KICI_LOG_MAX_SIZE: z.string().default("500m"),
|
|
22
22
|
KICI_LOG_RETENTION_DAYS: z.coerce.number().default(7),
|
|
23
|
+
/**
|
|
24
|
+
* Output format selection: `auto` (default — JSON when stdout is piped,
|
|
25
|
+
* plain text when stdout is a TTY), `plain` (always pretty), or `json`
|
|
26
|
+
* (always structured). Runtime services pin this to `json` so a stray
|
|
27
|
+
* journal-attached PTY can never flip them into plain mode.
|
|
28
|
+
*/
|
|
29
|
+
KICI_LOG_FORMAT: z.enum([
|
|
30
|
+
"auto",
|
|
31
|
+
"plain",
|
|
32
|
+
"json"
|
|
33
|
+
]).default("auto"),
|
|
23
34
|
/** Set by the orchestrator process; used as a filename suffix. */
|
|
24
35
|
KICI_CLUSTER_INSTANCE_ID: z.string().optional(),
|
|
25
36
|
/** Set by the agent process; used as a filename suffix. */
|
|
@@ -32,6 +43,7 @@ const LOGGER_ENV_VARS = [
|
|
|
32
43
|
"KICI_LOG_DIR",
|
|
33
44
|
"KICI_LOG_MAX_SIZE",
|
|
34
45
|
"KICI_LOG_RETENTION_DAYS",
|
|
46
|
+
"KICI_LOG_FORMAT",
|
|
35
47
|
"KICI_CLUSTER_INSTANCE_ID",
|
|
36
48
|
"KICI_AGENT_ID",
|
|
37
49
|
"KICI_PLATFORM_INSTANCE_ID"
|
|
@@ -62,6 +74,15 @@ const LOGGER_ENV_FIELD_SPECS = [
|
|
|
62
74
|
type: "string",
|
|
63
75
|
description: "Directory for rotated JSON log files. When unset, the logger only writes to stdout/stderr."
|
|
64
76
|
},
|
|
77
|
+
{
|
|
78
|
+
envVar: "KICI_LOG_FORMAT",
|
|
79
|
+
aliases: [],
|
|
80
|
+
fieldPath: "KICI_LOG_FORMAT",
|
|
81
|
+
required: false,
|
|
82
|
+
defaultValue: "\"auto\"",
|
|
83
|
+
type: "enum:auto|plain|json",
|
|
84
|
+
description: "Output format for stdout/stderr. `auto` (default) emits JSON when stdout is piped and plain coloured text when stdout is a TTY. `plain` and `json` force the corresponding format regardless of TTY. Runtime services (orchestrator, agent, platform, dashboard SSR) pin `json` so a journal-attached PTY cannot flip them into plain mode."
|
|
85
|
+
},
|
|
65
86
|
{
|
|
66
87
|
envVar: "KICI_LOG_MAX_SIZE",
|
|
67
88
|
aliases: [],
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env-file semantic diff for the idempotent-step preview path.
|
|
3
|
+
*
|
|
4
|
+
* Inputs are the raw byte strings of two env files (local generated +
|
|
5
|
+
* remote-cat'd). Output is a sorted list of `EnvDiffEntry` records — one
|
|
6
|
+
* per key that was added, removed, or changed (plus an unchanged count).
|
|
7
|
+
*
|
|
8
|
+
* The renderer masks every value by default: an env file is sops-derived
|
|
9
|
+
* credential material in the deploy-prod context, and the confirm prompt
|
|
10
|
+
* is tee'd to `release-prod.log.<ts>`. Operators who need to see the
|
|
11
|
+
* actual values pass `--reveal-env-values` end-to-end (CLI flag → env var
|
|
12
|
+
* `KICI_REVEAL_ENV_VALUES=1` → `reveal: true` on this renderer). Without
|
|
13
|
+
* the flag the rendered output carries only the key name + classification.
|
|
14
|
+
*/
|
|
15
|
+
export type EnvDiffKind = 'added' | 'removed' | 'changed' | 'unchanged';
|
|
16
|
+
export interface EnvDiffEntry {
|
|
17
|
+
key: string;
|
|
18
|
+
kind: EnvDiffKind;
|
|
19
|
+
/** Only populated for `changed` / `removed` kinds. */
|
|
20
|
+
oldValue?: string;
|
|
21
|
+
/** Only populated for `added` / `changed` kinds. */
|
|
22
|
+
newValue?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse an env-file body into a `Map<string, string>`. The shape matches
|
|
26
|
+
* what Docker / systemd `EnvironmentFile=` / Keycloak's env loader consume:
|
|
27
|
+
*
|
|
28
|
+
* - Blank lines and `#`-prefixed comments are skipped.
|
|
29
|
+
* - The first `=` splits key and value; values may contain further `=`.
|
|
30
|
+
* - Surrounding single or double quotes on the value are stripped when
|
|
31
|
+
* they are balanced; otherwise the literal bytes are preserved.
|
|
32
|
+
* - Duplicate keys: last write wins (POSIX shell semantics).
|
|
33
|
+
* - Whitespace is stripped from the key only — values keep their bytes
|
|
34
|
+
* verbatim so a trailing space in a credential is detected as drift.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseEnvLines(content: string): Map<string, string>;
|
|
37
|
+
/**
|
|
38
|
+
* Diff two env-file bodies. Returns one entry per key in the union of
|
|
39
|
+
* both inputs. Entry order: `added`, `changed`, `removed`, `unchanged`,
|
|
40
|
+
* alphabetically inside each group.
|
|
41
|
+
*
|
|
42
|
+
* `local` is the new-side bytes (what the deploy would ship), `remote`
|
|
43
|
+
* is the old-side bytes (what's currently on the box). The convention
|
|
44
|
+
* matches `previewRsyncFileToBox`'s `localContent` / `remoteContent`
|
|
45
|
+
* field names.
|
|
46
|
+
*/
|
|
47
|
+
export declare function diffEnvFiles(local: string, remote: string): EnvDiffEntry[];
|
|
48
|
+
export interface EnvDiffRenderOpts {
|
|
49
|
+
/** When true, show old + new values inline. Default false (masked output). */
|
|
50
|
+
reveal?: boolean;
|
|
51
|
+
/** When true, color the kind label with ANSI red/green/cyan. */
|
|
52
|
+
color?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render the env-semantic diff body lines. The returned array carries
|
|
56
|
+
* NO surrounding indentation — the caller (e.g. `renderFileDrifts`)
|
|
57
|
+
* decides how to nest each line under its containing per-file row.
|
|
58
|
+
*
|
|
59
|
+
* Masked layout (default):
|
|
60
|
+
*
|
|
61
|
+
* KEY_A: changed
|
|
62
|
+
* KEY_B: added
|
|
63
|
+
* KEY_C: removed
|
|
64
|
+
* 3 other key(s) unchanged
|
|
65
|
+
*
|
|
66
|
+
* Revealed layout (`reveal: true`):
|
|
67
|
+
*
|
|
68
|
+
* KEY_A: changed
|
|
69
|
+
* - old=hunter2
|
|
70
|
+
* + new=tr0ub4dor
|
|
71
|
+
* KEY_B: added
|
|
72
|
+
* + new=AKIA…
|
|
73
|
+
* KEY_C: removed
|
|
74
|
+
* - old=zzz
|
|
75
|
+
* 3 other key(s) unchanged
|
|
76
|
+
*
|
|
77
|
+
* When there are no changed / added / removed entries (every key matches
|
|
78
|
+
* byte-for-byte), the function returns a single "N key(s) unchanged"
|
|
79
|
+
* line so the operator gets a positive "in sync at the env level"
|
|
80
|
+
* confirmation rather than an empty body.
|
|
81
|
+
*/
|
|
82
|
+
export declare function renderEnvDiff(entries: EnvDiffEntry[], opts?: EnvDiffRenderOpts): string[];
|
|
83
|
+
//# sourceMappingURL=idempotency-env-diff.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import "./chunk-gOLHoazu.js";
|
|
2
|
+
//#region src/idempotency-env-diff.ts
|
|
3
|
+
/**
|
|
4
|
+
* Env-file semantic diff for the idempotent-step preview path.
|
|
5
|
+
*
|
|
6
|
+
* Inputs are the raw byte strings of two env files (local generated +
|
|
7
|
+
* remote-cat'd). Output is a sorted list of `EnvDiffEntry` records — one
|
|
8
|
+
* per key that was added, removed, or changed (plus an unchanged count).
|
|
9
|
+
*
|
|
10
|
+
* The renderer masks every value by default: an env file is sops-derived
|
|
11
|
+
* credential material in the deploy-prod context, and the confirm prompt
|
|
12
|
+
* is tee'd to `release-prod.log.<ts>`. Operators who need to see the
|
|
13
|
+
* actual values pass `--reveal-env-values` end-to-end (CLI flag → env var
|
|
14
|
+
* `KICI_REVEAL_ENV_VALUES=1` → `reveal: true` on this renderer). Without
|
|
15
|
+
* the flag the rendered output carries only the key name + classification.
|
|
16
|
+
*/
|
|
17
|
+
const ANSI_RED = "\x1B[31m";
|
|
18
|
+
const ANSI_GREEN = "\x1B[32m";
|
|
19
|
+
const ANSI_CYAN = "\x1B[36m";
|
|
20
|
+
const ANSI_RESET = "\x1B[0m";
|
|
21
|
+
/**
|
|
22
|
+
* Parse an env-file body into a `Map<string, string>`. The shape matches
|
|
23
|
+
* what Docker / systemd `EnvironmentFile=` / Keycloak's env loader consume:
|
|
24
|
+
*
|
|
25
|
+
* - Blank lines and `#`-prefixed comments are skipped.
|
|
26
|
+
* - The first `=` splits key and value; values may contain further `=`.
|
|
27
|
+
* - Surrounding single or double quotes on the value are stripped when
|
|
28
|
+
* they are balanced; otherwise the literal bytes are preserved.
|
|
29
|
+
* - Duplicate keys: last write wins (POSIX shell semantics).
|
|
30
|
+
* - Whitespace is stripped from the key only — values keep their bytes
|
|
31
|
+
* verbatim so a trailing space in a credential is detected as drift.
|
|
32
|
+
*/
|
|
33
|
+
function parseEnvLines(content) {
|
|
34
|
+
const out = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const rawLine of content.split("\n")) {
|
|
36
|
+
const trimmedLeft = rawLine.replace(/\r$/, "").replace(/^[\t ]+/, "");
|
|
37
|
+
if (trimmedLeft === "") continue;
|
|
38
|
+
if (trimmedLeft.startsWith("#")) continue;
|
|
39
|
+
const eqIdx = trimmedLeft.indexOf("=");
|
|
40
|
+
if (eqIdx < 0) continue;
|
|
41
|
+
const key = trimmedLeft.slice(0, eqIdx).trim();
|
|
42
|
+
if (key === "") continue;
|
|
43
|
+
let value = trimmedLeft.slice(eqIdx + 1);
|
|
44
|
+
if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
|
|
45
|
+
out.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Diff two env-file bodies. Returns one entry per key in the union of
|
|
51
|
+
* both inputs. Entry order: `added`, `changed`, `removed`, `unchanged`,
|
|
52
|
+
* alphabetically inside each group.
|
|
53
|
+
*
|
|
54
|
+
* `local` is the new-side bytes (what the deploy would ship), `remote`
|
|
55
|
+
* is the old-side bytes (what's currently on the box). The convention
|
|
56
|
+
* matches `previewRsyncFileToBox`'s `localContent` / `remoteContent`
|
|
57
|
+
* field names.
|
|
58
|
+
*/
|
|
59
|
+
function diffEnvFiles(local, remote) {
|
|
60
|
+
const localMap = parseEnvLines(local);
|
|
61
|
+
const remoteMap = parseEnvLines(remote);
|
|
62
|
+
const entries = [];
|
|
63
|
+
const keys = new Set([...localMap.keys(), ...remoteMap.keys()]);
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
const inLocal = localMap.has(key);
|
|
66
|
+
const inRemote = remoteMap.has(key);
|
|
67
|
+
const localValue = localMap.get(key);
|
|
68
|
+
const remoteValue = remoteMap.get(key);
|
|
69
|
+
if (inLocal && !inRemote) entries.push({
|
|
70
|
+
key,
|
|
71
|
+
kind: "added",
|
|
72
|
+
newValue: localValue
|
|
73
|
+
});
|
|
74
|
+
else if (!inLocal && inRemote) entries.push({
|
|
75
|
+
key,
|
|
76
|
+
kind: "removed",
|
|
77
|
+
oldValue: remoteValue
|
|
78
|
+
});
|
|
79
|
+
else if (inLocal && inRemote && localValue !== remoteValue) entries.push({
|
|
80
|
+
key,
|
|
81
|
+
kind: "changed",
|
|
82
|
+
oldValue: remoteValue,
|
|
83
|
+
newValue: localValue
|
|
84
|
+
});
|
|
85
|
+
else entries.push({
|
|
86
|
+
key,
|
|
87
|
+
kind: "unchanged"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const ORDER = {
|
|
91
|
+
added: 0,
|
|
92
|
+
changed: 1,
|
|
93
|
+
removed: 2,
|
|
94
|
+
unchanged: 3
|
|
95
|
+
};
|
|
96
|
+
entries.sort((a, b) => {
|
|
97
|
+
if (ORDER[a.kind] !== ORDER[b.kind]) return ORDER[a.kind] - ORDER[b.kind];
|
|
98
|
+
return a.key.localeCompare(b.key);
|
|
99
|
+
});
|
|
100
|
+
return entries;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Render the env-semantic diff body lines. The returned array carries
|
|
104
|
+
* NO surrounding indentation — the caller (e.g. `renderFileDrifts`)
|
|
105
|
+
* decides how to nest each line under its containing per-file row.
|
|
106
|
+
*
|
|
107
|
+
* Masked layout (default):
|
|
108
|
+
*
|
|
109
|
+
* KEY_A: changed
|
|
110
|
+
* KEY_B: added
|
|
111
|
+
* KEY_C: removed
|
|
112
|
+
* 3 other key(s) unchanged
|
|
113
|
+
*
|
|
114
|
+
* Revealed layout (`reveal: true`):
|
|
115
|
+
*
|
|
116
|
+
* KEY_A: changed
|
|
117
|
+
* - old=hunter2
|
|
118
|
+
* + new=tr0ub4dor
|
|
119
|
+
* KEY_B: added
|
|
120
|
+
* + new=AKIA…
|
|
121
|
+
* KEY_C: removed
|
|
122
|
+
* - old=zzz
|
|
123
|
+
* 3 other key(s) unchanged
|
|
124
|
+
*
|
|
125
|
+
* When there are no changed / added / removed entries (every key matches
|
|
126
|
+
* byte-for-byte), the function returns a single "N key(s) unchanged"
|
|
127
|
+
* line so the operator gets a positive "in sync at the env level"
|
|
128
|
+
* confirmation rather than an empty body.
|
|
129
|
+
*/
|
|
130
|
+
function renderEnvDiff(entries, opts = {}) {
|
|
131
|
+
const reveal = opts.reveal ?? false;
|
|
132
|
+
const color = opts.color ?? false;
|
|
133
|
+
const lines = [];
|
|
134
|
+
const changedish = entries.filter((e) => e.kind !== "unchanged");
|
|
135
|
+
let unchangedCount = entries.length - changedish.length;
|
|
136
|
+
for (const entry of changedish) {
|
|
137
|
+
lines.push(`${entry.key}: ${colorizeKind(entry.kind, color)}`);
|
|
138
|
+
if (reveal) {
|
|
139
|
+
if (entry.kind === "changed" || entry.kind === "removed") lines.push(` ${colorize("- old=" + (entry.oldValue ?? ""), "old", color)}`);
|
|
140
|
+
if (entry.kind === "changed" || entry.kind === "added") lines.push(` ${colorize("+ new=" + (entry.newValue ?? ""), "new", color)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (unchangedCount > 0) lines.push(`${unchangedCount} other key(s) unchanged`);
|
|
144
|
+
else if (lines.length === 0) lines.push(`0 key(s) drifted (env content matches)`);
|
|
145
|
+
return lines;
|
|
146
|
+
}
|
|
147
|
+
function colorizeKind(kind, color) {
|
|
148
|
+
if (!color) return kind;
|
|
149
|
+
switch (kind) {
|
|
150
|
+
case "added": return `${ANSI_GREEN}${kind}${ANSI_RESET}`;
|
|
151
|
+
case "removed": return `${ANSI_RED}${kind}${ANSI_RESET}`;
|
|
152
|
+
case "changed": return `${ANSI_CYAN}${kind}${ANSI_RESET}`;
|
|
153
|
+
case "unchanged": return kind;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function colorize(text, side, color) {
|
|
157
|
+
if (!color) return text;
|
|
158
|
+
return side === "new" ? `${ANSI_GREEN}${text}${ANSI_RESET}` : `${ANSI_RED}${text}${ANSI_RESET}`;
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
export { diffEnvFiles, parseEnvLines, renderEnvDiff };
|
|
162
|
+
|
|
163
|
+
//# sourceMappingURL=idempotency-env-diff.js.map
|