@steve31415/baselib 3.48.2 → 3.49.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/dist/db.js +41 -8
- package/dist/extension-self-update.d.ts +17 -5
- package/dist/extension-self-update.js +18 -6
- package/package.json +1 -1
package/dist/db.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (per-checkout instance — see TESTING.md). Cloud Run: the Cloud SQL Node
|
|
3
3
|
// connector with IAM authentication (CLOUD_SQL_INSTANCE + DB_IAM_USER +
|
|
4
4
|
// DB_NAME env vars) — no database passwords anywhere in the serving path.
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { readdir, readFile } from 'node:fs/promises';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import pg from 'pg';
|
|
@@ -367,8 +368,10 @@ export async function endPool(pool) {
|
|
|
367
368
|
await allClosed;
|
|
368
369
|
}
|
|
369
370
|
// Plain-SQL migrations: numbered files (001-*.sql, 002-*.sql, …) applied in
|
|
370
|
-
// order, recorded in schema_migrations
|
|
371
|
-
// an advisory lock so concurrent boots can't
|
|
371
|
+
// order, recorded in schema_migrations with their content's sha256, each
|
|
372
|
+
// inside a transaction, guarded by an advisory lock so concurrent boots can't
|
|
373
|
+
// race. Applied at service boot; an applied file whose content has since
|
|
374
|
+
// changed fails the boot.
|
|
372
375
|
// Lock loss (see logPoolErrors): all the work runs on the lock's own client,
|
|
373
376
|
// so a dead connection fails the next statement and the boot aborts — no
|
|
374
377
|
// window in which a migration runs unlocked.
|
|
@@ -380,18 +383,48 @@ export async function migrate(pool, migrationsDir, logger) {
|
|
|
380
383
|
await client.query('select pg_advisory_lock(hashtext($1))', ['baselib_migrate']);
|
|
381
384
|
await client.query(`create table if not exists schema_migrations (
|
|
382
385
|
name text primary key,
|
|
383
|
-
applied_at timestamptz not null default now()
|
|
386
|
+
applied_at timestamptz not null default now(),
|
|
387
|
+
sha256 text
|
|
384
388
|
)`);
|
|
385
|
-
|
|
386
|
-
const
|
|
389
|
+
await client.query('alter table schema_migrations add column if not exists sha256 text');
|
|
390
|
+
const { rows } = await client.query('select name, sha256 from schema_migrations');
|
|
391
|
+
const recorded = new Map(rows.map((r) => [r.name, r.sha256]));
|
|
392
|
+
const contents = new Map();
|
|
387
393
|
for (const file of files) {
|
|
388
|
-
if (done.has(file))
|
|
389
|
-
continue;
|
|
390
394
|
const sql = await readFile(join(migrationsDir, file), 'utf8');
|
|
395
|
+
contents.set(file, { sql, sha256: createHash('sha256').update(sql).digest('hex') });
|
|
396
|
+
}
|
|
397
|
+
// A shipped migration file is never edited (plasticine-way docs/DATABASE.md): the
|
|
398
|
+
// runner skips a recorded name, so an edit would never reach the database while the
|
|
399
|
+
// code relying on it ships. Refuse to boot instead, before applying anything, so the
|
|
400
|
+
// new revision fails its start and the old one keeps serving (people, 2026-11-02).
|
|
401
|
+
// Rows recorded before hashes were kept get the file's current hash, once.
|
|
402
|
+
const edited = [];
|
|
403
|
+
for (const file of files) {
|
|
404
|
+
if (!recorded.has(file))
|
|
405
|
+
continue;
|
|
406
|
+
const { sha256 } = contents.get(file);
|
|
407
|
+
const was = recorded.get(file);
|
|
408
|
+
if (was == null) {
|
|
409
|
+
await client.query('update schema_migrations set sha256 = $2 where name = $1', [file, sha256]);
|
|
410
|
+
}
|
|
411
|
+
else if (was !== sha256) {
|
|
412
|
+
edited.push(file);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (edited.length > 0) {
|
|
416
|
+
throw new Error(`migration ${edited.join(', ')} was edited after it was applied; a shipped migration is never ` +
|
|
417
|
+
'edited: restore the file and put the change in a new migration (a local database holding an ' +
|
|
418
|
+
'unshipped draft: drop it and re-run)');
|
|
419
|
+
}
|
|
420
|
+
for (const file of files) {
|
|
421
|
+
if (recorded.has(file))
|
|
422
|
+
continue;
|
|
423
|
+
const { sql, sha256 } = contents.get(file);
|
|
391
424
|
try {
|
|
392
425
|
await client.query('begin');
|
|
393
426
|
await client.query(sql);
|
|
394
|
-
await client.query('insert into schema_migrations (name) values ($1)', [file]);
|
|
427
|
+
await client.query('insert into schema_migrations (name, sha256) values ($1, $2)', [file, sha256]);
|
|
395
428
|
await client.query('commit');
|
|
396
429
|
}
|
|
397
430
|
catch (err) {
|
|
@@ -46,13 +46,25 @@ export declare function codeIdOf(files: Record<string, string>): Promise<string>
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function diskCode(): Promise<string | null>;
|
|
48
48
|
/**
|
|
49
|
-
* Record the code Chrome just loaded. Call from runtime.onInstalled
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* Record the code Chrome just loaded. Call from runtime.onInstalled ONLY: that
|
|
50
|
+
* is when Chrome registers the worker script afresh from disk (install,
|
|
51
|
+
* chrome.runtime.reload(), a click on reload). Not from runtime.onStartup: a
|
|
52
|
+
* browser restart re-reads an unpacked extension's manifest and content
|
|
53
|
+
* scripts from disk but keeps running the worker script it registered
|
|
54
|
+
* earlier, even when the file on disk changed (measured 2026-11-02: Lurch's
|
|
55
|
+
* card said 2.6 after a restart while its worker still ran 2.5's code). And
|
|
56
|
+
* never on a worker restart after eviction, which also runs the registered
|
|
57
|
+
* script whatever the disk holds now.
|
|
53
58
|
*/
|
|
54
59
|
export declare function recordLoadedCode(): Promise<string>;
|
|
55
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* The code this extension runs: the record from its last load. 'unverified'
|
|
62
|
+
* when there is no record, or when the record predates the manifest Chrome now
|
|
63
|
+
* shows (a browser restart picked up a new manifest while the worker may still
|
|
64
|
+
* be the old script) — never re-read from disk, which would label an old
|
|
65
|
+
* worker as the new code. 'unverified' differs from every deployed id, so the
|
|
66
|
+
* extension reloads itself into the deployed code once it is on disk.
|
|
67
|
+
*/
|
|
56
68
|
export declare function loadedCode(): Promise<string>;
|
|
57
69
|
/** Mark the extension busy for BUSY_LEASE_MS (an open overlay calls this through the worker every ~10 s). */
|
|
58
70
|
export declare function extendBusyLease(now?: number): Promise<void>;
|
|
@@ -105,10 +105,15 @@ export async function diskCode() {
|
|
|
105
105
|
return id;
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* Record the code Chrome just loaded. Call from runtime.onInstalled
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
108
|
+
* Record the code Chrome just loaded. Call from runtime.onInstalled ONLY: that
|
|
109
|
+
* is when Chrome registers the worker script afresh from disk (install,
|
|
110
|
+
* chrome.runtime.reload(), a click on reload). Not from runtime.onStartup: a
|
|
111
|
+
* browser restart re-reads an unpacked extension's manifest and content
|
|
112
|
+
* scripts from disk but keeps running the worker script it registered
|
|
113
|
+
* earlier, even when the file on disk changed (measured 2026-11-02: Lurch's
|
|
114
|
+
* card said 2.6 after a restart while its worker still ran 2.5's code). And
|
|
115
|
+
* never on a worker restart after eviction, which also runs the registered
|
|
116
|
+
* script whatever the disk holds now.
|
|
112
117
|
*/
|
|
113
118
|
export async function recordLoadedCode() {
|
|
114
119
|
const code = (await diskCode()) ?? 'unverified';
|
|
@@ -116,13 +121,20 @@ export async function recordLoadedCode() {
|
|
|
116
121
|
await chrome.storage.local.set({ [LOADED_KEY]: record });
|
|
117
122
|
return code;
|
|
118
123
|
}
|
|
119
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* The code this extension runs: the record from its last load. 'unverified'
|
|
126
|
+
* when there is no record, or when the record predates the manifest Chrome now
|
|
127
|
+
* shows (a browser restart picked up a new manifest while the worker may still
|
|
128
|
+
* be the old script) — never re-read from disk, which would label an old
|
|
129
|
+
* worker as the new code. 'unverified' differs from every deployed id, so the
|
|
130
|
+
* extension reloads itself into the deployed code once it is on disk.
|
|
131
|
+
*/
|
|
120
132
|
export async function loadedCode() {
|
|
121
133
|
const { [LOADED_KEY]: stored } = await chrome.storage.local.get(LOADED_KEY);
|
|
122
134
|
const record = stored;
|
|
123
135
|
if (record && typeof record.code === 'string' && record.version === chrome.runtime.getManifest().version)
|
|
124
136
|
return record.code;
|
|
125
|
-
return
|
|
137
|
+
return 'unverified';
|
|
126
138
|
}
|
|
127
139
|
/** Mark the extension busy for BUSY_LEASE_MS (an open overlay calls this through the worker every ~10 s). */
|
|
128
140
|
export async function extendBusyLease(now = Date.now()) {
|
package/package.json
CHANGED