@creatornader/agent-bridge 0.3.1 → 0.5.0
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 +222 -1
- package/README.md +312 -12
- package/ROADMAP.md +88 -7
- package/SECURITY.md +78 -5
- package/SKILL.md +108 -1
- package/dist/archive.d.ts +2 -2
- package/dist/archive.js +2 -2
- package/dist/{bridge-service-hX1ZQp4W.d.ts → bridge-service-CNBKdYXh.d.ts} +2 -1
- package/dist/{bridge-store-DPVSCwfs.d.ts → bridge-store-ByJ6CaJk.d.ts} +3 -0
- package/dist/bridge.d.ts +25 -8
- package/dist/bridge.js +19 -9
- package/dist/{chunk-MBPZXTJA.js → chunk-7VJI2MXI.js} +285 -42
- package/dist/chunk-7VJI2MXI.js.map +1 -0
- package/dist/{chunk-4VB44KJU.js → chunk-AWE3DFZV.js} +3 -2
- package/dist/chunk-AWE3DFZV.js.map +1 -0
- package/dist/{chunk-GADH244D.js → chunk-E4OWHGUE.js} +16 -5
- package/dist/chunk-E4OWHGUE.js.map +1 -0
- package/dist/{chunk-NCO7RLBM.js → chunk-GTK4KOED.js} +108 -8
- package/dist/chunk-GTK4KOED.js.map +1 -0
- package/dist/{chunk-SPSMZOCJ.js → chunk-HUHG7XTB.js} +6 -5
- package/dist/chunk-HUHG7XTB.js.map +1 -0
- package/dist/{chunk-5GMB3A77.js → chunk-IVG4XWWS.js} +26 -14
- package/dist/chunk-IVG4XWWS.js.map +1 -0
- package/dist/{chunk-WAI2IYIQ.js → chunk-P635FAN4.js} +50 -7
- package/dist/chunk-P635FAN4.js.map +1 -0
- package/dist/{chunk-45BZGB77.js → chunk-QNXHDOJ5.js} +218 -24
- package/dist/chunk-QNXHDOJ5.js.map +1 -0
- package/dist/{chunk-UAHMP6GC.js → chunk-QPC6JHA3.js} +10 -7
- package/dist/chunk-QPC6JHA3.js.map +1 -0
- package/dist/{chunk-Q6PPMXJC.js → chunk-SN7PIVDF.js} +377 -21
- package/dist/chunk-SN7PIVDF.js.map +1 -0
- package/dist/{chunk-A6W4QALF.js → chunk-TC6A7GMF.js} +2 -2
- package/dist/{chunk-2DFNOLMO.js → chunk-XDNT6ZNZ.js} +9 -6
- package/dist/chunk-XDNT6ZNZ.js.map +1 -0
- package/dist/{chunk-CEXBMOI5.js → chunk-Z6JP6MYD.js} +2 -2
- package/dist/cli.js +5867 -438
- package/dist/cli.js.map +1 -1
- package/dist/client-runtime.d.ts +4 -2
- package/dist/client-runtime.js +7 -7
- package/dist/contracts/generate.js +1 -1
- package/dist/dr.d.ts +9 -2
- package/dist/dr.js +6 -4
- package/dist/{gateway-CggQ8vTC.d.ts → gateway-CW-61lvs.d.ts} +27 -3
- package/dist/gateway-main.js +71 -7
- package/dist/gateway-main.js.map +1 -1
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -3
- package/dist/index.js +8 -8
- package/dist/{postgres-bridge-store-Bfd3hRId.d.ts → postgres-bridge-store-C9KG9Z69.d.ts} +1 -1
- package/dist/postgres-production-preflight.d.ts +32 -0
- package/dist/postgres-production-preflight.js +249 -0
- package/dist/postgres-production-preflight.js.map +1 -0
- package/dist/server.js +8 -8
- package/dist/sqlite-edge-store-CdMIOrho.d.ts +148 -0
- package/dist/sqlite.d.ts +19 -80
- package/dist/sqlite.js +11 -3
- package/docs/architecture-v2.md +292 -15
- package/docs/decisions/0004-client-lifecycle-and-endpoint-migration.md +204 -0
- package/docs/deployment.md +349 -0
- package/docs/troubleshooting.md +23 -0
- package/llms.txt +69 -1
- package/openapi/agent-bridge-v2.openapi.json +1151 -124
- package/package.json +10 -2
- package/schemas/agent-bridge-v2.capabilities.json +26 -1
- package/schemas/agent-bridge-v2.mcp.json +11 -1
- package/schemas/agent-bridge-v2.schema.json +167 -0
- package/sql/migrations/007_runtime_role.sql +8 -4
- package/sql/migrations/013_row_isolation.sql +71 -8
- package/sql/migrations/014_owner_control_plane.sql +124 -31
- package/sql/migrations/015_portable_archives.sql +143 -41
- package/sql/migrations/016_native_dr_fence.sql +102 -35
- package/sql/migrations/017_gateway_authority_binding.sql +99 -0
- package/sql/migrations/018_endpoint_migration_challenges.sql +1054 -0
- package/sql/migrations/019_endpoint_migration_same_successor.sql +348 -0
- package/sql/migrations/020_managed_authority_compat.sql +698 -0
- package/sql/migrations/021_native_backup_reader.sql +421 -0
- package/sql/setup.sql +1 -3
- package/dist/chunk-2DFNOLMO.js.map +0 -1
- package/dist/chunk-45BZGB77.js.map +0 -1
- package/dist/chunk-4VB44KJU.js.map +0 -1
- package/dist/chunk-5GMB3A77.js.map +0 -1
- package/dist/chunk-GADH244D.js.map +0 -1
- package/dist/chunk-MBPZXTJA.js.map +0 -1
- package/dist/chunk-NCO7RLBM.js.map +0 -1
- package/dist/chunk-Q6PPMXJC.js.map +0 -1
- package/dist/chunk-SPSMZOCJ.js.map +0 -1
- package/dist/chunk-UAHMP6GC.js.map +0 -1
- package/dist/chunk-WAI2IYIQ.js.map +0 -1
- /package/dist/{chunk-A6W4QALF.js.map → chunk-TC6A7GMF.js.map} +0 -0
- /package/dist/{chunk-CEXBMOI5.js.map → chunk-Z6JP6MYD.js.map} +0 -0
|
@@ -10,18 +10,18 @@ import {
|
|
|
10
10
|
isSupportedLocalSqliteSchemaContract,
|
|
11
11
|
sqliteSchemaContractHash,
|
|
12
12
|
verifySqliteHealth
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-IVG4XWWS.js";
|
|
14
14
|
import {
|
|
15
15
|
preparePrivateFileLocation,
|
|
16
16
|
preparePrivateSqliteLocation,
|
|
17
17
|
securePrivatePath,
|
|
18
18
|
securePrivateSqliteFiles,
|
|
19
19
|
verifyPrivatePathAccess
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-GTK4KOED.js";
|
|
21
21
|
import {
|
|
22
22
|
NATIVE_DR_ADVISORY_LOCK_KEY,
|
|
23
23
|
rowIsolationReady
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-QNXHDOJ5.js";
|
|
25
25
|
|
|
26
26
|
// src/native-dr-bundle.ts
|
|
27
27
|
import { createHash } from "crypto";
|
|
@@ -88,12 +88,19 @@ var POSTGRES_SCHEMA_FIELDS = [
|
|
|
88
88
|
"claimedDeliveryCount",
|
|
89
89
|
"readinessAttestations"
|
|
90
90
|
];
|
|
91
|
-
var
|
|
91
|
+
var POSTGRES_LEGACY_EXCLUDED_DATA = [
|
|
92
92
|
"agent_bridge.agent_instances",
|
|
93
93
|
"agent_bridge.rate_limit_buckets",
|
|
94
94
|
"agent_bridge.request_authorities",
|
|
95
95
|
"agent_bridge.archive_transaction_authorizations"
|
|
96
96
|
];
|
|
97
|
+
var POSTGRES_EXCLUDED_DATA = [
|
|
98
|
+
...POSTGRES_LEGACY_EXCLUDED_DATA,
|
|
99
|
+
"agent_bridge.endpoint_migration_challenges"
|
|
100
|
+
];
|
|
101
|
+
function postgresExcludedDataForSchemaVersion(schemaVersion) {
|
|
102
|
+
return schemaVersion <= 17 ? POSTGRES_LEGACY_EXCLUDED_DATA : POSTGRES_EXCLUDED_DATA;
|
|
103
|
+
}
|
|
97
104
|
function validateSchema(kind, value) {
|
|
98
105
|
if (kind === "sqlite") {
|
|
99
106
|
const schema2 = exactObject(value, ["applicationId", "schemaContractSha256", "userVersion"], "DR SQLite schema");
|
|
@@ -106,7 +113,7 @@ function validateSchema(kind, value) {
|
|
|
106
113
|
}
|
|
107
114
|
assertString(schema.databaseName, "DR database name");
|
|
108
115
|
assertString(schema.pgDumpVersion, "DR pg_dump version");
|
|
109
|
-
if (!Array.isArray(schema.migrations) || schema.migrations.length > 1e4) throw new NativeDrBundleError("DR PostgreSQL migration inventory is invalid");
|
|
116
|
+
if (!Array.isArray(schema.migrations) || schema.migrations.length === 0 || schema.migrations.length > 1e4) throw new NativeDrBundleError("DR PostgreSQL migration inventory is invalid");
|
|
110
117
|
let priorVersion = 0;
|
|
111
118
|
for (const raw of schema.migrations) {
|
|
112
119
|
const migration = exactObject(raw, ["checksum", "name", "version"], "DR migration");
|
|
@@ -115,13 +122,17 @@ function validateSchema(kind, value) {
|
|
|
115
122
|
}
|
|
116
123
|
priorVersion = Number(migration.version);
|
|
117
124
|
}
|
|
125
|
+
if (priorVersion !== Number(schema.schemaVersion)) {
|
|
126
|
+
throw new NativeDrBundleError("DR PostgreSQL schema version does not match migrations");
|
|
127
|
+
}
|
|
118
128
|
if (!schema.tableCounts || typeof schema.tableCounts !== "object" || Array.isArray(schema.tableCounts)) throw new NativeDrBundleError("DR PostgreSQL table counts are invalid");
|
|
119
129
|
for (const [table, count] of Object.entries(schema.tableCounts)) {
|
|
120
130
|
if (!/^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$/.test(table) || typeof count !== "string" || !/^(0|[1-9][0-9]*)$/.test(count)) {
|
|
121
131
|
throw new NativeDrBundleError("DR PostgreSQL table counts are invalid");
|
|
122
132
|
}
|
|
123
133
|
}
|
|
124
|
-
|
|
134
|
+
const excludedData = postgresExcludedDataForSchemaVersion(Number(schema.schemaVersion));
|
|
135
|
+
if (!Array.isArray(schema.excludedDataTables) || schema.excludedDataTables.length !== excludedData.length || schema.excludedDataTables.some((name, index) => name !== excludedData[index])) {
|
|
125
136
|
throw new NativeDrBundleError("DR PostgreSQL excluded-data inventory is invalid");
|
|
126
137
|
}
|
|
127
138
|
const readiness = exactObject(schema.readinessAttestations, [
|
|
@@ -453,12 +464,19 @@ function createBoundedToolOutput(maximumBytes) {
|
|
|
453
464
|
var POSTGRES_NATIVE_DR_ROLE_SCHEMA = "agent-bridge.postgres-native-dr-roles";
|
|
454
465
|
var POSTGRES_NATIVE_DR_ROLE_VERSION = 1;
|
|
455
466
|
var POSTGRES_NATIVE_DR_SERVICE = "agent_bridge_dr";
|
|
456
|
-
var
|
|
467
|
+
var POSTGRES_NATIVE_DR_LEGACY_EXCLUDED_DATA_TABLES = [
|
|
457
468
|
"agent_bridge.agent_instances",
|
|
458
469
|
"agent_bridge.rate_limit_buckets",
|
|
459
470
|
"agent_bridge.request_authorities",
|
|
460
471
|
"agent_bridge.archive_transaction_authorizations"
|
|
461
472
|
];
|
|
473
|
+
var POSTGRES_NATIVE_DR_EXCLUDED_DATA_TABLES = [
|
|
474
|
+
...POSTGRES_NATIVE_DR_LEGACY_EXCLUDED_DATA_TABLES,
|
|
475
|
+
"agent_bridge.endpoint_migration_challenges"
|
|
476
|
+
];
|
|
477
|
+
function excludedDataTablesForSchemaVersion(schemaVersion) {
|
|
478
|
+
return schemaVersion <= 17 ? POSTGRES_NATIVE_DR_LEGACY_EXCLUDED_DATA_TABLES : POSTGRES_NATIVE_DR_EXCLUDED_DATA_TABLES;
|
|
479
|
+
}
|
|
462
480
|
var SSL_PARAMETERS = /* @__PURE__ */ new Set([
|
|
463
481
|
"channel_binding",
|
|
464
482
|
"sslcert",
|
|
@@ -469,7 +487,7 @@ var SSL_PARAMETERS = /* @__PURE__ */ new Set([
|
|
|
469
487
|
"sslrootcert",
|
|
470
488
|
"sslsni"
|
|
471
489
|
]);
|
|
472
|
-
var DERIVED_ROLE = /^agent_bridge_(?:runtime|data_owner|context_reader|event_writer|control_owner|control_operator|control_auditor|archive_operator)_[0-9a-f]{16}$/;
|
|
490
|
+
var DERIVED_ROLE = /^agent_bridge_(?:runtime|data_owner|context_reader|event_writer|control_owner|control_operator|control_auditor|archive_operator|backup_reader)_[0-9a-f]{16}$/;
|
|
473
491
|
var ROLE_NAME = /^[^\u0000-\u001f\u007f]{1,63}$/u;
|
|
474
492
|
function validPostgresIdentifier(value) {
|
|
475
493
|
return value !== "PUBLIC" && !value.includes(":") && ROLE_NAME.test(value) && Buffer.byteLength(value, "utf8") <= 63;
|
|
@@ -579,6 +597,9 @@ function createPostgresDrLibpqFiles(connectionUrl, directory) {
|
|
|
579
597
|
};
|
|
580
598
|
}
|
|
581
599
|
function buildPgDumpArgs(outputPath, snapshot, serviceName = POSTGRES_NATIVE_DR_SERVICE) {
|
|
600
|
+
return buildPgDumpArgsForTables(outputPath, snapshot, serviceName, POSTGRES_NATIVE_DR_EXCLUDED_DATA_TABLES);
|
|
601
|
+
}
|
|
602
|
+
function buildPgDumpArgsForTables(outputPath, snapshot, serviceName, excludedDataTables) {
|
|
582
603
|
assertSafeText(outputPath, "PostgreSQL dump path");
|
|
583
604
|
assertSafeText(snapshot, "PostgreSQL snapshot");
|
|
584
605
|
assertSafeText(serviceName, "PostgreSQL service name");
|
|
@@ -588,7 +609,7 @@ function buildPgDumpArgs(outputPath, snapshot, serviceName = POSTGRES_NATIVE_DR_
|
|
|
588
609
|
"--schema=agent_bridge",
|
|
589
610
|
"--no-tablespaces",
|
|
590
611
|
`--snapshot=${snapshot}`,
|
|
591
|
-
...
|
|
612
|
+
...excludedDataTables.map((table) => `--exclude-table-data=${table}`),
|
|
592
613
|
`--dbname=service=${serviceName}`
|
|
593
614
|
];
|
|
594
615
|
}
|
|
@@ -661,6 +682,29 @@ var POSTGRES_TOC_DESCRIPTORS = [
|
|
|
661
682
|
"ACL"
|
|
662
683
|
];
|
|
663
684
|
var POSTGRES_TOC_MAX_BYTES = 16 * 1024 * 1024;
|
|
685
|
+
function parsePostgresDumpTocBody(entry) {
|
|
686
|
+
let cursor = 0;
|
|
687
|
+
const readDigits = () => {
|
|
688
|
+
const start = cursor;
|
|
689
|
+
while (cursor < entry.length) {
|
|
690
|
+
const code = entry.charCodeAt(cursor);
|
|
691
|
+
if (code < 48 || code > 57) break;
|
|
692
|
+
cursor += 1;
|
|
693
|
+
}
|
|
694
|
+
return cursor > start;
|
|
695
|
+
};
|
|
696
|
+
const readSeparator = () => {
|
|
697
|
+
const start = cursor;
|
|
698
|
+
while (entry[cursor] === " " || entry[cursor] === " ") cursor += 1;
|
|
699
|
+
return cursor > start;
|
|
700
|
+
};
|
|
701
|
+
if (!readDigits() || entry[cursor] !== ";") return void 0;
|
|
702
|
+
cursor += 1;
|
|
703
|
+
if (!readSeparator() || !readDigits() || !readSeparator() || !readDigits() || !readSeparator()) {
|
|
704
|
+
return void 0;
|
|
705
|
+
}
|
|
706
|
+
return cursor < entry.length ? entry.slice(cursor) : void 0;
|
|
707
|
+
}
|
|
664
708
|
function validatePostgresDumpToc(text) {
|
|
665
709
|
if (Buffer.byteLength(text, "utf8") > POSTGRES_TOC_MAX_BYTES) {
|
|
666
710
|
throw new PostgresNativeDrError("PG_DUMP_TOC_INVALID", "PostgreSQL dump table of contents exceeds 16 MiB");
|
|
@@ -668,9 +712,8 @@ function validatePostgresDumpToc(text) {
|
|
|
668
712
|
const entries = text.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith(";"));
|
|
669
713
|
if (entries.length === 0) throw new PostgresNativeDrError("PG_DUMP_TOC_INVALID", "PostgreSQL dump table of contents is empty");
|
|
670
714
|
for (const entry of entries) {
|
|
671
|
-
const
|
|
672
|
-
if (
|
|
673
|
-
const body = match[1];
|
|
715
|
+
const body = parsePostgresDumpTocBody(entry);
|
|
716
|
+
if (body === void 0) throw new PostgresNativeDrError("PG_DUMP_TOC_INVALID", "PostgreSQL dump table of contents has an invalid record");
|
|
674
717
|
const descriptor = POSTGRES_TOC_DESCRIPTORS.find((candidate) => body.startsWith(`${candidate} `));
|
|
675
718
|
if (!descriptor) throw new PostgresNativeDrError("PG_DUMP_TOC_OUT_OF_SCOPE", "PostgreSQL dump contains an unsupported object kind");
|
|
676
719
|
const remainder = body.slice(descriptor.length + 1).split(/\s+/u);
|
|
@@ -891,21 +934,62 @@ function buildPostgresMembershipStatements(inventory, serverMajor2) {
|
|
|
891
934
|
if (!Number.isSafeInteger(serverMajor2) || serverMajor2 < 15 || serverMajor2 > 18) {
|
|
892
935
|
throw new Error("PostgreSQL membership target major must be between 15 and 18");
|
|
893
936
|
}
|
|
894
|
-
|
|
937
|
+
const validated = validatePostgresRoleInventory(inventory);
|
|
938
|
+
const schemaOwner = validated.roles.find((role) => role.kind === "schema-owner");
|
|
939
|
+
const rolesByName = new Map(validated.roles.map((role) => [role.name, role]));
|
|
940
|
+
const orderedMemberships = [...validated.memberships].sort((left, right) => {
|
|
941
|
+
const leftOwnerAuthority = left.member === schemaOwner.name ? 0 : 1;
|
|
942
|
+
const rightOwnerAuthority = right.member === schemaOwner.name ? 0 : 1;
|
|
943
|
+
return leftOwnerAuthority - rightOwnerAuthority || left.role.localeCompare(right.role) || left.member.localeCompare(right.member);
|
|
944
|
+
});
|
|
945
|
+
return orderedMemberships.filter(
|
|
946
|
+
(membership) => rolesByName.get(membership.member).kind !== "external-principal"
|
|
947
|
+
).map((membership) => {
|
|
948
|
+
let grant;
|
|
895
949
|
if (serverMajor2 === 15) {
|
|
896
950
|
if (!membership.inheritOption || !membership.setOption) {
|
|
897
951
|
throw new Error("PostgreSQL 15 cannot preserve membership INHERIT FALSE or SET FALSE semantics");
|
|
898
952
|
}
|
|
899
|
-
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
953
|
+
grant = `GRANT ${quoteIdentifier(membership.role)} TO ${quoteIdentifier(membership.member)}${membership.adminOption ? " WITH ADMIN OPTION" : ""};`;
|
|
954
|
+
} else {
|
|
955
|
+
const options = [
|
|
956
|
+
`ADMIN ${membership.adminOption ? "OPTION" : "FALSE"}`,
|
|
957
|
+
`INHERIT ${membership.inheritOption ? "TRUE" : "FALSE"}`,
|
|
958
|
+
`SET ${membership.setOption ? "TRUE" : "FALSE"}`
|
|
959
|
+
].join(", ");
|
|
960
|
+
grant = `GRANT ${quoteIdentifier(membership.role)} TO ${quoteIdentifier(membership.member)} WITH ${options};`;
|
|
961
|
+
}
|
|
962
|
+
const role = rolesByName.get(membership.role);
|
|
963
|
+
const schemaOwnerGrantedDerivedMembership = membership.member !== schemaOwner.name && role.kind === "derived";
|
|
964
|
+
return schemaOwnerGrantedDerivedMembership ? `SET LOCAL ROLE ${quoteIdentifier(schemaOwner.name)}; ${grant} RESET ROLE;` : grant;
|
|
907
965
|
});
|
|
908
966
|
}
|
|
967
|
+
function suspendedPostgresMemberships(inventory) {
|
|
968
|
+
const suffix = createHash2("md5").update(inventory.databaseName).digest("hex").slice(0, 16);
|
|
969
|
+
const recognizedRoles = /* @__PURE__ */ new Map([
|
|
970
|
+
[`agent_bridge_control_operator_${suffix}`, "operator"],
|
|
971
|
+
[`agent_bridge_control_auditor_${suffix}`, "auditor"],
|
|
972
|
+
[`agent_bridge_archive_operator_${suffix}`, "archive"]
|
|
973
|
+
]);
|
|
974
|
+
const rolesByName = new Map(inventory.roles.map((role) => [role.name, role]));
|
|
975
|
+
const suspended = [];
|
|
976
|
+
for (const membership of inventory.memberships) {
|
|
977
|
+
if (rolesByName.get(membership.member)?.kind !== "external-principal") continue;
|
|
978
|
+
const recognized = recognizedRoles.get(membership.role);
|
|
979
|
+
if (recognized === "archive") {
|
|
980
|
+
suspended.push({ kind: "archive", member: membership.member });
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (recognized === "operator" || recognized === "auditor") {
|
|
984
|
+
suspended.push({
|
|
985
|
+
kind: "control",
|
|
986
|
+
member: membership.member,
|
|
987
|
+
controlRole: recognized
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return suspended;
|
|
992
|
+
}
|
|
909
993
|
var DEFAULT_PRIVILEGE_OBJECTS = {
|
|
910
994
|
r: "TABLES",
|
|
911
995
|
S: "SEQUENCES",
|
|
@@ -1178,7 +1262,8 @@ async function collectPostgresRoleInventory(client, databaseName) {
|
|
|
1178
1262
|
FROM unnest(ARRAY[
|
|
1179
1263
|
'agent_bridge_runtime_','agent_bridge_data_owner_','agent_bridge_context_reader_',
|
|
1180
1264
|
'agent_bridge_event_writer_','agent_bridge_control_owner_','agent_bridge_control_operator_',
|
|
1181
|
-
'agent_bridge_control_auditor_','agent_bridge_archive_operator_'
|
|
1265
|
+
'agent_bridge_control_auditor_','agent_bridge_archive_operator_',
|
|
1266
|
+
'agent_bridge_backup_reader_'
|
|
1182
1267
|
]) prefix
|
|
1183
1268
|
), object_roles AS (
|
|
1184
1269
|
SELECT namespace.nspowner AS oid, 'schema-owner'::text AS kind
|
|
@@ -1257,13 +1342,14 @@ async function collectPostgresRoleInventory(client, databaseName) {
|
|
|
1257
1342
|
const names = roleRows.rows.map((row) => row.name);
|
|
1258
1343
|
const membershipRows = names.length === 0 ? { rows: [], rowCount: 0 } : await client.query(`
|
|
1259
1344
|
SELECT granted.rolname AS role, member.rolname AS member,
|
|
1260
|
-
membership.admin_option AS "adminOption",
|
|
1261
|
-
coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) AS "inheritOption",
|
|
1262
|
-
coalesce((to_jsonb(membership)->>'set_option')::boolean,true) AS "setOption"
|
|
1345
|
+
bool_or(membership.admin_option) AS "adminOption",
|
|
1346
|
+
bool_or(coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true)) AS "inheritOption",
|
|
1347
|
+
bool_or(coalesce((to_jsonb(membership)->>'set_option')::boolean,true)) AS "setOption"
|
|
1263
1348
|
FROM pg_catalog.pg_auth_members membership
|
|
1264
1349
|
JOIN pg_catalog.pg_roles granted ON granted.oid=membership.roleid
|
|
1265
1350
|
JOIN pg_catalog.pg_roles member ON member.oid=membership.member
|
|
1266
1351
|
WHERE granted.rolname=ANY($1::text[]) AND member.rolname=ANY($1::text[])
|
|
1352
|
+
GROUP BY granted.rolname,member.rolname
|
|
1267
1353
|
ORDER BY granted.rolname,member.rolname`, [names]);
|
|
1268
1354
|
if (names.length > 0) {
|
|
1269
1355
|
const boundary = await client.query(`
|
|
@@ -1350,9 +1436,13 @@ async function collectSnapshotMetadata(client, checkRowReady) {
|
|
|
1350
1436
|
}
|
|
1351
1437
|
}
|
|
1352
1438
|
if (migrations.length === 0) throw new PostgresNativeDrError("SOURCE_SCHEMA_EMPTY", "source has no Agent Bridge migrations");
|
|
1439
|
+
const schemaVersion = Math.max(...migrations.map((migration) => migration.version));
|
|
1353
1440
|
const tableRows = await client.query(`
|
|
1354
|
-
SELECT
|
|
1355
|
-
|
|
1441
|
+
SELECT relation.relname AS "tableName"
|
|
1442
|
+
FROM pg_catalog.pg_class relation
|
|
1443
|
+
JOIN pg_catalog.pg_namespace namespace ON namespace.oid=relation.relnamespace
|
|
1444
|
+
WHERE namespace.nspname='agent_bridge' AND relation.relkind IN ('r','p')
|
|
1445
|
+
ORDER BY relation.relname`);
|
|
1356
1446
|
const tableCounts = {};
|
|
1357
1447
|
for (const row of tableRows.rows) {
|
|
1358
1448
|
const qualified = `agent_bridge.${row.tableName}`;
|
|
@@ -1363,6 +1453,9 @@ async function collectSnapshotMetadata(client, checkRowReady) {
|
|
|
1363
1453
|
if (!value || !/^(?:0|[1-9][0-9]*)$/u.test(value)) throw new PostgresNativeDrError("SOURCE_COUNT_INVALID", `source count is invalid for ${qualified}`);
|
|
1364
1454
|
tableCounts[qualified] = value;
|
|
1365
1455
|
}
|
|
1456
|
+
if (!tableRows.rows.some((row) => row.tableName === "deliveries")) {
|
|
1457
|
+
throw new PostgresNativeDrError("SOURCE_COUNT_INVALID", "source deliveries table is unavailable");
|
|
1458
|
+
}
|
|
1366
1459
|
const claimed = await client.query(
|
|
1367
1460
|
`SELECT count(*)::text AS count FROM agent_bridge.deliveries WHERE state='claimed'`
|
|
1368
1461
|
);
|
|
@@ -1374,13 +1467,15 @@ async function collectSnapshotMetadata(client, checkRowReady) {
|
|
|
1374
1467
|
SELECT agent_bridge.security_schema_ready() AS security,
|
|
1375
1468
|
agent_bridge.owner_control_plane_ready() AS "ownerControl",
|
|
1376
1469
|
agent_bridge.portable_archive_ready() AS "portableArchive",
|
|
1470
|
+
${schemaVersion >= 21 ? 'agent_bridge.native_backup_ready() AS "nativeBackup",' : ""}
|
|
1377
1471
|
agent_bridge.credential_security_prerequisite_definition() AS "securityDefinition",
|
|
1378
1472
|
agent_bridge.row_isolation_catalog_definition() AS "rowIsolationDefinition",
|
|
1379
1473
|
agent_bridge.owner_control_attestation_definition() AS "ownerControlDefinition",
|
|
1380
1474
|
agent_bridge.portable_archive_attestation_definition() AS "portableArchiveDefinition"`);
|
|
1381
1475
|
const ready = readiness.rows[0];
|
|
1382
1476
|
const rowReady = await checkRowReady(client);
|
|
1383
|
-
|
|
1477
|
+
const authorityReady = await gatewayAuthorityReady(client, schemaVersion);
|
|
1478
|
+
if (!ready?.security || !rowReady || !ready.ownerControl || !ready.portableArchive || schemaVersion >= 21 && !ready.nativeBackup || !authorityReady || typeof ready.securityDefinition !== "string" || typeof ready.rowIsolationDefinition !== "string" || typeof ready.ownerControlDefinition !== "string" || typeof ready.portableArchiveDefinition !== "string") {
|
|
1384
1479
|
throw new PostgresNativeDrError("SOURCE_NOT_READY", "source PostgreSQL schema failed readiness attestation");
|
|
1385
1480
|
}
|
|
1386
1481
|
return {
|
|
@@ -1397,6 +1492,105 @@ async function collectSnapshotMetadata(client, checkRowReady) {
|
|
|
1397
1492
|
}
|
|
1398
1493
|
};
|
|
1399
1494
|
}
|
|
1495
|
+
async function gatewayAuthorityReady(client, schemaVersion) {
|
|
1496
|
+
if (schemaVersion <= 16) return true;
|
|
1497
|
+
const result = await client.query(`
|
|
1498
|
+
WITH names AS (
|
|
1499
|
+
SELECT ('agent_bridge_runtime_' || substr(md5(current_database()),1,16))::name AS runtime_role,
|
|
1500
|
+
(SELECT nspowner FROM pg_catalog.pg_namespace WHERE nspname='agent_bridge') AS schema_owner
|
|
1501
|
+
) SELECT
|
|
1502
|
+
agent_bridge.gateway_authority_ready()
|
|
1503
|
+
AND EXISTS (
|
|
1504
|
+
SELECT 1
|
|
1505
|
+
FROM pg_catalog.pg_class relation
|
|
1506
|
+
JOIN pg_catalog.pg_namespace namespace ON namespace.oid=relation.relnamespace
|
|
1507
|
+
WHERE namespace.nspname='agent_bridge' AND relation.relname='gateway_authority'
|
|
1508
|
+
AND relation.relkind='r' AND relation.relowner=(SELECT schema_owner FROM names)
|
|
1509
|
+
AND NOT relation.relrowsecurity AND NOT relation.relforcerowsecurity
|
|
1510
|
+
)
|
|
1511
|
+
AND EXISTS (
|
|
1512
|
+
SELECT 1 FROM pg_catalog.pg_constraint table_constraint
|
|
1513
|
+
WHERE table_constraint.conrelid='agent_bridge.gateway_authority'::regclass
|
|
1514
|
+
AND table_constraint.conname='gateway_authority_singleton'
|
|
1515
|
+
AND table_constraint.contype='p'
|
|
1516
|
+
AND table_constraint.conkey::smallint[]=ARRAY[(SELECT attribute.attnum FROM pg_catalog.pg_attribute attribute
|
|
1517
|
+
WHERE attribute.attrelid='agent_bridge.gateway_authority'::regclass
|
|
1518
|
+
AND attribute.attname='singleton' AND attribute.attnum>0 AND NOT attribute.attisdropped)]
|
|
1519
|
+
)
|
|
1520
|
+
AND EXISTS (
|
|
1521
|
+
SELECT 1 FROM pg_catalog.pg_constraint table_constraint
|
|
1522
|
+
WHERE table_constraint.conrelid='agent_bridge.gateway_authority'::regclass
|
|
1523
|
+
AND table_constraint.conname='gateway_authority_singleton_true'
|
|
1524
|
+
AND table_constraint.contype='c'
|
|
1525
|
+
AND regexp_replace(
|
|
1526
|
+
pg_get_expr(table_constraint.conbin,table_constraint.conrelid),'[[:space:]()]','','g'
|
|
1527
|
+
)='singleton'
|
|
1528
|
+
)
|
|
1529
|
+
AND EXISTS (
|
|
1530
|
+
SELECT 1 FROM pg_catalog.pg_constraint table_constraint
|
|
1531
|
+
WHERE table_constraint.conrelid='agent_bridge.gateway_authority'::regclass
|
|
1532
|
+
AND table_constraint.conname='gateway_authority_id_unique'
|
|
1533
|
+
AND table_constraint.contype='u'
|
|
1534
|
+
AND table_constraint.conkey::smallint[]=ARRAY[(SELECT attribute.attnum FROM pg_catalog.pg_attribute attribute
|
|
1535
|
+
WHERE attribute.attrelid='agent_bridge.gateway_authority'::regclass
|
|
1536
|
+
AND attribute.attname='authority_id' AND attribute.attnum>0 AND NOT attribute.attisdropped)]
|
|
1537
|
+
)
|
|
1538
|
+
AND EXISTS (
|
|
1539
|
+
SELECT 1
|
|
1540
|
+
FROM pg_catalog.pg_trigger trigger
|
|
1541
|
+
JOIN pg_catalog.pg_proc trigger_function ON trigger_function.oid=trigger.tgfoid
|
|
1542
|
+
JOIN pg_catalog.pg_namespace trigger_namespace ON trigger_namespace.oid=trigger_function.pronamespace
|
|
1543
|
+
WHERE trigger.tgrelid='agent_bridge.gateway_authority'::regclass
|
|
1544
|
+
AND trigger.tgname='gateway_authority_immutable'
|
|
1545
|
+
AND NOT trigger.tgisinternal AND trigger.tgenabled='O'
|
|
1546
|
+
AND (trigger.tgtype::integer & 1)=0
|
|
1547
|
+
AND (trigger.tgtype::integer & 2)=2
|
|
1548
|
+
AND (trigger.tgtype::integer & 56)=56
|
|
1549
|
+
AND trigger_function.proname='reject_gateway_authority_mutation'
|
|
1550
|
+
AND trigger_namespace.nspname='agent_bridge'
|
|
1551
|
+
AND trigger_function.proowner=(SELECT schema_owner FROM names)
|
|
1552
|
+
AND NOT trigger_function.prosecdef
|
|
1553
|
+
AND coalesce(trigger_function.proconfig @> ARRAY['search_path=""'],false)
|
|
1554
|
+
)
|
|
1555
|
+
AND EXISTS (
|
|
1556
|
+
SELECT 1 FROM pg_catalog.pg_proc procedure
|
|
1557
|
+
WHERE procedure.oid='agent_bridge.gateway_authority_ready()'::regprocedure
|
|
1558
|
+
AND procedure.prosecdef AND procedure.provolatile='s'
|
|
1559
|
+
AND procedure.proowner=(SELECT schema_owner FROM names)
|
|
1560
|
+
AND coalesce(procedure.proconfig @> ARRAY['search_path=""'],false)
|
|
1561
|
+
)
|
|
1562
|
+
AND EXISTS (
|
|
1563
|
+
SELECT 1 FROM pg_catalog.pg_proc procedure
|
|
1564
|
+
WHERE procedure.oid='agent_bridge.open_request_authority_bound(uuid,text,uuid)'::regprocedure
|
|
1565
|
+
AND procedure.prosecdef AND procedure.provolatile='v'
|
|
1566
|
+
AND procedure.proowner=(SELECT schema_owner FROM names)
|
|
1567
|
+
AND coalesce(procedure.proconfig @> ARRAY['search_path=""'],false)
|
|
1568
|
+
)
|
|
1569
|
+
AND NOT EXISTS (
|
|
1570
|
+
SELECT 1
|
|
1571
|
+
FROM unnest(ARRAY[
|
|
1572
|
+
'SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER'
|
|
1573
|
+
]) privilege(value)
|
|
1574
|
+
WHERE has_table_privilege((SELECT runtime_role FROM names),
|
|
1575
|
+
'agent_bridge.gateway_authority',privilege.value)
|
|
1576
|
+
OR has_table_privilege('public','agent_bridge.gateway_authority',privilege.value)
|
|
1577
|
+
)
|
|
1578
|
+
AND NOT EXISTS (
|
|
1579
|
+
SELECT 1
|
|
1580
|
+
FROM unnest(ARRAY['SELECT','INSERT','UPDATE','REFERENCES']) privilege(value)
|
|
1581
|
+
WHERE has_any_column_privilege((SELECT runtime_role FROM names),
|
|
1582
|
+
'agent_bridge.gateway_authority',privilege.value)
|
|
1583
|
+
OR has_any_column_privilege('public','agent_bridge.gateway_authority',privilege.value)
|
|
1584
|
+
)
|
|
1585
|
+
AND has_function_privilege((SELECT runtime_role FROM names),
|
|
1586
|
+
'agent_bridge.gateway_authority_ready()','EXECUTE')
|
|
1587
|
+
AND has_function_privilege((SELECT runtime_role FROM names),
|
|
1588
|
+
'agent_bridge.open_request_authority_bound(uuid,text,uuid)','EXECUTE')
|
|
1589
|
+
AND NOT has_function_privilege('public','agent_bridge.gateway_authority_ready()','EXECUTE')
|
|
1590
|
+
AND NOT has_function_privilege('public',
|
|
1591
|
+
'agent_bridge.open_request_authority_bound(uuid,text,uuid)','EXECUTE') AS ready`);
|
|
1592
|
+
return result.rows[0]?.ready === true;
|
|
1593
|
+
}
|
|
1400
1594
|
function createPrivateEmptyFile(path) {
|
|
1401
1595
|
const descriptor = openSync2(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL, 384);
|
|
1402
1596
|
closeSync2(descriptor);
|
|
@@ -1434,10 +1628,16 @@ async function backupPostgresNativeDr(options) {
|
|
|
1434
1628
|
if (!snapshot) throw new PostgresNativeDrError("SNAPSHOT_EXPORT_FAILED", "PostgreSQL did not export a disaster recovery snapshot");
|
|
1435
1629
|
const metadata = await collectSnapshotMetadata(source, deps.checkRowIsolationReady);
|
|
1436
1630
|
const major = serverMajor(metadata.serverVersionNum);
|
|
1631
|
+
const schemaVersion = Math.max(...metadata.migrations.map((migration) => migration.version));
|
|
1632
|
+
const excludedDataTables = excludedDataTablesForSchemaVersion(schemaVersion);
|
|
1437
1633
|
const beforeInventory = await collectPostgresRoleInventory(source, metadata.databaseName);
|
|
1438
1634
|
const canonicalRoles = canonicalPostgresRoleInventory(beforeInventory);
|
|
1439
1635
|
const tools = await checkedTools(major, libpq.environment, options.toolDirectory, deps);
|
|
1440
|
-
const dump = await deps.runTool(
|
|
1636
|
+
const dump = await deps.runTool(
|
|
1637
|
+
tools.pgDump,
|
|
1638
|
+
buildPgDumpArgsForTables(dumpPath, snapshot, libpq.serviceName, excludedDataTables),
|
|
1639
|
+
libpq.environment
|
|
1640
|
+
);
|
|
1441
1641
|
assertToolSucceeded(dump, "PG_DUMP_FAILED", "pg_dump");
|
|
1442
1642
|
verifyPrivatePathAccess(dumpPath, "file");
|
|
1443
1643
|
if (statSync(dumpPath).size === 0) throw new PostgresNativeDrError("PG_DUMP_EMPTY", "pg_dump produced an empty file");
|
|
@@ -1459,7 +1659,6 @@ async function backupPostgresNativeDr(options) {
|
|
|
1459
1659
|
transactionOpen = false;
|
|
1460
1660
|
await source.query("SELECT pg_catalog.pg_advisory_unlock($1)", [NATIVE_DR_ADVISORY_LOCK_KEY]);
|
|
1461
1661
|
lockHeld = false;
|
|
1462
|
-
const schemaVersion = Math.max(...metadata.migrations.map((migration) => migration.version));
|
|
1463
1662
|
const result = {
|
|
1464
1663
|
backupId: validateBackupId(options.backupId ?? deps.randomId()),
|
|
1465
1664
|
createdAt: deps.now().toISOString(),
|
|
@@ -1471,7 +1670,7 @@ async function backupPostgresNativeDr(options) {
|
|
|
1471
1670
|
schemaVersion,
|
|
1472
1671
|
migrations: metadata.migrations,
|
|
1473
1672
|
tableCounts: metadata.tableCounts,
|
|
1474
|
-
excludedDataTables: [...
|
|
1673
|
+
excludedDataTables: [...excludedDataTables],
|
|
1475
1674
|
claimedDeliveryCount: metadata.claimedDeliveryCount,
|
|
1476
1675
|
pgDumpVersion: tools.pgDumpVersion,
|
|
1477
1676
|
roleInventorySha256: sha256(canonicalRoles),
|
|
@@ -1558,10 +1757,11 @@ function validatePostgresNativeDrSchema(value) {
|
|
|
1558
1757
|
throw new PostgresNativeDrError("INVALID_MANIFEST", "PostgreSQL table count inventory is invalid");
|
|
1559
1758
|
}
|
|
1560
1759
|
}
|
|
1561
|
-
|
|
1760
|
+
const excludedDataTables = excludedDataTablesForSchemaVersion(value.schemaVersion);
|
|
1761
|
+
for (const table of ["agent_bridge.deliveries", "agent_bridge.delivery_events", ...excludedDataTables]) {
|
|
1562
1762
|
if (!(table in value.tableCounts)) throw new PostgresNativeDrError("INVALID_MANIFEST", `PostgreSQL table count is missing for ${table}`);
|
|
1563
1763
|
}
|
|
1564
|
-
if (JSON.stringify(value.excludedDataTables) !== JSON.stringify(
|
|
1764
|
+
if (JSON.stringify(value.excludedDataTables) !== JSON.stringify(excludedDataTables)) {
|
|
1565
1765
|
throw new PostgresNativeDrError("INVALID_MANIFEST", "PostgreSQL excluded table inventory is invalid");
|
|
1566
1766
|
}
|
|
1567
1767
|
if (!/^(?:0|[1-9][0-9]*)$/u.test(value.claimedDeliveryCount)) throw new PostgresNativeDrError("INVALID_MANIFEST", "PostgreSQL claimed delivery count is invalid");
|
|
@@ -1669,12 +1869,24 @@ async function targetTableCounts(client) {
|
|
|
1669
1869
|
}
|
|
1670
1870
|
return counts;
|
|
1671
1871
|
}
|
|
1672
|
-
function expectedRestoredTableCounts(schema) {
|
|
1872
|
+
function expectedRestoredTableCounts(schema, suspendedMemberships) {
|
|
1673
1873
|
const expected = { ...schema.tableCounts };
|
|
1674
|
-
for (const table of
|
|
1874
|
+
for (const table of excludedDataTablesForSchemaVersion(schema.schemaVersion)) expected[table] = "0";
|
|
1675
1875
|
const deliveryEvents = expected["agent_bridge.delivery_events"];
|
|
1676
1876
|
if (deliveryEvents === void 0) throw new PostgresNativeDrError("INVALID_MANIFEST", "PostgreSQL delivery event count is missing");
|
|
1677
1877
|
expected["agent_bridge.delivery_events"] = (BigInt(deliveryEvents) + BigInt(schema.claimedDeliveryCount)).toString();
|
|
1878
|
+
for (const [kind, table] of [
|
|
1879
|
+
["control", "agent_bridge.control_membership_events"],
|
|
1880
|
+
["archive", "agent_bridge.archive_membership_events"]
|
|
1881
|
+
]) {
|
|
1882
|
+
const increment = suspendedMemberships.filter((membership) => membership.kind === kind).length;
|
|
1883
|
+
if (increment === 0) continue;
|
|
1884
|
+
const current = expected[table];
|
|
1885
|
+
if (current === void 0) {
|
|
1886
|
+
throw new PostgresNativeDrError("INVALID_MANIFEST", `PostgreSQL ${kind} membership event count is missing`);
|
|
1887
|
+
}
|
|
1888
|
+
expected[table] = (BigInt(current) + BigInt(increment)).toString();
|
|
1889
|
+
}
|
|
1678
1890
|
return expected;
|
|
1679
1891
|
}
|
|
1680
1892
|
function compareStringMaps(actual, expected, label) {
|
|
@@ -1765,11 +1977,17 @@ async function restorePostgresNativeDrInternal(options) {
|
|
|
1765
1977
|
if (metadata.schemaExists || !metadata.databaseFresh) {
|
|
1766
1978
|
throw new PostgresNativeDrError("TARGET_NOT_FRESH", "target must be a dedicated fresh database without user objects or default ACLs");
|
|
1767
1979
|
}
|
|
1980
|
+
const schemaOwner = inventory.roles.find((role) => role.kind === "schema-owner");
|
|
1768
1981
|
const conflicts = await target.query(
|
|
1769
|
-
`SELECT rolname AS name
|
|
1982
|
+
`SELECT rolname AS name,rolname=current_user AS "isCurrentUser",
|
|
1983
|
+
rolsuper "isSuperuser"
|
|
1984
|
+
FROM pg_catalog.pg_roles WHERE rolname=ANY($1::text[]) ORDER BY rolname`,
|
|
1770
1985
|
[inventory.roles.map((role) => role.name)]
|
|
1771
1986
|
);
|
|
1772
|
-
|
|
1987
|
+
const reusesSchemaOwner = conflicts.rows.length === 1 && conflicts.rows[0]?.name === schemaOwner.name && conflicts.rows[0].isCurrentUser && conflicts.rows[0].isSuperuser;
|
|
1988
|
+
if (conflicts.rows.length > 0 && !reusesSchemaOwner) {
|
|
1989
|
+
throw new PostgresNativeDrError("TARGET_ROLE_CONFLICT", "target contains a role required by the backup");
|
|
1990
|
+
}
|
|
1773
1991
|
await assertPostgresRestoreAuthority(target);
|
|
1774
1992
|
admin = deps.createClient(administrativeDatabaseUrl(targetUrl, metadata.databaseName));
|
|
1775
1993
|
await admin.connect();
|
|
@@ -1779,12 +1997,13 @@ async function restorePostgresNativeDrInternal(options) {
|
|
|
1779
1997
|
securePrivatePath(schemaAclListPath, "file");
|
|
1780
1998
|
verifyPrivatePathAccess(schemaAclListPath, "file");
|
|
1781
1999
|
for (const [index, statement] of buildPostgresRoleShellStatements(inventory).entries()) {
|
|
2000
|
+
const role = inventory.roles[index];
|
|
2001
|
+
if (reusesSchemaOwner && role.name === schemaOwner.name) continue;
|
|
1782
2002
|
phase = "role shell creation";
|
|
1783
2003
|
restoreStarted = true;
|
|
1784
2004
|
await target.query(statement);
|
|
1785
|
-
createdRoles.push(
|
|
2005
|
+
createdRoles.push(role.name);
|
|
1786
2006
|
}
|
|
1787
|
-
const schemaOwner = inventory.roles.find((role) => role.kind === "schema-owner");
|
|
1788
2007
|
phase = "schema creation";
|
|
1789
2008
|
await target.query(`CREATE SCHEMA agent_bridge AUTHORIZATION ${quoteIdentifier(schemaOwner.name)}`);
|
|
1790
2009
|
phase = "schema ACL restore";
|
|
@@ -1815,6 +2034,25 @@ async function restorePostgresNativeDrInternal(options) {
|
|
|
1815
2034
|
transactionOpen = true;
|
|
1816
2035
|
phase = "membership restore";
|
|
1817
2036
|
for (const statement of buildPostgresMembershipStatements(inventory, major)) await target.query(statement);
|
|
2037
|
+
const suspendedMemberships = suspendedPostgresMemberships(inventory);
|
|
2038
|
+
phase = "external membership suspension";
|
|
2039
|
+
for (const membership of suspendedMemberships) {
|
|
2040
|
+
if (membership.kind === "control") {
|
|
2041
|
+
await target.query(
|
|
2042
|
+
`INSERT INTO agent_bridge.control_membership_events(
|
|
2043
|
+
request_id,action,member_role,control_role,actor
|
|
2044
|
+
) VALUES($1::uuid,'revoke',$2::name,$3,$4::name)`,
|
|
2045
|
+
[deps.randomId(), membership.member, membership.controlRole, schemaOwner.name]
|
|
2046
|
+
);
|
|
2047
|
+
} else {
|
|
2048
|
+
await target.query(
|
|
2049
|
+
`INSERT INTO agent_bridge.archive_membership_events(
|
|
2050
|
+
request_id,action,member_role,actor
|
|
2051
|
+
) VALUES($1::uuid,'revoke',$2::name,$3::name)`,
|
|
2052
|
+
[deps.randomId(), membership.member, schemaOwner.name]
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
1818
2056
|
phase = "default privilege restore";
|
|
1819
2057
|
for (const statement of buildPostgresDefaultPrivilegeStatements(inventory)) await target.query(statement);
|
|
1820
2058
|
phase = "claim normalization";
|
|
@@ -1840,23 +2078,27 @@ async function restorePostgresNativeDrInternal(options) {
|
|
|
1840
2078
|
}
|
|
1841
2079
|
phase = "table count validation";
|
|
1842
2080
|
const counts = await targetTableCounts(target);
|
|
1843
|
-
compareStringMaps(counts, expectedRestoredTableCounts(schema), "restored PostgreSQL");
|
|
2081
|
+
compareStringMaps(counts, expectedRestoredTableCounts(schema, suspendedMemberships), "restored PostgreSQL");
|
|
1844
2082
|
phase = "readiness validation";
|
|
1845
2083
|
const readiness = await target.query(`
|
|
1846
2084
|
SELECT agent_bridge.security_schema_ready() AS security,
|
|
1847
2085
|
agent_bridge.owner_control_plane_ready() AS "ownerControl",
|
|
1848
2086
|
agent_bridge.portable_archive_ready() AS "portableArchive",
|
|
2087
|
+
${schema.schemaVersion >= 21 ? 'agent_bridge.native_backup_ready() AS "nativeBackup",' : ""}
|
|
1849
2088
|
agent_bridge.credential_security_prerequisite_definition() AS "securityDefinition",
|
|
1850
2089
|
agent_bridge.row_isolation_catalog_definition() AS "rowIsolationDefinition",
|
|
1851
2090
|
agent_bridge.owner_control_attestation_definition() AS "ownerControlDefinition",
|
|
1852
2091
|
agent_bridge.portable_archive_attestation_definition() AS "portableArchiveDefinition"`);
|
|
1853
2092
|
const ready = readiness.rows[0];
|
|
1854
2093
|
const restoredRowReady = await deps.checkRowIsolationReady(target);
|
|
2094
|
+
const restoredGatewayAuthorityReady = await gatewayAuthorityReady(target, schema.schemaVersion);
|
|
1855
2095
|
const failedReadiness = [
|
|
1856
2096
|
!ready?.security ? "security" : void 0,
|
|
1857
2097
|
!restoredRowReady ? "row-isolation" : void 0,
|
|
1858
2098
|
!ready?.ownerControl ? "owner-control" : void 0,
|
|
1859
|
-
!ready?.portableArchive ? "portable-archive" : void 0
|
|
2099
|
+
!ready?.portableArchive ? "portable-archive" : void 0,
|
|
2100
|
+
schema.schemaVersion >= 21 && !ready?.nativeBackup ? "native-backup" : void 0,
|
|
2101
|
+
!restoredGatewayAuthorityReady ? "gateway-authority" : void 0
|
|
1860
2102
|
].filter(Boolean);
|
|
1861
2103
|
if (failedReadiness.length > 0) {
|
|
1862
2104
|
throw new PostgresNativeDrError("RESTORE_NOT_READY", `restored PostgreSQL schema failed readiness attestation: ${failedReadiness.join(", ")}`);
|
|
@@ -2396,6 +2638,7 @@ export {
|
|
|
2396
2638
|
POSTGRES_NATIVE_DR_ROLE_SCHEMA,
|
|
2397
2639
|
POSTGRES_NATIVE_DR_ROLE_VERSION,
|
|
2398
2640
|
POSTGRES_NATIVE_DR_SERVICE,
|
|
2641
|
+
POSTGRES_NATIVE_DR_LEGACY_EXCLUDED_DATA_TABLES,
|
|
2399
2642
|
POSTGRES_NATIVE_DR_EXCLUDED_DATA_TABLES,
|
|
2400
2643
|
PostgresNativeDrError,
|
|
2401
2644
|
createPostgresDrLibpqFiles,
|
|
@@ -2425,4 +2668,4 @@ export {
|
|
|
2425
2668
|
restoreLocalSqlite,
|
|
2426
2669
|
SQLITE_EDGE_APPLICATION_ID
|
|
2427
2670
|
};
|
|
2428
|
-
//# sourceMappingURL=chunk-
|
|
2671
|
+
//# sourceMappingURL=chunk-7VJI2MXI.js.map
|