@aiwg/cli 2026.8.25 → 2026.8.27
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/bin/aiwg.mjs +25 -16
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/backends/graphology-backend.js +11 -2
- package/dist/src/artifacts/backends/json-backend.js +12 -2
- package/dist/src/artifacts/backends/sqlite-backend.js +20 -7
- package/dist/src/artifacts/graph-backend.js +16 -0
- package/dist/src/features/catalog.js +11 -0
- package/dist/src/storage/backend-contract.js +11 -2
- package/dist/src/storage/backends/postgres-schema.js +144 -0
- package/dist/src/storage/backends/postgres.js +473 -0
- package/dist/src/storage/backends/postgrest-schema.js +192 -0
- package/dist/src/storage/backends/postgrest.js +346 -0
- package/dist/src/storage/index.js +6 -1
- package/dist/src/storage/migration-protocol.js +228 -50
- package/dist/src/storage/qualification.js +171 -0
- package/dist/src/update/notifier.mjs +59 -14
- package/package.json +1 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { STORAGE_BACKEND_CONTRACT } from '../backend-contract.js';
|
|
2
|
+
import { postgresBatchId, postgresPayloadDigest } from './postgres.js';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
4
|
+
const DEFAULT_MAX_PAYLOAD_BYTES = 1_048_576;
|
|
5
|
+
const DEFAULT_PAGE_SIZE = 1000;
|
|
6
|
+
export class PostgrestBackendError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
retryable;
|
|
9
|
+
retryAfterMs;
|
|
10
|
+
constructor(code, message, retryable = false, retryAfterMs) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.retryable = retryable;
|
|
14
|
+
this.retryAfterMs = retryAfterMs;
|
|
15
|
+
this.name = 'PostgrestBackendError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** PostgreSQL accessMode=postgrest; not a separate storage engine. */
|
|
19
|
+
export class PostgrestStorageBackend {
|
|
20
|
+
options;
|
|
21
|
+
descriptor = {
|
|
22
|
+
contract: STORAGE_BACKEND_CONTRACT,
|
|
23
|
+
backend: 'postgres-postgrest',
|
|
24
|
+
implementationVersion: '1.0.0',
|
|
25
|
+
schemaVersion: '1',
|
|
26
|
+
maturity: 'advanced',
|
|
27
|
+
capabilities: [
|
|
28
|
+
'read', 'atomic-batch', 'consistent-snapshot', 'change-cursor',
|
|
29
|
+
'tombstones', 'idempotency-keys', 'filtered-query', 'cursor-pagination',
|
|
30
|
+
'health', 'readiness', 'tenant-isolation', 'subsystem-isolation', 'tls',
|
|
31
|
+
],
|
|
32
|
+
durability: 'replicated',
|
|
33
|
+
availability: 'remote-service',
|
|
34
|
+
isolation: 'serializable',
|
|
35
|
+
dataClass: 'canonical',
|
|
36
|
+
};
|
|
37
|
+
identity;
|
|
38
|
+
baseUrl;
|
|
39
|
+
fetchImpl;
|
|
40
|
+
timeoutMs;
|
|
41
|
+
maxPayloadBytes;
|
|
42
|
+
maxResponseBytes;
|
|
43
|
+
maxBatchSize;
|
|
44
|
+
maxPageSize;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
48
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
49
|
+
if (!this.fetchImpl)
|
|
50
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_FETCH_UNAVAILABLE', 'fetch is unavailable');
|
|
51
|
+
this.timeoutMs = bounded(options.timeoutMs, DEFAULT_TIMEOUT_MS, 1, 600_000, 'timeoutMs');
|
|
52
|
+
this.maxPayloadBytes = bounded(options.maxPayloadBytes, DEFAULT_MAX_PAYLOAD_BYTES, 1024, 16_777_216, 'maxPayloadBytes');
|
|
53
|
+
this.maxResponseBytes = bounded(options.maxResponseBytes, DEFAULT_MAX_PAYLOAD_BYTES, 1024, 16_777_216, 'maxResponseBytes');
|
|
54
|
+
this.maxBatchSize = bounded(options.maxBatchSize, DEFAULT_PAGE_SIZE, 1, 10_000, 'maxBatchSize');
|
|
55
|
+
this.maxPageSize = bounded(options.maxPageSize, DEFAULT_PAGE_SIZE, 1, 10_000, 'maxPageSize');
|
|
56
|
+
this.identity = {
|
|
57
|
+
backend: 'postgres-postgrest', instance: options.instance ?? this.baseUrl,
|
|
58
|
+
tenant: options.tenant, subsystem: options.subsystem, schemaVersion: '1',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async init() {
|
|
62
|
+
const health = await this.health();
|
|
63
|
+
if (!health.ready || health.schemaVersion !== '1') {
|
|
64
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_SCHEMA_UNAVAILABLE', 'PostgREST schema/RPC contract v1 is unavailable');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async commitBatch(mutations) {
|
|
68
|
+
if (mutations.length === 0)
|
|
69
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_EMPTY_BATCH', 'batch must contain at least one mutation');
|
|
70
|
+
if (mutations.length > this.maxBatchSize)
|
|
71
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_BATCH_TOO_LARGE', `batch exceeds ${this.maxBatchSize} mutation ceiling`);
|
|
72
|
+
for (const mutation of mutations)
|
|
73
|
+
this.assertMutationIdentity(mutation);
|
|
74
|
+
return this.rpc('aiwg_commit_batch_v1', {
|
|
75
|
+
p_tenant: this.options.tenant,
|
|
76
|
+
p_subsystem: this.options.subsystem,
|
|
77
|
+
p_batch_id: postgresBatchId(mutations),
|
|
78
|
+
p_payload_digest: postgresPayloadDigest(mutations),
|
|
79
|
+
p_mutations: mutations,
|
|
80
|
+
}, 'return=representation');
|
|
81
|
+
}
|
|
82
|
+
async get(path) {
|
|
83
|
+
return this.rpc('aiwg_get_record_v1', {
|
|
84
|
+
p_tenant: this.options.tenant,
|
|
85
|
+
p_subsystem: this.options.subsystem,
|
|
86
|
+
p_path: path,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async readAll() {
|
|
90
|
+
const records = [];
|
|
91
|
+
let cursor;
|
|
92
|
+
do {
|
|
93
|
+
const page = await this.query({}, this.maxPageSize, cursor, true);
|
|
94
|
+
records.push(...page.records);
|
|
95
|
+
cursor = page.nextCursor;
|
|
96
|
+
} while (cursor);
|
|
97
|
+
return records;
|
|
98
|
+
}
|
|
99
|
+
async query(filters, limit = this.maxPageSize, cursor, includeTombstones = false) {
|
|
100
|
+
const boundedLimit = bounded(limit, this.maxPageSize, 1, this.maxPageSize, 'limit');
|
|
101
|
+
return this.rpc('aiwg_query_records_v1', {
|
|
102
|
+
p_tenant: this.options.tenant,
|
|
103
|
+
p_subsystem: this.options.subsystem,
|
|
104
|
+
p_filters: filters,
|
|
105
|
+
p_after_path: cursor ?? null,
|
|
106
|
+
p_limit: boundedLimit,
|
|
107
|
+
p_include_tombstones: includeTombstones,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async snapshot() {
|
|
111
|
+
const response = await this.rpc('aiwg_snapshot_v1', {
|
|
112
|
+
p_tenant: this.options.tenant,
|
|
113
|
+
p_subsystem: this.options.subsystem,
|
|
114
|
+
p_limit: this.maxPageSize,
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
id: response.snapshot_id,
|
|
118
|
+
highWaterMark: response.high_water_mark,
|
|
119
|
+
cursor: response.high_water_mark,
|
|
120
|
+
records: response.records,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async changes(cursor) {
|
|
124
|
+
if (cursor !== undefined && !/^\d+$/.test(cursor)) {
|
|
125
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_CURSOR_INVALID', 'change cursor must be an unsigned integer');
|
|
126
|
+
}
|
|
127
|
+
const response = await this.rpc('aiwg_changes_v1', {
|
|
128
|
+
p_tenant: this.options.tenant,
|
|
129
|
+
p_subsystem: this.options.subsystem,
|
|
130
|
+
p_after_cursor: cursor ?? '0',
|
|
131
|
+
p_limit: this.maxPageSize,
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
records: response.records,
|
|
135
|
+
highWaterMark: response.high_water_mark,
|
|
136
|
+
...(response.next_cursor ? { nextCursor: response.next_cursor } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async reloadSchemaCache() {
|
|
140
|
+
await this.rpc('aiwg_reload_schema_v1', {});
|
|
141
|
+
}
|
|
142
|
+
async health() {
|
|
143
|
+
return this.rpc('aiwg_health_v1', {
|
|
144
|
+
p_tenant: this.options.tenant,
|
|
145
|
+
p_subsystem: this.options.subsystem,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Receipt-less native JSON bulk upsert for controlled bootstrap only. */
|
|
149
|
+
async bulkBootstrapJson(rows) {
|
|
150
|
+
this.assertBootstrapCount(rows.length);
|
|
151
|
+
for (const row of rows)
|
|
152
|
+
this.assertBootstrapIdentity(row.tenant, row.subsystem);
|
|
153
|
+
return this.request('/aiwg_storage_records?on_conflict=tenant%2Csubsystem%2Cpath', {
|
|
154
|
+
method: 'POST', body: rows, prefer: 'resolution=merge-duplicates,return=representation',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/** Receipt-less native CSV bulk upsert for controlled bootstrap only. */
|
|
158
|
+
async bulkBootstrapCsv(csv) {
|
|
159
|
+
const parsed = parseCsv(csv);
|
|
160
|
+
const required = ['tenant', 'subsystem', 'path', 'source_revision', 'digest', 'value', 'tombstone', 'idempotency_key'];
|
|
161
|
+
for (const column of required) {
|
|
162
|
+
if (!parsed.header.includes(column))
|
|
163
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_CSV_INVALID', `CSV header is missing ${column}`);
|
|
164
|
+
}
|
|
165
|
+
this.assertBootstrapCount(parsed.rows.length);
|
|
166
|
+
const tenantIndex = parsed.header.indexOf('tenant');
|
|
167
|
+
const subsystemIndex = parsed.header.indexOf('subsystem');
|
|
168
|
+
for (const row of parsed.rows)
|
|
169
|
+
this.assertBootstrapIdentity(row[tenantIndex] ?? '', row[subsystemIndex] ?? '');
|
|
170
|
+
return this.request('/aiwg_storage_records?on_conflict=tenant%2Csubsystem%2Cpath', {
|
|
171
|
+
method: 'POST', rawBody: csv, contentType: 'text/csv',
|
|
172
|
+
prefer: 'resolution=merge-duplicates,return=representation',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async rpc(name, body, prefer) {
|
|
176
|
+
return this.request(`/rpc/${name}`, { method: 'POST', body, ...(prefer ? { prefer } : {}) });
|
|
177
|
+
}
|
|
178
|
+
async request(pathname, options = {}) {
|
|
179
|
+
const body = options.rawBody ?? (options.body === undefined ? undefined : JSON.stringify(options.body));
|
|
180
|
+
if (body && Buffer.byteLength(body) > this.maxPayloadBytes) {
|
|
181
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_PAYLOAD_TOO_LARGE', `request exceeds ${this.maxPayloadBytes} byte ceiling`);
|
|
182
|
+
}
|
|
183
|
+
const headers = { Accept: 'application/json' };
|
|
184
|
+
if (body)
|
|
185
|
+
headers['Content-Type'] = options.contentType ?? 'application/json';
|
|
186
|
+
if (options.prefer)
|
|
187
|
+
headers.Prefer = options.prefer;
|
|
188
|
+
const authorization = this.authorization();
|
|
189
|
+
if (authorization)
|
|
190
|
+
headers.Authorization = authorization;
|
|
191
|
+
let response;
|
|
192
|
+
try {
|
|
193
|
+
response = await this.fetchImpl(`${this.baseUrl}${pathname}`, {
|
|
194
|
+
method: options.method ?? 'GET', headers, body,
|
|
195
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_TRANSPORT_FAILED', error instanceof Error ? error.message : String(error), true);
|
|
200
|
+
}
|
|
201
|
+
if (response.status === 204)
|
|
202
|
+
return undefined;
|
|
203
|
+
const declaredLength = Number(response.headers.get('content-length') ?? '0');
|
|
204
|
+
if (declaredLength > this.maxResponseBytes) {
|
|
205
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_RESPONSE_TOO_LARGE', `response exceeds ${this.maxResponseBytes} byte ceiling`);
|
|
206
|
+
}
|
|
207
|
+
const responseBody = await response.text();
|
|
208
|
+
if (Buffer.byteLength(responseBody) > this.maxResponseBytes) {
|
|
209
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_RESPONSE_TOO_LARGE', `response exceeds ${this.maxResponseBytes} byte ceiling`);
|
|
210
|
+
}
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const databaseError = parseDatabaseError(responseBody);
|
|
213
|
+
const retryable = response.status === 408 || response.status === 425 || response.status === 429 ||
|
|
214
|
+
response.status >= 500 || isRetryableDatabaseCode(databaseError.databaseCode);
|
|
215
|
+
const exposedCode = databaseError.message?.startsWith('AIWG_')
|
|
216
|
+
? databaseError.message
|
|
217
|
+
: retryable ? 'AIWG_POSTGREST_RETRYABLE' : 'AIWG_POSTGREST_REQUEST_FAILED';
|
|
218
|
+
throw new PostgrestBackendError(exposedCode, `PostgREST request failed with HTTP ${response.status}${databaseError.databaseCode ? ` (database ${databaseError.databaseCode})` : ''}`, retryable, parseRetryAfter(response.headers.get('retry-after')));
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(responseBody);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_RESPONSE_INVALID', 'PostgREST returned invalid JSON');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
authorization() {
|
|
228
|
+
if (!this.options.authorizationEnv)
|
|
229
|
+
return undefined;
|
|
230
|
+
const value = process.env[this.options.authorizationEnv];
|
|
231
|
+
if (!value)
|
|
232
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_CREDENTIAL_UNAVAILABLE', `authorization locator ${this.options.authorizationEnv} is not set`);
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
assertMutationIdentity(mutation) {
|
|
236
|
+
if (mutation.record.identity.tenant !== this.options.tenant || mutation.record.identity.subsystem !== this.options.subsystem) {
|
|
237
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_IDENTITY_MISMATCH', 'mutation identity is outside this backend tenant/subsystem');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
assertBootstrapCount(count) {
|
|
241
|
+
if (count < 1)
|
|
242
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_EMPTY_BATCH', 'bootstrap batch must contain at least one row');
|
|
243
|
+
if (count > this.maxBatchSize)
|
|
244
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_BATCH_TOO_LARGE', `batch exceeds ${this.maxBatchSize} row ceiling`);
|
|
245
|
+
}
|
|
246
|
+
assertBootstrapIdentity(tenant, subsystem) {
|
|
247
|
+
if (tenant !== this.options.tenant || subsystem !== this.options.subsystem) {
|
|
248
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_IDENTITY_MISMATCH', 'bootstrap row is outside this backend tenant/subsystem');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function parseCsv(input) {
|
|
253
|
+
const output = [];
|
|
254
|
+
let row = [];
|
|
255
|
+
let field = '';
|
|
256
|
+
let quoted = false;
|
|
257
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
258
|
+
const character = input[index];
|
|
259
|
+
if (character === '"') {
|
|
260
|
+
if (quoted && input[index + 1] === '"') {
|
|
261
|
+
field += '"';
|
|
262
|
+
index += 1;
|
|
263
|
+
}
|
|
264
|
+
else
|
|
265
|
+
quoted = !quoted;
|
|
266
|
+
}
|
|
267
|
+
else if (character === ',' && !quoted) {
|
|
268
|
+
row.push(field);
|
|
269
|
+
field = '';
|
|
270
|
+
}
|
|
271
|
+
else if ((character === '\n' || character === '\r') && !quoted) {
|
|
272
|
+
if (character === '\r' && input[index + 1] === '\n')
|
|
273
|
+
index += 1;
|
|
274
|
+
row.push(field);
|
|
275
|
+
field = '';
|
|
276
|
+
if (row.some(value => value.length > 0))
|
|
277
|
+
output.push(row);
|
|
278
|
+
row = [];
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
field += character;
|
|
282
|
+
}
|
|
283
|
+
if (quoted)
|
|
284
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_CSV_INVALID', 'CSV contains an unterminated quoted field');
|
|
285
|
+
row.push(field);
|
|
286
|
+
if (row.some(value => value.length > 0))
|
|
287
|
+
output.push(row);
|
|
288
|
+
const header = output.shift();
|
|
289
|
+
if (!header)
|
|
290
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_CSV_INVALID', 'CSV must contain a header');
|
|
291
|
+
for (const candidate of output) {
|
|
292
|
+
if (candidate.length !== header.length)
|
|
293
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_CSV_INVALID', 'CSV row width does not match header');
|
|
294
|
+
}
|
|
295
|
+
return { header, rows: output };
|
|
296
|
+
}
|
|
297
|
+
function parseDatabaseError(body) {
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(body);
|
|
300
|
+
return {
|
|
301
|
+
...(typeof parsed.code === 'string' ? { databaseCode: parsed.code } : {}),
|
|
302
|
+
...(typeof parsed.message === 'string' ? { message: parsed.message } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function isRetryableDatabaseCode(code) {
|
|
310
|
+
return Boolean(code && (code === '40001' || code === '40P01' || code === '55P03' || code === '57014' || code.startsWith('08')));
|
|
311
|
+
}
|
|
312
|
+
function normalizeBaseUrl(raw) {
|
|
313
|
+
let url;
|
|
314
|
+
try {
|
|
315
|
+
url = new URL(raw);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_URL_INVALID', 'baseUrl must be an absolute URL');
|
|
319
|
+
}
|
|
320
|
+
const loopback = ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
|
|
321
|
+
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
|
|
322
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_TLS_REQUIRED', 'PostgREST requires HTTPS except on explicit loopback development endpoints');
|
|
323
|
+
}
|
|
324
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
325
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_URL_SECRET_FORBIDDEN', 'baseUrl cannot contain credentials, query parameters, or fragments');
|
|
326
|
+
}
|
|
327
|
+
return url.toString().replace(/\/$/, '');
|
|
328
|
+
}
|
|
329
|
+
function parseRetryAfter(value) {
|
|
330
|
+
if (!value)
|
|
331
|
+
return undefined;
|
|
332
|
+
if (/^\d+$/.test(value))
|
|
333
|
+
return Math.min(Number(value) * 1000, 60_000);
|
|
334
|
+
const timestamp = Date.parse(value);
|
|
335
|
+
if (!Number.isFinite(timestamp))
|
|
336
|
+
return undefined;
|
|
337
|
+
return Math.max(0, Math.min(timestamp - Date.now(), 60_000));
|
|
338
|
+
}
|
|
339
|
+
function bounded(value, fallback, min, max, label) {
|
|
340
|
+
const resolved = value ?? fallback;
|
|
341
|
+
if (!Number.isInteger(resolved) || resolved < min || resolved > max) {
|
|
342
|
+
throw new PostgrestBackendError('AIWG_POSTGREST_OPTION_INVALID', `${label} must be an integer from ${min} through ${max}`);
|
|
343
|
+
}
|
|
344
|
+
return resolved;
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=postgrest.js.map
|
|
@@ -22,7 +22,12 @@ export { ObsidianAdapter } from './backends/obsidian.js';
|
|
|
22
22
|
export { LogseqAdapter } from './backends/logseq.js';
|
|
23
23
|
export { FortemiAdapter } from './backends/fortemi.js';
|
|
24
24
|
export { STORAGE_BACKEND_CONTRACT, STORAGE_BACKEND_MATRIX, StorageCapabilityError, negotiateStorageCapabilities, } from './backend-contract.js';
|
|
25
|
-
export { STORAGE_MIGRATION_PROTOCOL, MigrationProtocolError, StorageMigrationCoordinator, approvalDigest, digestRecords, validateManifest, } from './migration-protocol.js';
|
|
25
|
+
export { STORAGE_MIGRATION_PROTOCOL, MigrationProtocolError, StorageMigrationCoordinator, approvalDigest, digestRecordChunks, digestRecords, validateManifest, } from './migration-protocol.js';
|
|
26
|
+
export { PostgresStorageBackend, PostgresBackendError, } from './backends/postgres.js';
|
|
27
|
+
export { POSTGRES_SCHEMA_VERSION, POSTGRES_SCHEMA_V1_SQL, inspectPostgresSchema, upgradePostgresSchemaV1, rollbackPostgresSchemaV1, postgresLeastPrivilegeSql, } from './backends/postgres-schema.js';
|
|
28
|
+
export { PostgrestStorageBackend, PostgrestBackendError, } from './backends/postgrest.js';
|
|
29
|
+
export { POSTGREST_SCHEMA_V1_SQL, installPostgrestSchemaV1, postgrestLeastPrivilegeSql } from './backends/postgrest-schema.js';
|
|
30
|
+
export { STORAGE_QUALIFICATION_REPORT, StorageQualificationError, qualifyStorageBackend, verifyExactRecords, assertCurrentStorageEvidence, } from './qualification.js';
|
|
26
31
|
let state = null;
|
|
27
32
|
/**
|
|
28
33
|
* Initialize or reset the registry for a given project root. Most
|