@zincapp/znvault-migrate 1.0.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/README.md +74 -0
- package/dist/adapters/engine-adapter.d.ts +91 -0
- package/dist/adapters/engine-adapter.d.ts.map +1 -0
- package/dist/adapters/engine-adapter.js +1 -0
- package/dist/adapters/engine-adapter.js.map +1 -0
- package/dist/adapters/mysql/connection.d.ts +30 -0
- package/dist/adapters/mysql/connection.d.ts.map +1 -0
- package/dist/adapters/mysql/connection.js +132 -0
- package/dist/adapters/mysql/connection.js.map +1 -0
- package/dist/adapters/mysql/lock.d.ts +41 -0
- package/dist/adapters/mysql/lock.d.ts.map +1 -0
- package/dist/adapters/mysql/lock.js +77 -0
- package/dist/adapters/mysql/lock.js.map +1 -0
- package/dist/adapters/mysql/mysql-adapter.d.ts +11 -0
- package/dist/adapters/mysql/mysql-adapter.d.ts.map +1 -0
- package/dist/adapters/mysql/mysql-adapter.js +130 -0
- package/dist/adapters/mysql/mysql-adapter.js.map +1 -0
- package/dist/adapters/mysql/mysql-conn.d.ts +22 -0
- package/dist/adapters/mysql/mysql-conn.d.ts.map +1 -0
- package/dist/adapters/mysql/mysql-conn.js +1 -0
- package/dist/adapters/mysql/mysql-conn.js.map +1 -0
- package/dist/adapters/mysql/scaffolding.d.ts +49 -0
- package/dist/adapters/mysql/scaffolding.d.ts.map +1 -0
- package/dist/adapters/mysql/scaffolding.js +70 -0
- package/dist/adapters/mysql/scaffolding.js.map +1 -0
- package/dist/adapters/mysql/schema-migrations-repo.d.ts +42 -0
- package/dist/adapters/mysql/schema-migrations-repo.d.ts.map +1 -0
- package/dist/adapters/mysql/schema-migrations-repo.js +77 -0
- package/dist/adapters/mysql/schema-migrations-repo.js.map +1 -0
- package/dist/adapters/mysql/sql-splitter.d.ts +15 -0
- package/dist/adapters/mysql/sql-splitter.d.ts.map +1 -0
- package/dist/adapters/mysql/sql-splitter.js +160 -0
- package/dist/adapters/mysql/sql-splitter.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/core/baseline-marker.d.ts +2 -0
- package/dist/core/baseline-marker.d.ts.map +1 -0
- package/dist/core/baseline-marker.js +10 -0
- package/dist/core/baseline-marker.js.map +1 -0
- package/dist/core/checksum.d.ts +18 -0
- package/dist/core/checksum.d.ts.map +1 -0
- package/dist/core/checksum.js +30 -0
- package/dist/core/checksum.js.map +1 -0
- package/dist/core/migration-files.d.ts +8 -0
- package/dist/core/migration-files.d.ts.map +1 -0
- package/dist/core/migration-files.js +32 -0
- package/dist/core/migration-files.js.map +1 -0
- package/dist/core/planner.d.ts +36 -0
- package/dist/core/planner.d.ts.map +1 -0
- package/dist/core/planner.js +81 -0
- package/dist/core/planner.js.map +1 -0
- package/dist/core/runner.d.ts +159 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +301 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/types.d.ts +24 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lease/dynamic-secrets-client.d.ts +43 -0
- package/dist/lease/dynamic-secrets-client.d.ts.map +1 -0
- package/dist/lease/dynamic-secrets-client.js +110 -0
- package/dist/lease/dynamic-secrets-client.js.map +1 -0
- package/dist/lease/run-migrations.d.ts +169 -0
- package/dist/lease/run-migrations.d.ts.map +1 -0
- package/dist/lease/run-migrations.js +302 -0
- package/dist/lease/run-migrations.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
import { discover } from './migration-files.js';
|
|
4
|
+
import { readBaselineMarker } from './baseline-marker.js';
|
|
5
|
+
import { canonicalChecksumFile } from './checksum.js';
|
|
6
|
+
import { plan } from './planner.js';
|
|
7
|
+
/**
|
|
8
|
+
* Orchestrates migration discovery, planning, locking, and execution.
|
|
9
|
+
*
|
|
10
|
+
* Ports MigrationRunner.kt + MigrateMain.kt orchestration EXACTLY:
|
|
11
|
+
* - status(): read-only — ensureTable + plan; preflight(version-check only).
|
|
12
|
+
* - run(): write path — preflight(requireWritePrimary=true) → ensureTable (before lock,
|
|
13
|
+
* Kotlin parity) → acquire → seedIfVirgin → plan → reconcile → pending → release.
|
|
14
|
+
*
|
|
15
|
+
* `zn_*` helper procedures (0000_ files) are NOT created by this engine — see the
|
|
16
|
+
* doc comment on run() for details.
|
|
17
|
+
*
|
|
18
|
+
* Engine-agnostic core: the six MySQL-specific collaborators (preflight, lock
|
|
19
|
+
* acquire/isHeld/release, migration-file apply, reconcile, definer-object cleanup,
|
|
20
|
+
* schema_migrations repo) are delegated to an injected `EngineAdapter` operating
|
|
21
|
+
* on a live `Conn`. The runner's own control flow — ordering, guards, error
|
|
22
|
+
* strings — is unchanged from the MySQL-only source.
|
|
23
|
+
*/
|
|
24
|
+
export class MigrationRunner {
|
|
25
|
+
adapter;
|
|
26
|
+
conn;
|
|
27
|
+
migrationsDir;
|
|
28
|
+
appliedBy;
|
|
29
|
+
integrityDirs;
|
|
30
|
+
scaffolding;
|
|
31
|
+
repo;
|
|
32
|
+
/**
|
|
33
|
+
* @param adapter The engine adapter — implements the six MySQL-specific
|
|
34
|
+
* collaborators (preflight, lock, migration-file apply,
|
|
35
|
+
* reconcile, definer-object cleanup, schema_migrations repo).
|
|
36
|
+
* @param conn The live connection the adapter's methods operate on.
|
|
37
|
+
* @param migrationsDir The CURRENT phase's migrations directory — the one whose
|
|
38
|
+
* files are classified and applied.
|
|
39
|
+
* @param appliedBy The applied-by identity recorded on each row.
|
|
40
|
+
* @param integrityDirs Additional directories that share this DB's
|
|
41
|
+
* schema_migrations history (the OTHER migration phase's dir,
|
|
42
|
+
* e.g. the pre/ dir when this runner is running post/). Used
|
|
43
|
+
* only to widen the orphan/checksum integrity lookup so a row
|
|
44
|
+
* applied by a sibling phase is not mistaken for a
|
|
45
|
+
* renamed/deleted file. Defaults to none (single-dir configs).
|
|
46
|
+
* @param scaffolding Optional migration-helper scaffolding config for THIS phase.
|
|
47
|
+
* When set, `run()` applies `<migrationsDir>/<filename>` at the
|
|
48
|
+
* start of the phase (after discover(), before seeding/reconcile/
|
|
49
|
+
* pending) and, unconditionally, drops every object the given
|
|
50
|
+
* `leaseUser` defined once the reconcile+pending work concludes
|
|
51
|
+
* (success or failure) — see the ordering note on run(). Undefined
|
|
52
|
+
* (the default) means no scaffolding: byte-identical to the
|
|
53
|
+
* pre-scaffolding runner.
|
|
54
|
+
*/
|
|
55
|
+
constructor(adapter, conn, migrationsDir, appliedBy, integrityDirs = [], scaffolding) {
|
|
56
|
+
this.adapter = adapter;
|
|
57
|
+
this.conn = conn;
|
|
58
|
+
this.migrationsDir = migrationsDir;
|
|
59
|
+
this.appliedBy = appliedBy;
|
|
60
|
+
this.integrityDirs = integrityDirs;
|
|
61
|
+
this.scaffolding = scaffolding;
|
|
62
|
+
this.repo = adapter.schemaMigrationsRepo(conn);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the union of migration files across this phase's dir and any sibling
|
|
66
|
+
* integrity dirs (pre ∪ post), for the planner's integrity lookup. Missing dirs
|
|
67
|
+
* are skipped defensively (discover() throws on a non-existent path). If two dirs
|
|
68
|
+
* ever declare the same version prefix, the current phase's file wins the lookup.
|
|
69
|
+
*/
|
|
70
|
+
allTrackedFiles(phaseFiles) {
|
|
71
|
+
const byVersion = new Map(phaseFiles.map((f) => [f.version, f]));
|
|
72
|
+
for (const dir of this.integrityDirs) {
|
|
73
|
+
if (dir === this.migrationsDir || !existsSync(dir))
|
|
74
|
+
continue;
|
|
75
|
+
for (const f of discover(dir)) {
|
|
76
|
+
if (!byVersion.has(f.version))
|
|
77
|
+
byVersion.set(f.version, f);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return [...byVersion.values()];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Read-only status: how many migrations are in each state.
|
|
84
|
+
* Does NOT acquire the lock or apply any helpers.
|
|
85
|
+
* Mirrors MigrateMain: preflight(requireWritePrimary=false) for 'status'.
|
|
86
|
+
*/
|
|
87
|
+
async status() {
|
|
88
|
+
await this.adapter.preflight(this.conn, false); // version-check only; skip read-only check
|
|
89
|
+
await this.repo.ensureTable();
|
|
90
|
+
const phaseFiles = discover(this.migrationsDir);
|
|
91
|
+
const p = plan(phaseFiles, await this.repo.all(), canonicalChecksumFile, this.allTrackedFiles(phaseFiles));
|
|
92
|
+
return { applied: p.applied.length, reconcile: p.reconcile.length, pending: p.pending.length };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Apply all pending and reconcile migrations.
|
|
96
|
+
*
|
|
97
|
+
* Order (load-bearing — matches Kotlin MigrationRunner.run() exactly):
|
|
98
|
+
* 1. preflight(requireWritePrimary=true) — refuse read-only replicas.
|
|
99
|
+
* 2. ensureTable() BEFORE the lock (the only pre-lock mutation, per Kotlin parity).
|
|
100
|
+
* 3. acquire(db) — GET_LOCK.
|
|
101
|
+
* 3a. scaffolding (if configured) — apply THIS phase's helper objects (migration_utils.sql
|
|
102
|
+
* or equivalent) before any seeding/reconcile/pending work touches the DB.
|
|
103
|
+
* 4. seedBaselineIfVirgin — seed baselined rows when schema_migrations is empty.
|
|
104
|
+
* 5. plan() — classify remaining files.
|
|
105
|
+
* 6. reconcile — asserts-first; re-run body only when unmet or no asserts.
|
|
106
|
+
* 7. pending — claim(success=0) → exec → markSuccess(success=1).
|
|
107
|
+
* 7a. finally (nested, scoped to 6+7): scaffolding cleanup — if configured and the
|
|
108
|
+
* lock is still held, drop every object the lease user defined
|
|
109
|
+
* (dropDefinerObjects), unconditionally — runs whether 6/7 succeeded or threw.
|
|
110
|
+
* A cleanup failure is logged, never rethrown (must not mask a primary error).
|
|
111
|
+
* The lock-held check itself (lockHeld()) is also non-throwing — a dead
|
|
112
|
+
* connection can make the underlying IS_USED_LOCK query reject rather than
|
|
113
|
+
* resolve false, and this guard must not mask a primary error either.
|
|
114
|
+
* 8. finally: release lock; if release was not clean and no primary error, throw tripwire.
|
|
115
|
+
*
|
|
116
|
+
* The `zn_*` helper procedures (0000_ files) are NO LONGER created here. They are
|
|
117
|
+
* now provisioned ahead of the migration phase by vault's routines-apply step,
|
|
118
|
+
* owned by a persistent routines DB account — see the vault-side dynsec-routines-
|
|
119
|
+
* provisioning feature (docs/superpowers/specs/2026-07-01-dynsec-routines-
|
|
120
|
+
* provisioning-design.md). Root cause: DROP+CREATE'ing them here made the
|
|
121
|
+
* ephemeral migrate user their DEFINER, and MySQL 8.4 refuses `DROP USER` for an
|
|
122
|
+
* account referenced as a stored-routine DEFINER (ER 4006) — which broke lease
|
|
123
|
+
* revocation on every migration run. Migrations now only `CALL zn_*`; they never
|
|
124
|
+
* (re)create the procedures themselves.
|
|
125
|
+
*/
|
|
126
|
+
async run() {
|
|
127
|
+
await this.adapter.preflight(this.conn, true); // refuse read-only replica
|
|
128
|
+
// ensureTable BEFORE the lock — the CREATE TABLE IF NOT EXISTS is the sole
|
|
129
|
+
// pre-lock mutation. This matches Kotlin to allow a fresh DB to get the table
|
|
130
|
+
// without contention risk (only one runner is expected in normal operation).
|
|
131
|
+
await this.repo.ensureTable();
|
|
132
|
+
await this.adapter.acquireLock(this.conn);
|
|
133
|
+
let primary = null;
|
|
134
|
+
try {
|
|
135
|
+
const files = discover(this.migrationsDir);
|
|
136
|
+
// NOTE: 0000_ helper files are discovered above (discover() does not filter
|
|
137
|
+
// them out) but are intentionally never applied here — plan() excludes them
|
|
138
|
+
// from all buckets (migration-planner.ts) and seedBaselineIfVirgin() skips
|
|
139
|
+
// them too, so they are inert as far as this engine is concerned. See the
|
|
140
|
+
// class doc comment on run() for why per-run creation was removed.
|
|
141
|
+
// Step 3a: Scaffolding — create THIS phase's migration-helper objects (dropped
|
|
142
|
+
// after reconcile+pending, unconditionally — see the finally block below).
|
|
143
|
+
// Applied before any seeding/reconcile/pending work so bodies can CALL them.
|
|
144
|
+
if (this.scaffolding && this.adapter.applyScaffolding) {
|
|
145
|
+
const scaffoldPath = isAbsolute(this.scaffolding.filename)
|
|
146
|
+
? this.scaffolding.filename
|
|
147
|
+
: join(this.migrationsDir, this.scaffolding.filename);
|
|
148
|
+
await this.adapter.applyScaffolding(this.conn, scaffoldPath);
|
|
149
|
+
}
|
|
150
|
+
// Step 4: Seed baseline rows for a virgin DB (schema_migrations is empty).
|
|
151
|
+
const seeded = await this.seedBaselineIfVirgin(files);
|
|
152
|
+
// Step 5: Plan. Integrity lookup spans this dir ∪ the sibling phase dir(s)
|
|
153
|
+
// (they share one schema_migrations table); classification stays scoped to
|
|
154
|
+
// `files` (this phase only ever applies its own directory).
|
|
155
|
+
const p = plan(files, await this.repo.all(), canonicalChecksumFile, this.allTrackedFiles(files));
|
|
156
|
+
let reconciled = 0;
|
|
157
|
+
let applied = 0;
|
|
158
|
+
try {
|
|
159
|
+
// Step 6: Reconcile — asserts-first; re-run body only when asserts are absent
|
|
160
|
+
// or one failed (indicating the migration was only partially applied).
|
|
161
|
+
for (const f of p.reconcile) {
|
|
162
|
+
await this.requireLockHeld();
|
|
163
|
+
await this.reconcile(f.path);
|
|
164
|
+
reconciled++;
|
|
165
|
+
}
|
|
166
|
+
// Step 7: Pending — claim(success=0) → execute body → markSuccess(success=1).
|
|
167
|
+
// A throw in executeStatements leaves the row at success=0 for later reconcile.
|
|
168
|
+
for (const f of p.pending) {
|
|
169
|
+
await this.requireLockHeld();
|
|
170
|
+
const checksum = canonicalChecksumFile(f.path);
|
|
171
|
+
await this.repo.claim(f.version, checksum, this.appliedBy); // autocommit → durable
|
|
172
|
+
const start = Date.now();
|
|
173
|
+
await this.executeStatements(f.path); // throws → row stays success=0
|
|
174
|
+
await this.requireLockHeld();
|
|
175
|
+
await this.repo.markSuccess(f.version, Date.now() - start);
|
|
176
|
+
applied++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
// Step 7a: Scaffolding cleanup — drop every object the lease user defined,
|
|
181
|
+
// so the ephemeral migrate user is never left a DEFINER (ER-4006). Runs
|
|
182
|
+
// whether the reconcile/pending block above succeeded or threw (unconditional),
|
|
183
|
+
// but ONLY when scaffolding is configured AND the lock is still held (a lost
|
|
184
|
+
// lock means another runner may be concurrently active — do not touch the DB).
|
|
185
|
+
if (this.scaffolding && (await this.lockHeld())) {
|
|
186
|
+
try {
|
|
187
|
+
await this.adapter.dropOwnedObjects(this.conn, this.scaffolding.leaseUser);
|
|
188
|
+
}
|
|
189
|
+
catch (cleanupErr) {
|
|
190
|
+
// Log but do not rethrow — this must never mask a primary phase error.
|
|
191
|
+
// A failed/partial sweep surfaces later via the eventual DROP USER
|
|
192
|
+
// re-hitting ER-4006, which is visible and actionable.
|
|
193
|
+
console.warn(`scaffolding cleanup (dropDefinerObjects) failed for lease user '${this.scaffolding.leaseUser}': ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { seeded, reconciled, applied, pendingRemaining: 0 };
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
primary = e;
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
// release() never throws — it must not mask the primary error.
|
|
205
|
+
// A non-clean release is the lost-lock tripwire: surface it only when
|
|
206
|
+
// there is no primary error to preserve.
|
|
207
|
+
const released = await this.adapter.releaseLock(this.conn);
|
|
208
|
+
if (!released && primary === null) {
|
|
209
|
+
// eslint-disable-next-line no-unsafe-finally
|
|
210
|
+
throw new Error('Lock was not held at release — a concurrent runner may have run. Re-run status and verify schema_migrations.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Abort immediately if this session no longer holds the GET_LOCK.
|
|
216
|
+
* A killed session or proxy reconnect would silently drop the lock server-side.
|
|
217
|
+
*/
|
|
218
|
+
async requireLockHeld() {
|
|
219
|
+
if (!(await this.adapter.lockHeld(this.conn))) {
|
|
220
|
+
throw new Error('Lost the migration lock mid-run (session killed or reconnected). Aborting to avoid concurrent DDL.');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Non-throwing counterpart to requireLockHeld() — used by cleanup paths (e.g.
|
|
225
|
+
* scaffolding's finally block) that must NOT touch the DB once the lock is lost,
|
|
226
|
+
* but also must not themselves throw while already unwinding another error.
|
|
227
|
+
*
|
|
228
|
+
* `lock.isHeld()` runs `SELECT IS_USED_LOCK(...)`, which can itself REJECT on a
|
|
229
|
+
* dead connection (killed session / proxy reconnect — exactly the scenario this
|
|
230
|
+
* guard exists for), not just resolve to false. Since callers await this from a
|
|
231
|
+
* `finally` block, an unswallowed rejection here would replace/mask whatever
|
|
232
|
+
* primary error is already in flight (a throwing `finally` overrides the pending
|
|
233
|
+
* exception). Treat any error the same as "lock not held": skip the guarded
|
|
234
|
+
* cleanup rather than risk touching a possibly-dead connection or masking the
|
|
235
|
+
* real error.
|
|
236
|
+
*/
|
|
237
|
+
async lockHeld() {
|
|
238
|
+
try {
|
|
239
|
+
return await this.adapter.lockHeld(this.conn);
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
console.warn(`lock.isHeld() failed while checking scaffolding cleanup eligibility (treating as lock-not-held): ${e instanceof Error ? e.message : String(e)}`);
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Execute every SQL statement in a file (text protocol, one at a time).
|
|
248
|
+
* A failed CALL zn_assert_* signals SIGNAL '45000', which propagates as an
|
|
249
|
+
* Error and leaves any pending claim row at success=0.
|
|
250
|
+
*
|
|
251
|
+
* The split+per-statement-execute logic lives on the adapter (`applyMigrationFile`)
|
|
252
|
+
* — core only reads the file and hands the SQL text over.
|
|
253
|
+
*/
|
|
254
|
+
async executeStatements(path) {
|
|
255
|
+
const sql = readFileSync(path, 'utf8');
|
|
256
|
+
await this.adapter.applyMigrationFile(this.conn, sql);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Reconcile a success=0 row per spec step 7a.
|
|
260
|
+
*
|
|
261
|
+
* Run the file's trailing CALL zn_assert_* postconditions FIRST:
|
|
262
|
+
* - If they exist and all pass → the migration is already fully applied;
|
|
263
|
+
* markSuccess WITHOUT re-running the body (avoids double-apply hazard for
|
|
264
|
+
* non-idempotent bodies).
|
|
265
|
+
* - If there are no asserts, OR any assert throws → re-run the ENTIRE
|
|
266
|
+
* (idempotent) body, then markSuccess.
|
|
267
|
+
*
|
|
268
|
+
* The asserts-first logic (and the requireLockHeld() guard before markSuccess)
|
|
269
|
+
* lives on the adapter (`reconcile`) — core only reads the file, derives the
|
|
270
|
+
* version, and delegates.
|
|
271
|
+
*/
|
|
272
|
+
async reconcile(path) {
|
|
273
|
+
const fileSql = readFileSync(path, 'utf8');
|
|
274
|
+
const version = path.split('/').pop();
|
|
275
|
+
await this.adapter.reconcile(this.conn, version, fileSql, this.repo);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Seed baselined rows for a virgin DB (schema_migrations has no rows yet).
|
|
279
|
+
*
|
|
280
|
+
* Reads the BASELINE_MARKER from baseline/00-baseline-schema.sql and seeds
|
|
281
|
+
* a baselined row for every non-0000_ file whose prefix <= the marker.
|
|
282
|
+
* Returns the number of rows seeded.
|
|
283
|
+
*/
|
|
284
|
+
async seedBaselineIfVirgin(files) {
|
|
285
|
+
if ((await this.repo.all()).length > 0)
|
|
286
|
+
return 0;
|
|
287
|
+
const marker = readBaselineMarker(join(this.migrationsDir, 'baseline', '00-baseline-schema.sql'));
|
|
288
|
+
if (!marker)
|
|
289
|
+
return 0; // no baseline file (unit/throwaway DB) → nothing to seed
|
|
290
|
+
let n = 0;
|
|
291
|
+
for (const f of files) {
|
|
292
|
+
if (f.version.startsWith('0000_'))
|
|
293
|
+
continue; // helpers are never seeded
|
|
294
|
+
if (f.prefix <= marker) {
|
|
295
|
+
await this.repo.seedBaseline(f.version, canonicalChecksumFile(f.path), this.appliedBy);
|
|
296
|
+
n++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return n;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/core/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface MigrationFile {
|
|
2
|
+
version: string;
|
|
3
|
+
prefix: string;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MigrationRow {
|
|
7
|
+
version: string;
|
|
8
|
+
checksum: string;
|
|
9
|
+
checksumAlgo: string;
|
|
10
|
+
success: boolean;
|
|
11
|
+
baselined: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface MigrationPlan {
|
|
14
|
+
applied: MigrationFile[];
|
|
15
|
+
reconcile: MigrationFile[];
|
|
16
|
+
pending: MigrationFile[];
|
|
17
|
+
}
|
|
18
|
+
export interface RunResult {
|
|
19
|
+
seeded: number;
|
|
20
|
+
reconciled: number;
|
|
21
|
+
applied: number;
|
|
22
|
+
pendingRemaining: number;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAAG,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAEjF,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;CAC1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { MigrationConfig } from './config.js';
|
|
2
|
+
export { validateMigrationConfig } from './config.js';
|
|
3
|
+
export { MigrationRunner } from './core/runner.js';
|
|
4
|
+
export type { MigrationFile, MigrationRow, RunResult, MigrationPlan } from './core/types.js';
|
|
5
|
+
export type { EngineAdapter, Conn, SchemaMigrationsRepo, EngineCredentials } from './adapters/engine-adapter.js';
|
|
6
|
+
export { mysqlAdapter } from './adapters/mysql/mysql-adapter.js';
|
|
7
|
+
export { ChecksumMismatchError, OrphanTrackedRowError } from './core/planner.js';
|
|
8
|
+
export { DuplicatePrefixError } from './core/migration-files.js';
|
|
9
|
+
export { runMigrations, defaultDeps } from './lease/run-migrations.js';
|
|
10
|
+
export type { RunMigrationsOpts, RunMigrationsDeps } from './lease/run-migrations.js';
|
|
11
|
+
export type { Lease } from './lease/dynamic-secrets-client.js';
|
|
12
|
+
export { makeDynamicSecretsClient } from './lease/dynamic-secrets-client.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE7F,YAAY,EAAE,aAAa,EAAE,IAAI,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjH,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAGjE,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAIjE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACtF,YAAY,EAAE,KAAK,EAAE,MAAM,mCAAmC,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Path: znvault-migrate/src/index.ts
|
|
2
|
+
// Public API surface for @zincapp/znvault-migrate.
|
|
3
|
+
// Consumed by znvault-plugin-payara (Phase 2) and znvault-cli (Phase 3).
|
|
4
|
+
export { validateMigrationConfig } from './config.js';
|
|
5
|
+
export { MigrationRunner } from './core/runner.js';
|
|
6
|
+
export { mysqlAdapter } from './adapters/mysql/mysql-adapter.js';
|
|
7
|
+
// Error classes surfaced by the core so consumers can catch/discriminate them.
|
|
8
|
+
export { ChecksumMismatchError, OrphanTrackedRowError } from './core/planner.js';
|
|
9
|
+
export { DuplicatePrefixError } from './core/migration-files.js';
|
|
10
|
+
// Lease layer (Task 7): mint a dynamic-secrets lease → open a connection via
|
|
11
|
+
// the selected engine adapter → run the migration engine → revoke the lease.
|
|
12
|
+
export { runMigrations, defaultDeps } from './lease/run-migrations.js';
|
|
13
|
+
export { makeDynamicSecretsClient } from './lease/dynamic-secrets-client.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,mDAAmD;AACnD,yEAAyE;AAGzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEjE,+EAA+E;AAC/E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEjE,6EAA6E;AAC7E,6EAA6E;AAC7E,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAGvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic-secrets REST client for ZN-Vault.
|
|
3
|
+
*
|
|
4
|
+
* These are raw async functions hitting the Vault REST API — NOT the znvault-cli
|
|
5
|
+
* command handlers (which call inquirer.prompt / process.exit and are unusable as
|
|
6
|
+
* lifecycle primitives).
|
|
7
|
+
*
|
|
8
|
+
* Key design (Codex F6.1): a 404 / 410 / non-ACTIVE-lease response on revokeCredential
|
|
9
|
+
* is treated as SUCCESS — the lease is already gone. This is critical because a
|
|
10
|
+
* timed-out-then-retried revoke hits a now-non-ACTIVE lease on the 2nd attempt and
|
|
11
|
+
* must not be retried into an error.
|
|
12
|
+
*/
|
|
13
|
+
export interface Lease {
|
|
14
|
+
leaseId: string;
|
|
15
|
+
username: string;
|
|
16
|
+
password: string;
|
|
17
|
+
/** MySQL hostname returned by the Vault dynamic-secrets connection. */
|
|
18
|
+
host: string;
|
|
19
|
+
/** MySQL port returned by the Vault dynamic-secrets connection. */
|
|
20
|
+
port: number;
|
|
21
|
+
/** MySQL database name returned by the Vault dynamic-secrets connection (may be undefined if the connection doesn't pin a database). */
|
|
22
|
+
database?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface VaultHttp {
|
|
25
|
+
post(path: string, body: unknown): Promise<{
|
|
26
|
+
status: number;
|
|
27
|
+
body: any;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a dynamic-secrets client bound to the given HTTP transport.
|
|
32
|
+
*
|
|
33
|
+
* @param http - The vault HTTP client (e.g. the plugin's existing VaultHttp).
|
|
34
|
+
*/
|
|
35
|
+
export declare function makeDynamicSecretsClient(http: VaultHttp): {
|
|
36
|
+
issueCredential(roleId: string, opts: {
|
|
37
|
+
ttlSeconds: number;
|
|
38
|
+
}): Promise<Lease>;
|
|
39
|
+
revokeCredential(leaseId: string, opts: {
|
|
40
|
+
reason: string;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=dynamic-secrets-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-secrets-client.d.ts","sourceRoot":"","sources":["../../src/lease/dynamic-secrets-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,wIAAwI;IACxI,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;CAC3E;AAmCD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,GAAG;IACzD,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9E,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E,CAkEA"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic-secrets REST client for ZN-Vault.
|
|
3
|
+
*
|
|
4
|
+
* These are raw async functions hitting the Vault REST API — NOT the znvault-cli
|
|
5
|
+
* command handlers (which call inquirer.prompt / process.exit and are unusable as
|
|
6
|
+
* lifecycle primitives).
|
|
7
|
+
*
|
|
8
|
+
* Key design (Codex F6.1): a 404 / 410 / non-ACTIVE-lease response on revokeCredential
|
|
9
|
+
* is treated as SUCCESS — the lease is already gone. This is critical because a
|
|
10
|
+
* timed-out-then-retried revoke hits a now-non-ACTIVE lease on the 2nd attempt and
|
|
11
|
+
* must not be retried into an error.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Classify a revoke error as "already gone" (the lease no longer exists).
|
|
15
|
+
*
|
|
16
|
+
* A lease is already gone when:
|
|
17
|
+
* - The error carries HTTP status 404 (not found) or 410 (gone).
|
|
18
|
+
* - The error carries a response body indicating a non-ACTIVE or not-found lease state
|
|
19
|
+
* (e.g. status: 'EXPIRED', 'REVOKED', 'NOT_FOUND').
|
|
20
|
+
*
|
|
21
|
+
* Any other error (500, network failure, etc.) is NOT already-gone.
|
|
22
|
+
*/
|
|
23
|
+
function isAlreadyGone(e) {
|
|
24
|
+
if (e == null || typeof e !== 'object')
|
|
25
|
+
return false;
|
|
26
|
+
const obj = e;
|
|
27
|
+
// 404 / 410 by HTTP status
|
|
28
|
+
const status = obj['status'];
|
|
29
|
+
if (status === 404 || status === 410)
|
|
30
|
+
return true;
|
|
31
|
+
// Response body carries a non-ACTIVE lease status
|
|
32
|
+
const body = obj['body'];
|
|
33
|
+
if (body != null && typeof body === 'object') {
|
|
34
|
+
const bodyStatus = body['status'];
|
|
35
|
+
if (typeof bodyStatus === 'string' &&
|
|
36
|
+
bodyStatus !== 'ACTIVE') {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a dynamic-secrets client bound to the given HTTP transport.
|
|
44
|
+
*
|
|
45
|
+
* @param http - The vault HTTP client (e.g. the plugin's existing VaultHttp).
|
|
46
|
+
*/
|
|
47
|
+
export function makeDynamicSecretsClient(http) {
|
|
48
|
+
return {
|
|
49
|
+
/**
|
|
50
|
+
* Issue a new dynamic-secrets credential for the given role.
|
|
51
|
+
*
|
|
52
|
+
* POST /v1/dynamic-secrets/roles/:roleId/credentials
|
|
53
|
+
* → { leaseId, username, password, host, port, database? }
|
|
54
|
+
*
|
|
55
|
+
* CRITICAL: validates that host and port are present in the response.
|
|
56
|
+
* If missing, the just-minted lease is revoked (best-effort) before
|
|
57
|
+
* throwing so the orphaned credential is not left to expire passively.
|
|
58
|
+
* Mirrors the znvault-cli mysql broker pattern (spec F2).
|
|
59
|
+
*/
|
|
60
|
+
async issueCredential(roleId, opts) {
|
|
61
|
+
const path = `/v1/dynamic-secrets/roles/${roleId}/credentials`;
|
|
62
|
+
const res = await http.post(path, { ttlSeconds: opts.ttlSeconds });
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
const body = res.body;
|
|
65
|
+
// Validate that the server returned host and port (spec F2 — no --host fallback).
|
|
66
|
+
if (!body.host || body.port === undefined) {
|
|
67
|
+
// Best-effort revoke of the just-minted partial lease before throwing.
|
|
68
|
+
// Wrap in try/catch so a revoke failure does not mask the validation error.
|
|
69
|
+
try {
|
|
70
|
+
const revokePath = `/v1/dynamic-secrets/leases/${body.leaseId}/revoke`;
|
|
71
|
+
await http.post(revokePath, { reason: 'incomplete credential' });
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Intentionally swallowed — cleanup job + TTL will expire the lease.
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`Vault did not return host/port in the credential for role '${roleId}'. ` +
|
|
77
|
+
`Please upgrade vault to a version that returns host/port in dynamic-secret credentials.`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
leaseId: body.leaseId,
|
|
81
|
+
username: body.username,
|
|
82
|
+
password: body.password,
|
|
83
|
+
host: body.host,
|
|
84
|
+
port: body.port,
|
|
85
|
+
database: body.database,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* Revoke an existing dynamic-secrets credential lease.
|
|
90
|
+
*
|
|
91
|
+
* POST /v1/dynamic-secrets/leases/:leaseId/revoke
|
|
92
|
+
*
|
|
93
|
+
* CRITICAL (Codex F6.1): a 404 / 410 / non-ACTIVE-lease error is treated as
|
|
94
|
+
* SUCCESS — the lease is already gone. Resolve, do NOT throw.
|
|
95
|
+
*/
|
|
96
|
+
async revokeCredential(leaseId, opts) {
|
|
97
|
+
const path = `/v1/dynamic-secrets/leases/${leaseId}/revoke`;
|
|
98
|
+
try {
|
|
99
|
+
await http.post(path, { reason: opts.reason });
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (isAlreadyGone(e)) {
|
|
103
|
+
// Lease is already gone — treat as success per spec F6.1.
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-secrets-client.js","sourceRoot":"","sources":["../../src/lease/dynamic-secrets-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAkBH;;;;;;;;;GASG;AACH,SAAS,aAAa,CAAC,CAAU;IAC/B,IAAI,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACrD,MAAM,GAAG,GAAG,CAA4B,CAAC;IAEzC,2BAA2B;IAC3B,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAElD,kDAAkD;IAClD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7C,MAAM,UAAU,GAAI,IAAgC,CAAC,QAAQ,CAAC,CAAC;QAC/D,IACE,OAAO,UAAU,KAAK,QAAQ;YAC9B,UAAU,KAAK,QAAQ,EACvB,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAe;IAItD,OAAO;QACL;;;;;;;;;;WAUG;QACH,KAAK,CAAC,eAAe,CAAC,MAAc,EAAE,IAA4B;YAChE,MAAM,IAAI,GAAG,6BAA6B,MAAM,cAAc,CAAC;YAC/D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACnE,8DAA8D;YAC9D,MAAM,IAAI,GAAG,GAAG,CAAC,IAAW,CAAC;YAE7B,kFAAkF;YAClF,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC1C,uEAAuE;gBACvE,4EAA4E;gBAC5E,IAAI,CAAC;oBACH,MAAM,UAAU,GAAG,8BAA8B,IAAI,CAAC,OAAiB,SAAS,CAAC;oBACjF,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC,CAAC;gBACnE,CAAC;gBAAC,MAAM,CAAC;oBACP,qEAAqE;gBACvE,CAAC;gBACD,MAAM,IAAI,KAAK,CACb,8DAA8D,MAAM,KAAK;oBACzE,yFAAyF,CAC1F,CAAC;YACJ,CAAC;YAED,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,OAAiB;gBAC/B,QAAQ,EAAE,IAAI,CAAC,QAAkB;gBACjC,QAAQ,EAAE,IAAI,CAAC,QAAkB;gBACjC,IAAI,EAAE,IAAI,CAAC,IAAc;gBACzB,IAAI,EAAE,IAAI,CAAC,IAAc;gBACzB,QAAQ,EAAE,IAAI,CAAC,QAA8B;aAC9C,CAAC;QACJ,CAAC;QAED;;;;;;;WAOG;QACH,KAAK,CAAC,gBAAgB,CAAC,OAAe,EAAE,IAAwB;YAC9D,MAAM,IAAI,GAAG,8BAA8B,OAAO,SAAS,CAAC;YAC5D,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YACjD,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;oBACrB,0DAA0D;oBAC1D,OAAO;gBACT,CAAC;gBACD,MAAM,CAAC,CAAC;YACV,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|