feat(federation): instance epoch schema + minting
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE `federation_reset_events` (
|
||||
`origin` text PRIMARY KEY NOT NULL,
|
||||
`dead_epoch` text NOT NULL,
|
||||
`new_epoch` text,
|
||||
`detected_at` integer NOT NULL,
|
||||
`resolved_at` integer,
|
||||
`stub_count` integer DEFAULT 0 NOT NULL,
|
||||
`orphaned_account_count` integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `federation_peers` ADD `peer_instance_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `federation_peers` ADD `observed_peer_instance_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `federation_peers` ADD `needs_attention_reason` text;--> statement-breakpoint
|
||||
ALTER TABLE `instance_settings` ADD `instance_id` text;--> statement-breakpoint
|
||||
ALTER TABLE `users` ADD `federation_heal_pending` integer DEFAULT 0;--> statement-breakpoint
|
||||
ALTER TABLE `users` ADD `federation_home_orphaned` integer DEFAULT 0;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,13 @@
|
||||
"when": 1782832912087,
|
||||
"tag": "0007_nervous_orphan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1782932926154,
|
||||
"tag": "0008_cute_sebastian_shaw",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
import { ensureDefaults } from './migrate.js';
|
||||
|
||||
function freshDb(): Database.Database {
|
||||
const db = new Database(':memory:');
|
||||
db.exec(`CREATE TABLE instance_settings (id integer PRIMARY KEY, worker_id integer, instance_id text, max_bitrate_kbps integer, min_bitrate_kbps integer, bitrate_step_kbps integer, allowed_resolutions text, allowed_framerates text, max_resolution integer, max_framerate integer, updated_at integer);
|
||||
CREATE TABLE users (id text PRIMARY KEY, is_admin integer DEFAULT 0, created_at integer);`);
|
||||
return db;
|
||||
}
|
||||
|
||||
describe('ensureDefaults instance epoch', () => {
|
||||
it('mints an instance_id when null and is idempotent', () => {
|
||||
const db = freshDb();
|
||||
ensureDefaults(db);
|
||||
const first = (db.prepare('SELECT instance_id FROM instance_settings WHERE id = 1').get() as { instance_id: string }).instance_id;
|
||||
expect(first).toMatch(/^[0-9a-f-]{36}$/);
|
||||
ensureDefaults(db);
|
||||
const second = (db.prepare('SELECT instance_id FROM instance_settings WHERE id = 1').get() as { instance_id: string }).instance_id;
|
||||
expect(second).toBe(first); // stable across boots
|
||||
});
|
||||
|
||||
it('mints a different id for a separate fresh DB', () => {
|
||||
const a = freshDb(); ensureDefaults(a);
|
||||
const b = freshDb(); ensureDefaults(b);
|
||||
const idA = (a.prepare('SELECT instance_id FROM instance_settings WHERE id = 1').get() as { instance_id: string }).instance_id;
|
||||
const idB = (b.prepare('SELECT instance_id FROM instance_settings WHERE id = 1').get() as { instance_id: string }).instance_id;
|
||||
expect(idA).not.toBe(idB);
|
||||
});
|
||||
});
|
||||
@@ -27,6 +27,18 @@ export function ensureDefaults(db: Database.Database): void {
|
||||
console.log(`[defaults] Generated Snowflake worker ID: ${workerId}`);
|
||||
}
|
||||
|
||||
// 2b. Ensure a persistent instance epoch (incarnation UUID) exists. A fresh
|
||||
// DB mints a new one — this is the discriminator for detecting resets.
|
||||
// The id=1 row is guaranteed by step 1's INSERT OR IGNORE above.
|
||||
const epochRow = db.prepare('SELECT instance_id FROM instance_settings WHERE id = 1').get() as
|
||||
{ instance_id: string | null } | undefined;
|
||||
if (!epochRow || epochRow.instance_id === null) {
|
||||
const instanceId = crypto.randomUUID();
|
||||
const res = db.prepare('UPDATE instance_settings SET instance_id = ? WHERE id = 1').run(instanceId);
|
||||
if (res.changes !== 1) throw new Error('ensureDefaults: instance_settings id=1 row missing — cannot mint epoch');
|
||||
console.log('[defaults] Generated instance epoch');
|
||||
}
|
||||
|
||||
// 3. Ensure at least one admin exists (promote earliest registered user)
|
||||
const anyAdmin = db.prepare('SELECT id FROM users WHERE is_admin = 1 LIMIT 1').get();
|
||||
if (!anyAdmin) {
|
||||
|
||||
@@ -23,6 +23,8 @@ export const users = sqliteTable('users', {
|
||||
passwordChangedAt: integer('password_changed_at'),
|
||||
showActivity: integer('show_activity').notNull().default(1),
|
||||
federationRegistryUpdatedAt: integer('federation_registry_updated_at').default(0),
|
||||
federationHealPending: integer('federation_heal_pending').default(0),
|
||||
federationHomeOrphaned: integer('federation_home_orphaned').default(0),
|
||||
createdAt: integer('created_at').notNull(),
|
||||
});
|
||||
|
||||
@@ -308,6 +310,7 @@ export const instanceSettings = sqliteTable('instance_settings', {
|
||||
id: integer('id').primaryKey().default(1),
|
||||
instanceName: text('instance_name').default('Backspace'),
|
||||
workerId: integer('worker_id'),
|
||||
instanceId: text('instance_id'),
|
||||
discoveryEnabled: integer('discovery_enabled').notNull().default(1),
|
||||
maxBitrateKbps: integer('max_bitrate_kbps').notNull().default(20000),
|
||||
minBitrateKbps: integer('min_bitrate_kbps').notNull().default(500),
|
||||
@@ -385,6 +388,22 @@ export const federationPeers = sqliteTable('federation_peers', {
|
||||
autoRotateIntervalDays: integer('auto_rotate_interval_days').notNull().default(90),
|
||||
createdAt: integer('created_at').notNull(),
|
||||
approvalToken: text('approval_token'),
|
||||
peerInstanceId: text('peer_instance_id'),
|
||||
observedPeerInstanceId: text('observed_peer_instance_id'),
|
||||
needsAttentionReason: text('needs_attention_reason'),
|
||||
});
|
||||
|
||||
// Records a detected federated-peer reset (same origin, new instance epoch).
|
||||
// One row per origin; upserted when a live epoch change is observed, resolved
|
||||
// once stale replicated identities are healed.
|
||||
export const federationResetEvents = sqliteTable('federation_reset_events', {
|
||||
origin: text('origin').primaryKey(),
|
||||
deadEpoch: text('dead_epoch').notNull(),
|
||||
newEpoch: text('new_epoch'),
|
||||
detectedAt: integer('detected_at').notNull(),
|
||||
resolvedAt: integer('resolved_at'),
|
||||
stubCount: integer('stub_count').notNull().default(0),
|
||||
orphanedAccountCount: integer('orphaned_account_count').notNull().default(0),
|
||||
});
|
||||
|
||||
// SQL-level CHECK constraint enforces (direction='inbound' → hmac_secret NOT NULL).
|
||||
|
||||
Reference in New Issue
Block a user