@hasna/economy 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/chunks/{index-kgy9b6bf.js → index-9d2aky06.js} +1 -1
- package/dist/chunks/{index-axh32d30.js → index-dj3tctqh.js} +11 -2
- package/dist/chunks/{index-kn5572kd.js → index-kzwqw3ve.js} +1 -1
- package/dist/chunks/{index-emvs5ph2.js → index-npsjjv96.js} +1 -1
- package/dist/chunks/{serve-0k69e4h9.js → serve-p2yarhm8.js} +28 -3
- package/dist/chunks/{tui-7f8f951w.js → tui-x64hjfrh.js} +3 -3
- package/dist/chunks/{watch-mm1j39vy.js → watch-bxstjpbx.js} +3 -3
- package/dist/cli/index.js +10 -10
- package/dist/db/legacy-large-schema.d.ts +6 -0
- package/dist/db/legacy-large-schema.d.ts.map +1 -0
- package/dist/db/pg-legacy-schema.d.ts +12 -0
- package/dist/db/pg-legacy-schema.d.ts.map +1 -0
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/mcp/index.js +11 -2
- package/dist/otel/index.js +11 -2
- package/dist/server/index.js +358 -51
- package/dist/server/legacy-limits.d.ts +51 -0
- package/dist/server/legacy-limits.d.ts.map +1 -0
- package/dist/server/legacy-operations.d.ts +2 -1
- package/dist/server/legacy-operations.d.ts.map +1 -1
- package/dist/server/legacy-plan.d.ts +2 -1
- package/dist/server/legacy-plan.d.ts.map +1 -1
- package/dist/server/legacy-preservation.d.ts +2 -1
- package/dist/server/legacy-preservation.d.ts.map +1 -1
- package/dist/server/service-worker.js +11 -2
- package/docs/historical-data-reconciliation.md +38 -0
- package/docs/postgresql-legacy-schema-repair.md +30 -0
- package/package.json +11 -2
package/dist/server/index.js
CHANGED
|
@@ -3360,6 +3360,30 @@ function resolvePgConnection(dsn) {
|
|
|
3360
3360
|
}
|
|
3361
3361
|
var init_pg_connection = () => {};
|
|
3362
3362
|
|
|
3363
|
+
// src/db/legacy-large-schema.ts
|
|
3364
|
+
var LEGACY_LARGE_SESSION_PG_SQL = `
|
|
3365
|
+
CREATE INDEX idx_legacy_targets_session_operation
|
|
3366
|
+
ON legacy_reconciliation_targets(session_id,operation_key);
|
|
3367
|
+
CREATE OR REPLACE FUNCTION economy_legacy_target_guard() RETURNS trigger LANGUAGE plpgsql AS $body$
|
|
3368
|
+
BEGIN
|
|
3369
|
+
IF EXISTS(
|
|
3370
|
+
SELECT 1 FROM (SELECT DISTINCT session_id,operation_key FROM inserted_legacy_targets) n
|
|
3371
|
+
JOIN economy_legacy_active a ON a.session_id=n.session_id AND a.operation_key<>n.operation_key
|
|
3372
|
+
) OR EXISTS(
|
|
3373
|
+
SELECT 1 FROM inserted_legacy_targets n
|
|
3374
|
+
JOIN economy_legacy_active a ON a.request_id=n.request_id AND a.operation_key<>n.operation_key
|
|
3375
|
+
) THEN
|
|
3376
|
+
RAISE EXCEPTION 'legacy reconciliation is already active for this session' USING ERRCODE='23000';
|
|
3377
|
+
END IF;
|
|
3378
|
+
RETURN NULL;
|
|
3379
|
+
END
|
|
3380
|
+
$body$;
|
|
3381
|
+
DROP TRIGGER legacy_target_no_overlap ON legacy_reconciliation_targets;
|
|
3382
|
+
CREATE TRIGGER legacy_target_no_overlap AFTER INSERT ON legacy_reconciliation_targets
|
|
3383
|
+
REFERENCING NEW TABLE AS inserted_legacy_targets
|
|
3384
|
+
FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_target_guard();
|
|
3385
|
+
`;
|
|
3386
|
+
|
|
3363
3387
|
// src/db/pg-migrations.ts
|
|
3364
3388
|
var PG_MIGRATIONS;
|
|
3365
3389
|
var init_pg_migrations = __esm(() => {
|
|
@@ -3595,7 +3619,8 @@ var init_pg_migrations = __esm(() => {
|
|
|
3595
3619
|
...OBSERVATION_PG_MIGRATIONS,
|
|
3596
3620
|
`ALTER TABLE budgets ADD COLUMN IF NOT EXISTS metric TEXT NOT NULL DEFAULT 'legacy_request_cost_usd'
|
|
3597
3621
|
CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`,
|
|
3598
|
-
LEGACY_PG_SQL
|
|
3622
|
+
LEGACY_PG_SQL,
|
|
3623
|
+
LEGACY_LARGE_SESSION_PG_SQL
|
|
3599
3624
|
];
|
|
3600
3625
|
});
|
|
3601
3626
|
|
|
@@ -4164,6 +4189,208 @@ var init_pg_migrate = __esm(() => {
|
|
|
4164
4189
|
init_pg_connection();
|
|
4165
4190
|
});
|
|
4166
4191
|
|
|
4192
|
+
// src/db/pg-legacy-schema.ts
|
|
4193
|
+
var exports_pg_legacy_schema = {};
|
|
4194
|
+
__export(exports_pg_legacy_schema, {
|
|
4195
|
+
repairLegacyCostCenterSchema: () => repairLegacyCostCenterSchema
|
|
4196
|
+
});
|
|
4197
|
+
import { createHash as createHash5 } from "crypto";
|
|
4198
|
+
import pg4 from "pg";
|
|
4199
|
+
async function assertReceiptTable(client, schema) {
|
|
4200
|
+
await client.query("LOCK TABLE " + quote2(schema) + ".economy_schema_repairs IN SHARE ROW EXCLUSIVE MODE");
|
|
4201
|
+
const shape = await relation(client, schema, "economy_schema_repairs");
|
|
4202
|
+
if (!shape || shape.columns.length !== 3 || !checkColumn("economy_schema_repairs", shape.columns.find((c) => c.name === "id"), true) || !checkColumn("economy_schema_repairs", shape.columns.find((c) => c.name === "sql_sha256"), true)) {
|
|
4203
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger columns");
|
|
4204
|
+
}
|
|
4205
|
+
const completed = shape.columns.find((c) => c.name === "completed_at");
|
|
4206
|
+
if (!completed || completed.type !== "timestamp with time zone" || !completed.required || completed.generated || completed.identity) {
|
|
4207
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger timestamp");
|
|
4208
|
+
}
|
|
4209
|
+
const meta = await client.query(`
|
|
4210
|
+
SELECT c.relpersistence='p' AND NOT c.relrowsecurity AND NOT c.relforcerowsecurity
|
|
4211
|
+
AND NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid=c.oid AND NOT tgisinternal)
|
|
4212
|
+
AND NOT EXISTS (SELECT 1 FROM pg_rewrite WHERE ev_class=c.oid)
|
|
4213
|
+
AND NOT EXISTS (SELECT 1 FROM pg_inherits WHERE inhrelid=c.oid OR inhparent=c.oid)
|
|
4214
|
+
AND NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid=c.oid AND (contype<>'p' OR condeferrable))
|
|
4215
|
+
AS ordinary,
|
|
4216
|
+
ARRAY(SELECT a.attname::text FROM pg_index i
|
|
4217
|
+
CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
|
|
4218
|
+
JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum=k.attnum
|
|
4219
|
+
WHERE i.indrelid=c.oid AND i.indisprimary AND i.indisvalid AND i.indisready
|
|
4220
|
+
ORDER BY k.ordinality) AS primary_columns,
|
|
4221
|
+
(SELECT pg_get_expr(d.adbin,d.adrelid) FROM pg_attrdef d JOIN pg_attribute a
|
|
4222
|
+
ON a.attrelid=d.adrelid AND a.attnum=d.adnum
|
|
4223
|
+
WHERE d.adrelid=c.oid AND a.attname='completed_at') AS clock_default
|
|
4224
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
4225
|
+
WHERE n.nspname=$1 AND c.relname='economy_schema_repairs'`, [schema]);
|
|
4226
|
+
const row = meta.rows[0];
|
|
4227
|
+
if (!row?.ordinary || JSON.stringify(row.primary_columns) !== '["id"]' || row.clock_default !== "clock_timestamp()") {
|
|
4228
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected repair ledger behavior");
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
async function relation(client, schema, name) {
|
|
4232
|
+
const result = await client.query(`
|
|
4233
|
+
SELECT c.relkind AS kind, a.attname AS name, a.atttypid::regtype::text AS type,
|
|
4234
|
+
a.attnotnull AS required, a.attgenerated AS generated, a.attidentity AS identity
|
|
4235
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
4236
|
+
LEFT JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum>0 AND NOT a.attisdropped
|
|
4237
|
+
WHERE n.nspname=$1 AND c.relname=$2 ORDER BY a.attnum`, [schema, name]);
|
|
4238
|
+
if (!result.rows.length)
|
|
4239
|
+
return null;
|
|
4240
|
+
if (result.rows[0].kind !== "r")
|
|
4241
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: expected ordinary table " + name);
|
|
4242
|
+
return { kind: result.rows[0].kind, columns: result.rows.filter((row) => row.name) };
|
|
4243
|
+
}
|
|
4244
|
+
function checkColumn(table, actual, required3) {
|
|
4245
|
+
if (!actual)
|
|
4246
|
+
return false;
|
|
4247
|
+
if (actual.type !== "text" || actual.required !== required3 || actual.generated || actual.identity) {
|
|
4248
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected column " + table + "." + actual.name);
|
|
4249
|
+
}
|
|
4250
|
+
return true;
|
|
4251
|
+
}
|
|
4252
|
+
async function missingStructure(client, schema) {
|
|
4253
|
+
const missing = [];
|
|
4254
|
+
for (const table of ["requests", "sessions", "budgets"]) {
|
|
4255
|
+
const found = await relation(client, schema, table);
|
|
4256
|
+
if (!found)
|
|
4257
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: missing core table " + table);
|
|
4258
|
+
if (!checkColumn(table, found.columns.find((column) => column.name === "cost_center_id"), false)) {
|
|
4259
|
+
missing.push(table + ".cost_center_id");
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
const centers = await relation(client, schema, "cost_centers");
|
|
4263
|
+
if (!centers)
|
|
4264
|
+
missing.push("cost_centers");
|
|
4265
|
+
else {
|
|
4266
|
+
for (const [name, required3] of [
|
|
4267
|
+
["id", true],
|
|
4268
|
+
["kind", true],
|
|
4269
|
+
["name", true],
|
|
4270
|
+
["repo_path", false],
|
|
4271
|
+
["labels_json", false],
|
|
4272
|
+
["created_at", true],
|
|
4273
|
+
["updated_at", true]
|
|
4274
|
+
]) {
|
|
4275
|
+
if (!checkColumn("cost_centers", centers.columns.find((column) => column.name === name), required3)) {
|
|
4276
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: incomplete cost_centers table");
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
const primary = await client.query(`
|
|
4280
|
+
SELECT array_agg(a.attname::text ORDER BY k.ordinality) AS columns
|
|
4281
|
+
FROM pg_index i JOIN pg_class c ON c.oid=i.indrelid JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
4282
|
+
CROSS JOIN LATERAL unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
|
|
4283
|
+
JOIN pg_attribute a ON a.attrelid=c.oid AND a.attnum=k.attnum
|
|
4284
|
+
WHERE n.nspname=$1 AND c.relname='cost_centers' AND i.indisprimary
|
|
4285
|
+
GROUP BY i.indexrelid`, [schema]);
|
|
4286
|
+
if (JSON.stringify(primary.rows.map((row) => row.columns)) !== '[["id"]]') {
|
|
4287
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected cost_centers primary key");
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
for (const [name, table, column] of INDEXES) {
|
|
4291
|
+
const result = await client.query(`
|
|
4292
|
+
SELECT t.relname AS table_name, am.amname AS method,
|
|
4293
|
+
i.indisunique AS unique, i.indisvalid AS valid, i.indisready AS ready,
|
|
4294
|
+
i.indpred IS NULL AS unfiltered, i.indexprs IS NULL AS simple,
|
|
4295
|
+
i.indnatts=i.indnkeyatts AS no_includes,
|
|
4296
|
+
ARRAY(SELECT a.attname::text FROM unnest(i.indkey) WITH ORDINALITY k(attnum,ordinality)
|
|
4297
|
+
JOIN pg_attribute a ON a.attrelid=t.oid AND a.attnum=k.attnum
|
|
4298
|
+
ORDER BY k.ordinality) AS columns
|
|
4299
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
4300
|
+
LEFT JOIN pg_index i ON i.indexrelid=c.oid LEFT JOIN pg_class t ON t.oid=i.indrelid
|
|
4301
|
+
LEFT JOIN pg_am am ON am.oid=c.relam WHERE n.nspname=$1 AND c.relname=$2`, [schema, name]);
|
|
4302
|
+
if (!result.rows.length) {
|
|
4303
|
+
missing.push(name);
|
|
4304
|
+
continue;
|
|
4305
|
+
}
|
|
4306
|
+
const actual = result.rows[0];
|
|
4307
|
+
if (actual.table_name !== table || actual.method !== "btree" || actual.unique || !actual.valid || !actual.ready || !actual.unfiltered || !actual.simple || !actual.no_includes || JSON.stringify(actual.columns) !== JSON.stringify([column])) {
|
|
4308
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: unexpected index " + name);
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
return missing;
|
|
4312
|
+
}
|
|
4313
|
+
async function repairLegacyCostCenterSchema(dsn) {
|
|
4314
|
+
const result = { id: REPAIR_ID, status: "not_needed", added: [] };
|
|
4315
|
+
const pool = new pg4.Pool({ ...resolvePgConnection(dsn), max: 1, connectionTimeoutMillis: 1e4 });
|
|
4316
|
+
pool.on("error", () => {});
|
|
4317
|
+
let client;
|
|
4318
|
+
try {
|
|
4319
|
+
client = await pool.connect();
|
|
4320
|
+
client.on("error", () => {});
|
|
4321
|
+
await client.query("SET statement_timeout='15min'; SET lock_timeout='15s'");
|
|
4322
|
+
await client.query("SELECT pg_advisory_lock(hashtext('economy.pg-migrations.v1'), hashtext(current_database() || ':' || current_schema()))");
|
|
4323
|
+
const schema = (await client.query("SELECT current_schema() AS name")).rows[0]?.name;
|
|
4324
|
+
if (typeof schema !== "string" || !schema)
|
|
4325
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: missing migration schema");
|
|
4326
|
+
const ledger = await relation(client, schema, "_pg_migrations");
|
|
4327
|
+
if (!ledger)
|
|
4328
|
+
return result;
|
|
4329
|
+
const history = await client.query("SELECT version FROM " + quote2(schema) + "._pg_migrations WHERE version>=0 AND version<85 ORDER BY version");
|
|
4330
|
+
if (history.rows.length !== 85 || history.rows.some((row, index) => row.version !== index)) {
|
|
4331
|
+
return result;
|
|
4332
|
+
}
|
|
4333
|
+
await client.query("BEGIN");
|
|
4334
|
+
try {
|
|
4335
|
+
await client.query("SELECT set_config('search_path', $1, true)", [quote2(schema)]);
|
|
4336
|
+
const missing = await missingStructure(client, schema);
|
|
4337
|
+
const receiptTable = await relation(client, schema, "economy_schema_repairs");
|
|
4338
|
+
if (receiptTable) {
|
|
4339
|
+
await assertReceiptTable(client, schema);
|
|
4340
|
+
const receipt = await client.query("SELECT sql_sha256 FROM economy_schema_repairs WHERE id=$1", [REPAIR_ID]);
|
|
4341
|
+
if (receipt.rows.length) {
|
|
4342
|
+
if (receipt.rows.length !== 1 || receipt.rows[0].sql_sha256 !== REPAIR_HASH || missing.length) {
|
|
4343
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: completed repair receipt or schema has changed");
|
|
4344
|
+
}
|
|
4345
|
+
await client.query("COMMIT");
|
|
4346
|
+
return { ...result, status: "already_applied" };
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
if (!missing.length) {
|
|
4350
|
+
await client.query("COMMIT");
|
|
4351
|
+
return result;
|
|
4352
|
+
}
|
|
4353
|
+
await client.query(`CREATE TABLE IF NOT EXISTS economy_schema_repairs (
|
|
4354
|
+
id TEXT PRIMARY KEY, sql_sha256 TEXT NOT NULL,
|
|
4355
|
+
completed_at TIMESTAMPTZ NOT NULL DEFAULT pg_catalog.clock_timestamp()
|
|
4356
|
+
)`);
|
|
4357
|
+
await assertReceiptTable(client, schema);
|
|
4358
|
+
for (const sql of REPAIR_SQL)
|
|
4359
|
+
await client.query(sql);
|
|
4360
|
+
if ((await missingStructure(client, schema)).length)
|
|
4361
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: repair postcondition failed");
|
|
4362
|
+
const inserted = await client.query("INSERT INTO economy_schema_repairs (id,sql_sha256) VALUES ($1,$2) RETURNING id,sql_sha256", [REPAIR_ID, REPAIR_HASH]);
|
|
4363
|
+
const readback = await client.query("SELECT id,sql_sha256 FROM economy_schema_repairs WHERE id=$1", [REPAIR_ID]);
|
|
4364
|
+
for (const receipt of [inserted, readback]) {
|
|
4365
|
+
if (receipt.rowCount !== 1 || receipt.rows[0]?.id !== REPAIR_ID || receipt.rows[0]?.sql_sha256 !== REPAIR_HASH) {
|
|
4366
|
+
throw Error("LEGACY_SCHEMA_INCOMPATIBLE: repair receipt readback failed");
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
await client.query("COMMIT");
|
|
4370
|
+
return { id: REPAIR_ID, status: "applied", added: missing };
|
|
4371
|
+
} catch (error) {
|
|
4372
|
+
await client.query("ROLLBACK").catch(() => {});
|
|
4373
|
+
throw error;
|
|
4374
|
+
}
|
|
4375
|
+
} finally {
|
|
4376
|
+
client?.release();
|
|
4377
|
+
await pool.end().catch(() => {});
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
var REPAIR_ID = "legacy-cost-centers-v1", REPAIR_SQL, REPAIR_HASH, INDEXES, quote2 = (name) => '"' + name.replaceAll('"', '""') + '"';
|
|
4381
|
+
var init_pg_legacy_schema = __esm(() => {
|
|
4382
|
+
init_pg_connection();
|
|
4383
|
+
init_pg_migrations();
|
|
4384
|
+
REPAIR_SQL = [0, 38, 47, 48, 15, 54, 55, 56].map((index) => PG_MIGRATIONS[index]);
|
|
4385
|
+
REPAIR_HASH = createHash5("sha256").update(JSON.stringify(REPAIR_SQL)).digest("hex");
|
|
4386
|
+
INDEXES = [
|
|
4387
|
+
["idx_cost_centers_kind", "cost_centers", "kind"],
|
|
4388
|
+
["idx_requests_cost_center", "requests", "cost_center_id"],
|
|
4389
|
+
["idx_sessions_cost_center", "sessions", "cost_center_id"],
|
|
4390
|
+
["idx_budgets_cost_center", "budgets", "cost_center_id"]
|
|
4391
|
+
];
|
|
4392
|
+
});
|
|
4393
|
+
|
|
4167
4394
|
// src/server/legacy-inventory.ts
|
|
4168
4395
|
var exports_legacy_inventory = {};
|
|
4169
4396
|
__export(exports_legacy_inventory, {
|
|
@@ -4306,6 +4533,42 @@ var init_legacy_inventory = __esm(() => {
|
|
|
4306
4533
|
allCandidates = LEGACY_FAMILIES.map((family) => `(${predicates[family]})`).join(" OR ");
|
|
4307
4534
|
});
|
|
4308
4535
|
|
|
4536
|
+
// src/server/legacy-limits.ts
|
|
4537
|
+
function legacyMode(kind, schema) {
|
|
4538
|
+
if (schema === LEGACY_STANDARD[kind])
|
|
4539
|
+
return LEGACY_STANDARD;
|
|
4540
|
+
if (schema === LEGACY_SINGLE_SESSION[kind])
|
|
4541
|
+
return LEGACY_SINGLE_SESSION;
|
|
4542
|
+
throw Error(`Unsupported legacy ${kind} schema`);
|
|
4543
|
+
}
|
|
4544
|
+
var LEGACY_STANDARD, LEGACY_SINGLE_SESSION;
|
|
4545
|
+
var init_legacy_limits = __esm(() => {
|
|
4546
|
+
LEGACY_STANDARD = {
|
|
4547
|
+
selection: "economy.legacy-selection.v1",
|
|
4548
|
+
plan: "economy.legacy-plan.v1",
|
|
4549
|
+
capsule: "economy.legacy-preservation.v1",
|
|
4550
|
+
receipt: "economy.legacy-operation-receipt.v1",
|
|
4551
|
+
items: 100,
|
|
4552
|
+
sessions: 100,
|
|
4553
|
+
members: 1e4,
|
|
4554
|
+
inputBytes: 256 * 1024,
|
|
4555
|
+
originalBytes: 16 * 1024 * 1024,
|
|
4556
|
+
capsuleBytes: 32 * 1024 * 1024
|
|
4557
|
+
};
|
|
4558
|
+
LEGACY_SINGLE_SESSION = {
|
|
4559
|
+
selection: "economy.legacy-session-selection.v1",
|
|
4560
|
+
plan: "economy.legacy-session-plan.v1",
|
|
4561
|
+
capsule: "economy.legacy-session-preservation.v1",
|
|
4562
|
+
receipt: "economy.legacy-session-operation-receipt.v1",
|
|
4563
|
+
items: 50000,
|
|
4564
|
+
sessions: 1,
|
|
4565
|
+
members: 50000,
|
|
4566
|
+
inputBytes: 32 * 1024 * 1024,
|
|
4567
|
+
originalBytes: 64 * 1024 * 1024,
|
|
4568
|
+
capsuleBytes: 128 * 1024 * 1024
|
|
4569
|
+
};
|
|
4570
|
+
});
|
|
4571
|
+
|
|
4309
4572
|
// src/server/legacy-plan.ts
|
|
4310
4573
|
var exports_legacy_plan = {};
|
|
4311
4574
|
__export(exports_legacy_plan, {
|
|
@@ -4316,7 +4579,7 @@ __export(exports_legacy_plan, {
|
|
|
4316
4579
|
parseLegacySelection: () => parseLegacySelection,
|
|
4317
4580
|
parseLegacyPlanOptions: () => parseLegacyPlanOptions
|
|
4318
4581
|
});
|
|
4319
|
-
import { createHash as
|
|
4582
|
+
import { createHash as createHash6 } from "crypto";
|
|
4320
4583
|
import { closeSync, constants, fstatSync, openSync, readSync } from "fs";
|
|
4321
4584
|
import { isAbsolute } from "path";
|
|
4322
4585
|
function object(value, fields, label) {
|
|
@@ -4341,10 +4604,11 @@ function digest2(value) {
|
|
|
4341
4604
|
}
|
|
4342
4605
|
function parseLegacySelection(value) {
|
|
4343
4606
|
const input = object(value, ["schema", "items"], "selection");
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4607
|
+
const mode = legacyMode("selection", input.schema);
|
|
4608
|
+
if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > mode.items)
|
|
4609
|
+
throw Error(`items must contain 1-${mode.items} explicit request selections`);
|
|
4610
|
+
if (Buffer.byteLength(JSON.stringify(input)) > mode.inputBytes)
|
|
4611
|
+
throw Error(`Selection exceeds its ${mode.inputBytes}-byte artifact bound`);
|
|
4348
4612
|
const ids = new Set;
|
|
4349
4613
|
const items = input.items.map((item) => {
|
|
4350
4614
|
const row = object(item, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref"], "item");
|
|
@@ -4367,7 +4631,7 @@ function parseLegacySelection(value) {
|
|
|
4367
4631
|
evidence_ref: text(row.evidence_ref, 512, "evidence_ref")
|
|
4368
4632
|
};
|
|
4369
4633
|
}).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
4370
|
-
return { schema:
|
|
4634
|
+
return { schema: mode.selection, items };
|
|
4371
4635
|
}
|
|
4372
4636
|
function readLegacySelectionFile(path) {
|
|
4373
4637
|
if (!isAbsolute(path) || path.includes("\x00"))
|
|
@@ -4380,9 +4644,10 @@ function readLegacySelectionFile(path) {
|
|
|
4380
4644
|
}
|
|
4381
4645
|
try {
|
|
4382
4646
|
const before = fstatSync(fd);
|
|
4647
|
+
const sizeError = "--input must be a regular JSON file of 1-262144 bytes for standard selection, or at most 33554432 bytes for explicit single-session selection";
|
|
4383
4648
|
if (!before.isFile() || before.size < 1 || before.size > INPUT_MAX)
|
|
4384
|
-
throw Error(
|
|
4385
|
-
const bytes = Buffer.alloc(INPUT_MAX + 1);
|
|
4649
|
+
throw Error(sizeError);
|
|
4650
|
+
const bytes = Buffer.alloc(Math.min(INPUT_MAX + 1, before.size + 1));
|
|
4386
4651
|
let offset = 0, n;
|
|
4387
4652
|
while (offset < bytes.length && (n = readSync(fd, bytes, offset, bytes.length - offset, null)) > 0)
|
|
4388
4653
|
offset += n;
|
|
@@ -4393,9 +4658,12 @@ function readLegacySelectionFile(path) {
|
|
|
4393
4658
|
try {
|
|
4394
4659
|
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset)));
|
|
4395
4660
|
} catch {
|
|
4396
|
-
throw Error("--input must contain valid UTF-8 JSON");
|
|
4661
|
+
throw Error(before.size > LEGACY_STANDARD.inputBytes ? sizeError : "--input must contain valid UTF-8 JSON");
|
|
4397
4662
|
}
|
|
4398
|
-
|
|
4663
|
+
const selection = parseLegacySelection(value);
|
|
4664
|
+
if (offset > legacyMode("selection", selection.schema).inputBytes)
|
|
4665
|
+
throw Error(sizeError);
|
|
4666
|
+
return selection;
|
|
4399
4667
|
} finally {
|
|
4400
4668
|
closeSync(fd);
|
|
4401
4669
|
}
|
|
@@ -4410,6 +4678,7 @@ async function prepareLegacyPlan(client, input) {
|
|
|
4410
4678
|
}
|
|
4411
4679
|
async function withLegacyPlanSnapshot(client, input, read) {
|
|
4412
4680
|
const selection = parseLegacySelection(input);
|
|
4681
|
+
const mode = legacyMode("selection", selection.schema);
|
|
4413
4682
|
let began = false;
|
|
4414
4683
|
try {
|
|
4415
4684
|
await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY");
|
|
@@ -4444,14 +4713,16 @@ async function withLegacyPlanSnapshot(client, input, read) {
|
|
|
4444
4713
|
return { ...item, session_id: identifier2(row.session_id, "session_id") };
|
|
4445
4714
|
});
|
|
4446
4715
|
const sessionIds = [...new Set(items.map((x) => x.session_id))].sort();
|
|
4716
|
+
if (mode.sessions === 1 && sessionIds.length !== 1)
|
|
4717
|
+
throw Error("Single-session reconciliation requires exactly one session");
|
|
4447
4718
|
const sessions = (await client.query(`SELECT id, encode(sha256(convert_to(row_to_json(sessions)::text,'UTF8')),'hex') AS original_sha256
|
|
4448
4719
|
FROM sessions WHERE id=ANY($1::text[]) ORDER BY id`, [sessionIds])).rows;
|
|
4449
4720
|
if (sessions.length !== sessionIds.length)
|
|
4450
4721
|
throw Error("A selected request has no session; resolve that inconsistency before reconciliation");
|
|
4451
4722
|
const members = (await client.query(`SELECT id, session_id, encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256
|
|
4452
|
-
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${
|
|
4453
|
-
if (members.length >
|
|
4454
|
-
throw Error(
|
|
4723
|
+
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${mode.members + 1}`, [sessionIds])).rows;
|
|
4724
|
+
if (members.length > mode.members)
|
|
4725
|
+
throw Error(`Selected sessions exceed ${mode.members} requests; complete-session reconciliation cannot truncate membership`);
|
|
4455
4726
|
const populations = sessions.map((session) => {
|
|
4456
4727
|
const rows = members.filter((row) => row.session_id === session.id);
|
|
4457
4728
|
if (!rows.length)
|
|
@@ -4461,7 +4732,7 @@ async function withLegacyPlanSnapshot(client, input, read) {
|
|
|
4461
4732
|
return { id: session.id, original_sha256, request_count: rows.length, members_sha256 };
|
|
4462
4733
|
});
|
|
4463
4734
|
const body = {
|
|
4464
|
-
schema:
|
|
4735
|
+
schema: mode.plan,
|
|
4465
4736
|
mode: "read-only",
|
|
4466
4737
|
original_encoding: "postgres-row-json-text-v1",
|
|
4467
4738
|
member_encoding: "sha256-json-id-digest-pairs-utf8-c-order-v1",
|
|
@@ -4505,10 +4776,11 @@ async function runLegacyPlan(selection) {
|
|
|
4505
4776
|
await pool.end();
|
|
4506
4777
|
}
|
|
4507
4778
|
}
|
|
4508
|
-
var INPUT_MAX,
|
|
4779
|
+
var INPUT_MAX, sha256 = (text) => createHash6("sha256").update(text).digest("hex");
|
|
4509
4780
|
var init_legacy_plan = __esm(() => {
|
|
4510
4781
|
init_legacy_inventory();
|
|
4511
|
-
|
|
4782
|
+
init_legacy_limits();
|
|
4783
|
+
INPUT_MAX = LEGACY_SINGLE_SESSION.inputBytes;
|
|
4512
4784
|
});
|
|
4513
4785
|
|
|
4514
4786
|
// src/server/legacy-preservation.ts
|
|
@@ -4524,20 +4796,21 @@ __export(exports_legacy_preservation, {
|
|
|
4524
4796
|
parseLegacyCapsule: () => parseLegacyCapsule,
|
|
4525
4797
|
assertLegacyPlanCurrent: () => assertLegacyPlanCurrent
|
|
4526
4798
|
});
|
|
4527
|
-
import { createHash as
|
|
4799
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
|
|
4528
4800
|
import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2, fsyncSync, lstatSync, openSync as openSync2, readSync as readSync2, realpathSync, writeSync } from "fs";
|
|
4529
4801
|
import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
|
|
4530
4802
|
function parseLegacyPlan(value) {
|
|
4531
4803
|
const p = record(value);
|
|
4804
|
+
const mode = legacyMode("plan", p.schema);
|
|
4532
4805
|
fields(p, ["schema", "mode", "original_encoding", "member_encoding", "snapshot", "items", "sessions", "mutations_performed", "limitations", "plan_sha256"]);
|
|
4533
|
-
if (p.
|
|
4806
|
+
if (p.mode !== "read-only" || p.original_encoding !== "postgres-row-json-text-v1" || p.member_encoding !== "sha256-json-id-digest-pairs-utf8-c-order-v1" || p.mutations_performed !== 0)
|
|
4534
4807
|
fail("Unsupported legacy plan contract");
|
|
4535
4808
|
const { plan_sha256, ...body } = p;
|
|
4536
4809
|
if (hash(plan_sha256) !== sha2(JSON.stringify(body)))
|
|
4537
4810
|
fail("Legacy plan digest mismatch");
|
|
4538
4811
|
if (!Array.isArray(p.items))
|
|
4539
4812
|
fail("Legacy plan items must be an array");
|
|
4540
|
-
const selection = parseLegacySelection({ schema:
|
|
4813
|
+
const selection = parseLegacySelection({ schema: mode.selection, items: p.items.map((item) => {
|
|
4541
4814
|
const row = record(item);
|
|
4542
4815
|
fields(row, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref", "session_id"]);
|
|
4543
4816
|
identifier3(row.session_id);
|
|
@@ -4565,7 +4838,7 @@ function parseLegacyPlan(value) {
|
|
|
4565
4838
|
if (typeof t.relation_oid !== "string" || !/^[0-9]+$/.test(t.relation_oid))
|
|
4566
4839
|
fail("Invalid relation locator");
|
|
4567
4840
|
}
|
|
4568
|
-
if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length >
|
|
4841
|
+
if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length > mode.sessions)
|
|
4569
4842
|
fail("Invalid legacy session population");
|
|
4570
4843
|
const ids = new Set;
|
|
4571
4844
|
let members = 0;
|
|
@@ -4582,7 +4855,7 @@ function parseLegacyPlan(value) {
|
|
|
4582
4855
|
fail("Invalid legacy member count");
|
|
4583
4856
|
members += s.request_count;
|
|
4584
4857
|
}
|
|
4585
|
-
if (members >
|
|
4858
|
+
if (members > mode.members || p.items.some((x) => !ids.has(x.session_id)) || ids.size !== new Set(p.items.map((x) => x.session_id)).size)
|
|
4586
4859
|
fail("Incomplete or excessive legacy session membership");
|
|
4587
4860
|
if (!Array.isArray(p.limitations) || p.limitations.length > 20 || p.limitations.some((x) => typeof x !== "string" || x.length > 2048))
|
|
4588
4861
|
fail("Invalid legacy plan limitations");
|
|
@@ -4599,16 +4872,28 @@ function assertLegacyPlanCurrent(expected, current) {
|
|
|
4599
4872
|
}
|
|
4600
4873
|
function parseLegacyCapsule(value) {
|
|
4601
4874
|
const c = record(value);
|
|
4875
|
+
const mode = legacyMode("capsule", c.schema);
|
|
4602
4876
|
fields(c, ["schema", "plan", "captured_at", "requests", "sessions", "capsule_sha256"]);
|
|
4603
|
-
if (
|
|
4877
|
+
if (typeof c.captured_at !== "string" || c.captured_at.length > 100 || !c.captured_at)
|
|
4604
4878
|
fail("Invalid preservation capsule");
|
|
4605
4879
|
const { capsule_sha256, ...body } = c;
|
|
4606
4880
|
const bytes = JSON.stringify(body);
|
|
4607
|
-
if (Buffer.byteLength(bytes) >
|
|
4881
|
+
if (Buffer.byteLength(bytes) > mode.capsuleBytes || hash(capsule_sha256) !== sha2(bytes))
|
|
4608
4882
|
fail("Preservation capsule bound or digest mismatch");
|
|
4609
4883
|
const plan = parseLegacyPlan(c.plan);
|
|
4610
|
-
if (
|
|
4884
|
+
if (plan.schema !== mode.plan)
|
|
4885
|
+
fail("Preservation capsule and plan use different artifact modes");
|
|
4886
|
+
if (!Array.isArray(c.requests) || c.requests.length > mode.members || !Array.isArray(c.sessions) || c.sessions.length !== plan.sessions.length)
|
|
4611
4887
|
fail("Invalid original populations");
|
|
4888
|
+
let originalBytes = 0;
|
|
4889
|
+
const boundOriginal = (original) => {
|
|
4890
|
+
if (typeof original !== "string")
|
|
4891
|
+
fail("Invalid original row text");
|
|
4892
|
+
originalBytes += Buffer.byteLength(original);
|
|
4893
|
+
if (originalBytes > mode.originalBytes)
|
|
4894
|
+
fail("Preserved original row text exceeds its artifact mode bound");
|
|
4895
|
+
return original;
|
|
4896
|
+
};
|
|
4612
4897
|
const requests = new Map;
|
|
4613
4898
|
for (const value2 of c.requests) {
|
|
4614
4899
|
const row = record(value2);
|
|
@@ -4616,7 +4901,7 @@ function parseLegacyCapsule(value) {
|
|
|
4616
4901
|
const id = identifier3(row.id);
|
|
4617
4902
|
if (requests.has(id) || typeof row.original !== "string")
|
|
4618
4903
|
fail("Invalid or duplicate request original");
|
|
4619
|
-
const original = record(JSON.parse(row.original));
|
|
4904
|
+
const original = record(JSON.parse(boundOriginal(row.original)));
|
|
4620
4905
|
if (original.id !== id)
|
|
4621
4906
|
fail("Request original identity mismatch");
|
|
4622
4907
|
requests.set(id, { session: identifier3(original.session_id), digest: sha2(row.original) });
|
|
@@ -4630,7 +4915,7 @@ function parseLegacyCapsule(value) {
|
|
|
4630
4915
|
fail("Invalid or duplicate session original");
|
|
4631
4916
|
sessions.add(id);
|
|
4632
4917
|
const expected = plan.sessions.find((s) => s.id === id) ?? fail("Session original does not match the plan");
|
|
4633
|
-
if (record(JSON.parse(row.original)).id !== id || sha2(row.original) !== expected.original_sha256)
|
|
4918
|
+
if (record(JSON.parse(boundOriginal(row.original))).id !== id || sha2(row.original) !== expected.original_sha256)
|
|
4634
4919
|
fail("Session original does not match the plan");
|
|
4635
4920
|
const members = [...requests].filter(([, r]) => r.session === id).sort(([a], [b]) => cOrder(a, b)).map(([rid, r]) => [rid, r.digest]);
|
|
4636
4921
|
if (members.length !== expected.request_count || sha2(JSON.stringify(members)) !== expected.members_sha256)
|
|
@@ -4647,18 +4932,19 @@ function parseLegacyCapsule(value) {
|
|
|
4647
4932
|
}
|
|
4648
4933
|
async function preserveLegacyOriginals(client, input) {
|
|
4649
4934
|
const expected = parseLegacyPlan(input);
|
|
4650
|
-
const
|
|
4935
|
+
const mode = legacyMode("plan", expected.schema);
|
|
4936
|
+
const selection = { schema: mode.selection, items: expected.items.map(({ session_id, ...item }) => item) };
|
|
4651
4937
|
return withLegacyPlanSnapshot(client, selection, async (current) => {
|
|
4652
4938
|
assertLegacyPlanCurrent(expected, current);
|
|
4653
4939
|
const ids = current.sessions.map((x) => x.id);
|
|
4654
4940
|
const sizes = (await client.query(`SELECT
|
|
4655
4941
|
(SELECT COALESCE(SUM(octet_length(row_to_json(requests)::text)),0) FROM requests WHERE session_id=ANY($1::text[]))::text AS request_bytes,
|
|
4656
4942
|
(SELECT COALESCE(SUM(octet_length(row_to_json(sessions)::text)),0) FROM sessions WHERE id=ANY($1::text[]))::text AS session_bytes`, [ids])).rows[0];
|
|
4657
|
-
if (!sizes || !/^[0-9]+$/.test(sizes.request_bytes) || !/^[0-9]+$/.test(sizes.session_bytes) || BigInt(sizes.request_bytes) + BigInt(sizes.session_bytes) > BigInt(
|
|
4658
|
-
fail(
|
|
4659
|
-
const requests = (await client.query(
|
|
4660
|
-
const sessions = (await client.query(
|
|
4661
|
-
const body = { schema:
|
|
4943
|
+
if (!sizes || !/^[0-9]+$/.test(sizes.request_bytes) || !/^[0-9]+$/.test(sizes.session_bytes) || BigInt(sizes.request_bytes) + BigInt(sizes.session_bytes) > BigInt(mode.originalBytes))
|
|
4944
|
+
fail(`Preservation exceeds ${mode.originalBytes / 1024 / 1024} MiB of original row text; complete sessions cannot be truncated`);
|
|
4945
|
+
const requests = (await client.query(`SELECT id,row_to_json(requests)::text AS original FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
|
|
4946
|
+
const sessions = (await client.query(`SELECT id,row_to_json(sessions)::text AS original FROM sessions WHERE id=ANY($1::text[]) ORDER BY id COLLATE "C" LIMIT ${mode.sessions + 1}`, [ids])).rows;
|
|
4947
|
+
const body = { schema: mode.capsule, plan: expected, captured_at: current.snapshot.at, requests, sessions };
|
|
4662
4948
|
return parseLegacyCapsule({ ...body, capsule_sha256: sha2(JSON.stringify(body)) });
|
|
4663
4949
|
});
|
|
4664
4950
|
}
|
|
@@ -4689,11 +4975,16 @@ function readLegacyBytes(path, maxBytes = MAX_BYTES) {
|
|
|
4689
4975
|
}
|
|
4690
4976
|
function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
|
|
4691
4977
|
const bytes = readLegacyBytes(path, maxBytes);
|
|
4978
|
+
let value;
|
|
4692
4979
|
try {
|
|
4693
|
-
|
|
4980
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
4694
4981
|
} catch {
|
|
4695
4982
|
fail("Artifact must contain valid UTF-8 JSON");
|
|
4696
4983
|
}
|
|
4984
|
+
const limit = value?.schema === LEGACY_SINGLE_SESSION.plan || value?.schema === LEGACY_SINGLE_SESSION.capsule ? LEGACY_SINGLE_SESSION.capsuleBytes : LEGACY_STANDARD.capsuleBytes;
|
|
4985
|
+
if (bytes.length > limit)
|
|
4986
|
+
fail("Artifact exceeds its schema file bound");
|
|
4987
|
+
return value;
|
|
4697
4988
|
}
|
|
4698
4989
|
function writeLegacyCapsule(path, input) {
|
|
4699
4990
|
const capsule = parseLegacyCapsule(input);
|
|
@@ -4704,7 +4995,7 @@ function writeLegacyCapsule(path, input) {
|
|
|
4704
4995
|
fail("Output parent must be an owner-only directory");
|
|
4705
4996
|
const bytes = Buffer.from(JSON.stringify(capsule) + `
|
|
4706
4997
|
`);
|
|
4707
|
-
if (bytes.length >
|
|
4998
|
+
if (bytes.length > legacyMode("capsule", capsule.schema).capsuleBytes)
|
|
4708
4999
|
fail("Preservation capsule exceeds the file bound");
|
|
4709
5000
|
const fd = openSync2(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
|
|
4710
5001
|
try {
|
|
@@ -4774,8 +5065,8 @@ async function rehearseLegacyRestore(client, input, scope = "transaction") {
|
|
|
4774
5065
|
const table = tables[i], expected = capsule.plan.snapshot.tables[i];
|
|
4775
5066
|
if (table.table_name !== expected.table_name || table.columns_sha256 !== expected.columns_sha256)
|
|
4776
5067
|
fail("Restore target schema differs from the preserved schema");
|
|
4777
|
-
const source =
|
|
4778
|
-
const temp =
|
|
5068
|
+
const source = quote3(table.schema_name) + "." + quote3(table.table_name);
|
|
5069
|
+
const temp = quote3("economy_restore_" + randomUUID3().replaceAll("-", ""));
|
|
4779
5070
|
await client.query("CREATE TEMP TABLE " + temp + " (LIKE " + source + " INCLUDING ALL) ON COMMIT DROP");
|
|
4780
5071
|
const originals = table.table_name === "requests" ? capsule.requests : capsule.sessions;
|
|
4781
5072
|
restoreTables.push({ source, temp, originals });
|
|
@@ -4835,15 +5126,16 @@ async function runLegacyPreservation(command, options) {
|
|
|
4835
5126
|
await pool.end();
|
|
4836
5127
|
}
|
|
4837
5128
|
}
|
|
4838
|
-
var MAX_BYTES,
|
|
5129
|
+
var MAX_BYTES, sha2 = (s) => createHash7("sha256").update(s).digest("hex"), fail = (message) => {
|
|
4839
5130
|
throw Error(message);
|
|
4840
5131
|
}, record = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : fail("Expected a JSON object"), hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v) ? v : fail("Expected a lowercase SHA-256 digest"), identifier3 = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024 && !v.includes("\x00") ? v : fail("Expected a nonempty identifier of at most 1024 characters without NUL"), fields = (o, names) => {
|
|
4841
5132
|
if (Object.keys(o).sort().join(",") !== [...names].sort().join(","))
|
|
4842
5133
|
fail("Unexpected or missing preservation fields");
|
|
4843
|
-
}, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b),
|
|
5134
|
+
}, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b), quote3 = (s) => '"' + s.replaceAll('"', '""') + '"';
|
|
4844
5135
|
var init_legacy_preservation = __esm(() => {
|
|
4845
5136
|
init_legacy_plan();
|
|
4846
|
-
|
|
5137
|
+
init_legacy_limits();
|
|
5138
|
+
MAX_BYTES = LEGACY_SINGLE_SESSION.capsuleBytes;
|
|
4847
5139
|
});
|
|
4848
5140
|
|
|
4849
5141
|
// src/server/legacy-operations.ts
|
|
@@ -4858,7 +5150,7 @@ __export(exports_legacy_operations, {
|
|
|
4858
5150
|
applyLegacyReconciliation: () => applyLegacyReconciliation,
|
|
4859
5151
|
LegacyOperationInputError: () => LegacyOperationInputError
|
|
4860
5152
|
});
|
|
4861
|
-
import { createHash as
|
|
5153
|
+
import { createHash as createHash8 } from "crypto";
|
|
4862
5154
|
import { isAbsolute as isAbsolute3 } from "path";
|
|
4863
5155
|
function parseLegacyIntent(action, value) {
|
|
4864
5156
|
try {
|
|
@@ -4907,6 +5199,7 @@ function matchIntent(intent, capsule, target) {
|
|
|
4907
5199
|
}
|
|
4908
5200
|
async function verifyOriginals(client, capsule) {
|
|
4909
5201
|
const p = capsule.plan;
|
|
5202
|
+
const mode = legacyMode("capsule", capsule.schema);
|
|
4910
5203
|
const context2 = (await client.query("SELECT current_database() AS database,(SELECT oid::text FROM pg_database WHERE datname=current_database()) AS database_oid")).rows[0];
|
|
4911
5204
|
if (context2?.database !== p.snapshot.database || context2?.database_oid !== p.snapshot.database_oid)
|
|
4912
5205
|
fail2("Legacy operation targets a different database");
|
|
@@ -4920,7 +5213,7 @@ async function verifyOriginals(client, capsule) {
|
|
|
4920
5213
|
fail2("Legacy session originals changed; preserve later work and prepare a new review");
|
|
4921
5214
|
const rows = (await client.query(`SELECT id,session_id,encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256,
|
|
4922
5215
|
CASE ${LEGACY_FAMILIES.map((f) => "WHEN " + LEGACY_PREDICATES[f] + " THEN '" + f + "'").join(" ")} END AS family
|
|
4923
|
-
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT
|
|
5216
|
+
FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
|
|
4924
5217
|
if (rows.length !== capsule.requests.length)
|
|
4925
5218
|
fail2("Legacy request membership changed; preserve later work and prepare a new review");
|
|
4926
5219
|
const originals = new Map(capsule.requests.map((r) => [r.id, sha3(r.original)]));
|
|
@@ -4974,13 +5267,14 @@ async function observed(client, run) {
|
|
|
4974
5267
|
}
|
|
4975
5268
|
function validateReceipt(value) {
|
|
4976
5269
|
const v = value;
|
|
4977
|
-
if (!v ||
|
|
5270
|
+
if (!v || !["apply", "rollback"].includes(v.action))
|
|
4978
5271
|
fail2("Invalid stored legacy receipt");
|
|
5272
|
+
const mode = legacyMode("receipt", v.schema);
|
|
4979
5273
|
key(v.operation_key);
|
|
4980
5274
|
key(v.apply_key);
|
|
4981
5275
|
digest3(v.plan_sha256);
|
|
4982
5276
|
digest3(v.capsule_sha256);
|
|
4983
|
-
if (v.source_rows_changed !== 0 || ![v.target_requests, v.preserved_requests, v.sessions].every((n) => Number.isSafeInteger(n) && n > 0 && n <=
|
|
5277
|
+
if (v.source_rows_changed !== 0 || ![v.target_requests, v.preserved_requests, v.sessions].every((n) => Number.isSafeInteger(n) && n > 0 && n <= mode.members) || v.target_requests > mode.items || v.sessions > mode.sessions || typeof v.created_at !== "string" || !Number.isFinite(Date.parse(v.created_at)))
|
|
4984
5278
|
fail2("Invalid stored legacy receipt fields");
|
|
4985
5279
|
const names = ["schema", "action", "operation_key", "apply_key", "plan_sha256", "capsule_sha256", "target_requests", "preserved_requests", "sessions", "source_rows_changed", "created_at"];
|
|
4986
5280
|
if (Object.keys(v).sort().join(",") !== names.sort().join(","))
|
|
@@ -5005,7 +5299,7 @@ async function reply(client, row, expectedHash) {
|
|
|
5005
5299
|
}
|
|
5006
5300
|
function receipt(action, intent, capsule) {
|
|
5007
5301
|
return {
|
|
5008
|
-
schema: "
|
|
5302
|
+
schema: legacyMode("capsule", capsule.schema).receipt,
|
|
5009
5303
|
action,
|
|
5010
5304
|
operation_key: intent.operation_key,
|
|
5011
5305
|
apply_key: action === "apply" ? intent.operation_key : intent.apply_key,
|
|
@@ -5045,8 +5339,9 @@ async function applyLegacyReconciliation(client, input, preserved, target) {
|
|
|
5045
5339
|
const saved = (await client.query("SELECT capsule_json,capsule_sha256 FROM legacy_reconciliations WHERE operation_key=$1", [intent.operation_key])).rows[0];
|
|
5046
5340
|
if (saved?.capsule_json !== json3 || saved?.capsule_sha256 !== capsule.capsule_sha256 || parseLegacyCapsule(JSON.parse(saved.capsule_json)).capsule_sha256 !== capsule.capsule_sha256)
|
|
5047
5341
|
fail2("Durable preservation readback failed");
|
|
5048
|
-
|
|
5049
|
-
|
|
5342
|
+
await client.query(`INSERT INTO legacy_reconciliation_targets(operation_key,request_id,session_id,disposition,original_sha256)
|
|
5343
|
+
SELECT $1, x.id, x.session_id, x.disposition, x.original_sha256
|
|
5344
|
+
FROM jsonb_to_recordset($2::jsonb) AS x(id text,session_id text,disposition text,original_sha256 text)`, [intent.operation_key, JSON.stringify(capsule.plan.items.map(({ id, session_id, disposition, original_sha256 }) => ({ id, session_id, disposition, original_sha256 })))]);
|
|
5050
5345
|
const stored = await lookup(client, "apply", intent.operation_key) ?? fail2("Legacy receipt readback failed");
|
|
5051
5346
|
if (stored.response_json !== responseJson)
|
|
5052
5347
|
fail2("Legacy receipt readback failed");
|
|
@@ -5150,7 +5445,7 @@ async function runLegacyOperation(command, options) {
|
|
|
5150
5445
|
await pool.end();
|
|
5151
5446
|
}
|
|
5152
5447
|
}
|
|
5153
|
-
var sha3 = (s) =>
|
|
5448
|
+
var sha3 = (s) => createHash8("sha256").update(s).digest("hex"), fail2 = (s) => {
|
|
5154
5449
|
throw Error(s);
|
|
5155
5450
|
}, digest3 = (v, label = "digest") => typeof v === "string" && /^[a-f0-9]{64}$/.test(v) ? v : fail2(label + " must be a lowercase SHA-256 digest"), key = (v, label = "operation_key") => typeof v === "string" && /^[A-Za-z0-9._:-]{1,200}$/.test(v) ? v : fail2(label + " must contain 1-200 letters, digits, dot, underscore, colon or hyphen"), reference = (v, label) => typeof v === "string" && /^[A-Za-z0-9:/._-]{1,512}$/.test(v) ? v : fail2(label + " must contain 1-512 letters, digits, colon, slash, dot, underscore or hyphen"), LegacyOperationInputError, TABLES_SQL = `SELECT c.relname AS table_name,n.nspname AS schema_name,c.oid::text AS relation_oid,
|
|
5156
5451
|
encode(sha256(convert_to(json_agg(json_build_array(a.attnum,a.attname,a.atttypid::text,a.atttypmod,a.attnotnull,a.attcollation::text) ORDER BY a.attnum)::text,'UTF8')),'hex') AS columns_sha256
|
|
@@ -5158,6 +5453,7 @@ var sha3 = (s) => createHash7("sha256").update(s).digest("hex"), fail2 = (s) =>
|
|
|
5158
5453
|
WHERE c.oid IN ('requests'::regclass,'sessions'::regclass) AND a.attnum>0 AND NOT a.attisdropped
|
|
5159
5454
|
GROUP BY c.oid,c.relname,n.nspname ORDER BY c.relname`, same2 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
5160
5455
|
var init_legacy_operations = __esm(() => {
|
|
5456
|
+
init_legacy_limits();
|
|
5161
5457
|
init_pg_connection();
|
|
5162
5458
|
init_legacy_inventory();
|
|
5163
5459
|
init_legacy_preservation();
|
|
@@ -5194,7 +5490,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5194
5490
|
// package.json
|
|
5195
5491
|
var package_default = {
|
|
5196
5492
|
name: "@hasna/economy",
|
|
5197
|
-
version: "0.5.
|
|
5493
|
+
version: "0.5.2",
|
|
5198
5494
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5199
5495
|
repository: {
|
|
5200
5496
|
type: "git",
|
|
@@ -5289,7 +5585,16 @@ var package_default = {
|
|
|
5289
5585
|
},
|
|
5290
5586
|
trustedDependencies: [
|
|
5291
5587
|
"@hasna/projects"
|
|
5292
|
-
]
|
|
5588
|
+
],
|
|
5589
|
+
overrides: {
|
|
5590
|
+
"fast-uri": "3.1.8",
|
|
5591
|
+
hono: "4.13.7",
|
|
5592
|
+
"ip-address": "10.7.1",
|
|
5593
|
+
nanoid: "5.1.16",
|
|
5594
|
+
"path-to-regexp": "8.4.0",
|
|
5595
|
+
undici: "6.28.1",
|
|
5596
|
+
"express-rate-limit": "8.7.0"
|
|
5597
|
+
}
|
|
5293
5598
|
};
|
|
5294
5599
|
|
|
5295
5600
|
// src/lib/package-metadata.ts
|
|
@@ -12457,10 +12762,12 @@ function parsePort(raw, label) {
|
|
|
12457
12762
|
async function runMigrate() {
|
|
12458
12763
|
const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
12459
12764
|
const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
|
|
12765
|
+
const { repairLegacyCostCenterSchema: repairLegacyCostCenterSchema2 } = await Promise.resolve().then(() => (init_pg_legacy_schema(), exports_pg_legacy_schema));
|
|
12460
12766
|
const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
|
|
12461
12767
|
const dsn = getCloudDatabaseUrl2();
|
|
12462
12768
|
if (!dsn)
|
|
12463
12769
|
throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
|
|
12770
|
+
const repair = await repairLegacyCostCenterSchema2(dsn);
|
|
12464
12771
|
const result = await applyPgMigrations2(dsn, cloudMigrations2(), "economy");
|
|
12465
12772
|
const pool = createCloudPool2(dsn);
|
|
12466
12773
|
try {
|
|
@@ -12468,7 +12775,7 @@ async function runMigrate() {
|
|
|
12468
12775
|
} finally {
|
|
12469
12776
|
await pool.end().catch(() => {});
|
|
12470
12777
|
}
|
|
12471
|
-
console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, api_keys: "ensured" }));
|
|
12778
|
+
console.log(JSON.stringify({ evt: "migrate", service: result.service, applied: result.applied, alreadyApplied: result.alreadyApplied, errors: result.errors, repair, api_keys: "ensured" }));
|
|
12472
12779
|
if (result.errors.length)
|
|
12473
12780
|
process.exit(1);
|
|
12474
12781
|
}
|