fix: resume distributed task ID counter past existing IDs; extract plugin view types
The dashboard task-create route now uses the distributed task ID allocator (FN-3450). On projects whose tasks had been allocated through the legacy config.nextId counter, the allocator's `ensureStateRow` was seeding a fresh prefix at sequence 1, so new tasks restarted at FN-001 even when FN-3700 already existed. ensureStateRow now seeds past: - the legacy config.nextId counter (when configured taskPrefix matches), and - one past the highest numeric suffix on any existing tasks/archivedTasks row for the prefix. A regression test seeds FN-3700 in a fresh DB and asserts the next reservation returns FN-3701, not FN-001. Plugin dashboard view contracts are now exposed via a slim type-only module (`@fusion/dashboard/app/plugins/types`). External plugin tsc builds previously imported `pluginViewRegistry`, transitively pulling in dashboard runtime sources (React components, CSS, lucide-react). The dependency-graph plugin's import + path mapping is updated to use the new module. Schema housekeeping: drop unused scaffolding tables left over from an earlier migration via a new idempotent migration (v67). Fresh DBs see no change; existing DBs that ran the older migration get the orphan tables dropped on next init. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,10 +112,6 @@ describe("Database", () => {
|
||||
expect(tableNames).toContain("roadmap_features");
|
||||
// Verification cache (migration 61)
|
||||
expect(tableNames).toContain("verification_cache");
|
||||
expect(tableNames).toContain("project_auth_users");
|
||||
expect(tableNames).toContain("project_auth_memberships");
|
||||
expect(tableNames).toContain("project_auth_providers");
|
||||
expect(tableNames).toContain("project_auth_sessions");
|
||||
expect(tableNames).toContain("distributed_task_id_state");
|
||||
expect(tableNames).toContain("distributed_task_id_reservations");
|
||||
});
|
||||
@@ -165,17 +161,10 @@ describe("Database", () => {
|
||||
expect(indexNames).toContain("idxRoadmapFeaturesMilestoneOrder");
|
||||
// Verification cache index (migration 61)
|
||||
expect(indexNames).toContain("idxVerificationCacheRecordedAt");
|
||||
expect(indexNames).toContain("idxProjectAuthUsersEmail");
|
||||
expect(indexNames).toContain("idxProjectAuthMembershipsUserId");
|
||||
expect(indexNames).toContain("idxProjectAuthMembershipsRole");
|
||||
expect(indexNames).toContain("idxProjectAuthProvidersUserId");
|
||||
expect(indexNames).toContain("idxProjectAuthSessionsUserId");
|
||||
expect(indexNames).toContain("idxProjectAuthSessionsMembershipId");
|
||||
expect(indexNames).toContain("idxProjectAuthSessionsExpiry");
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
});
|
||||
it("seeds lastModified", () => {
|
||||
const ts = db.getLastModified();
|
||||
@@ -197,7 +186,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
});
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
// Update the config
|
||||
@@ -970,7 +959,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -995,11 +984,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -1034,7 +1023,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("priority");
|
||||
@@ -1075,7 +1064,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -1144,7 +1133,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -1247,7 +1236,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("attachments");
|
||||
@@ -1321,7 +1310,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "agentRatings" }]);
|
||||
@@ -1345,7 +1334,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "mission_events" }]);
|
||||
@@ -1449,7 +1438,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1918,7 +1907,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
@@ -2032,32 +2021,45 @@ describe("TaskStore — verification cache", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("migration v63 project auth tables", () => {
|
||||
it("migrates from v62 and creates project auth tables", () => {
|
||||
describe("migration v67 drops orphan project auth tables", () => {
|
||||
it("drops project_auth_* tables left over from the removed pluggable auth feature", () => {
|
||||
const temp = makeTmpDir();
|
||||
const fusion = join(temp, ".fusion");
|
||||
const localDb = new Database(fusion);
|
||||
localDb.init();
|
||||
localDb.prepare("UPDATE __meta SET value = '62' WHERE key = 'schemaVersion'").run();
|
||||
localDb.prepare("DROP TABLE IF EXISTS project_auth_sessions").run();
|
||||
localDb.prepare("DROP TABLE IF EXISTS project_auth_providers").run();
|
||||
localDb.prepare("DROP TABLE IF EXISTS project_auth_memberships").run();
|
||||
localDb.prepare("DROP TABLE IF EXISTS project_auth_users").run();
|
||||
// Simulate a user who ran the old migration 63 (schema version 63–66) and
|
||||
// therefore has the orphan project_auth_* tables sitting in their DB. We
|
||||
// recreate them by hand and roll the schemaVersion back so the new
|
||||
// migration runs on the next init.
|
||||
localDb.exec(`CREATE TABLE IF NOT EXISTS project_auth_users (id TEXT PRIMARY KEY)`);
|
||||
localDb.exec(`CREATE TABLE IF NOT EXISTS project_auth_memberships (id TEXT PRIMARY KEY, userId TEXT, FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE)`);
|
||||
localDb.exec(`CREATE TABLE IF NOT EXISTS project_auth_providers (id TEXT PRIMARY KEY, userId TEXT, FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE)`);
|
||||
localDb.exec(`CREATE TABLE IF NOT EXISTS project_auth_sessions (id TEXT PRIMARY KEY, userId TEXT, membershipId TEXT, FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE, FOREIGN KEY (membershipId) REFERENCES project_auth_memberships(id) ON DELETE CASCADE)`);
|
||||
localDb.prepare("UPDATE __meta SET value = '66' WHERE key = 'schemaVersion'").run();
|
||||
localDb.close();
|
||||
|
||||
const migrated = new Database(fusion);
|
||||
migrated.init();
|
||||
expect(migrated.getSchemaVersion()).toBe(66);
|
||||
expect(migrated.getSchemaVersion()).toBe(67);
|
||||
const tables = migrated
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%' ORDER BY name")
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
|
||||
.all() as Array<{ name: string }>;
|
||||
expect(tables.map((t) => t.name)).toEqual([
|
||||
"project_auth_memberships",
|
||||
"project_auth_providers",
|
||||
"project_auth_sessions",
|
||||
"project_auth_users",
|
||||
]);
|
||||
expect(tables).toEqual([]);
|
||||
migrated.close();
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("is a no-op on fresh DBs that never had the auth tables", () => {
|
||||
const temp = makeTmpDir();
|
||||
const fusion = join(temp, ".fusion");
|
||||
const fresh = new Database(fusion);
|
||||
fresh.init();
|
||||
expect(fresh.getSchemaVersion()).toBe(67);
|
||||
const tables = fresh
|
||||
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
|
||||
.all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([]);
|
||||
fresh.close();
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,30 @@ describe("distributed-task-id allocator", () => {
|
||||
expect(state.committedClusterTaskCount).toBe(0);
|
||||
});
|
||||
|
||||
it("seeds nextSequence past existing tasks for the configured prefix", async () => {
|
||||
// Regression: FN-3450 wired the dashboard task-create route to the
|
||||
// distributed allocator. On databases whose tasks were originally
|
||||
// allocated through TaskStore.allocateId() (config.nextId), the first
|
||||
// mesh-routed reservation used to restart at 1 and produce FN-001 even
|
||||
// when FN-3700 already existed. The allocator must now resume past any
|
||||
// existing task ID for the prefix.
|
||||
const db = new Database("/tmp/fusion-test", { inMemory: true });
|
||||
db.init();
|
||||
db.prepare("UPDATE config SET nextId = 3701, settings = ? WHERE id = 1").run(
|
||||
JSON.stringify({ taskPrefix: "FN" }),
|
||||
);
|
||||
db.prepare(
|
||||
"INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt) VALUES (?, '', 'todo', ?, ?)",
|
||||
).run("FN-3700", new Date().toISOString(), new Date().toISOString());
|
||||
const allocator = createDistributedTaskIdAllocator(db);
|
||||
|
||||
const reservation = await allocator.reserveDistributedTaskId({ prefix: "FN", nodeId: "node-a" });
|
||||
expect(reservation.taskId).toBe("FN-3701");
|
||||
|
||||
const state = await allocator.getDistributedTaskIdState({ prefix: "FN" });
|
||||
expect(state.nextSequence).toBe(3702);
|
||||
});
|
||||
|
||||
it("state reports committed count independently from nextSequence", async () => {
|
||||
const { allocator } = createAllocator();
|
||||
const first = await allocator.reserveDistributedTaskId({ prefix: "FN", nodeId: "node-a" });
|
||||
|
||||
@@ -886,7 +886,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
|
||||
const db1 = createDatabase(legacyDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(66);
|
||||
expect(db1.getSchemaVersion()).toBe(67);
|
||||
db1.close();
|
||||
|
||||
// Step 2: Manually downgrade to version 32 and drop insight tables
|
||||
@@ -921,7 +921,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
expect(tableNamesBefore).not.toContain("project_insight_runs");
|
||||
// Now run init — this triggers the v32→v33 migration
|
||||
db3.init();
|
||||
expect(db3.getSchemaVersion()).toBe(66);
|
||||
expect(db3.getSchemaVersion()).toBe(67);
|
||||
|
||||
// Step 4: Verify insight tables exist after migration
|
||||
const tablesAfter = db3.prepare(
|
||||
@@ -952,12 +952,12 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
try {
|
||||
const db1 = createDatabase(testDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(66);
|
||||
expect(db1.getSchemaVersion()).toBe(67);
|
||||
db1.close();
|
||||
|
||||
const db2 = createDatabase(testDir);
|
||||
expect(() => db2.init()).not.toThrow();
|
||||
expect(db2.getSchemaVersion()).toBe(66);
|
||||
expect(db2.getSchemaVersion()).toBe(67);
|
||||
db2.close();
|
||||
} finally {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
@@ -971,7 +971,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
// Step 1: Create a fresh DB and run migrations
|
||||
const db1 = createDatabase(compatDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(66);
|
||||
expect(db1.getSchemaVersion()).toBe(67);
|
||||
|
||||
// Step 2: Strip lifecycle and cancelledAt columns by recreating the
|
||||
// table without them. This simulates a DB that was created before the
|
||||
|
||||
@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
|
||||
|
||||
describe("Loop State & Validator Run Schema (v31)", () => {
|
||||
it("schema version is 40 after migration", () => {
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
});
|
||||
|
||||
it("mission_features table has loop state columns", () => {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { Database } from "../db.js";
|
||||
import { ProjectAuthStore } from "../project-auth-store.js";
|
||||
|
||||
describe("ProjectAuthStore", () => {
|
||||
let tmpDir: string;
|
||||
let db: Database;
|
||||
let store: ProjectAuthStore;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), "kb-project-auth-"));
|
||||
db = new Database(join(tmpDir, ".fusion"));
|
||||
db.init();
|
||||
store = new ProjectAuthStore(db);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
db.close();
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("persists users memberships providers and active sessions", () => {
|
||||
const user = store.createUser({ email: "owner@example.com", displayName: "Owner" });
|
||||
const membership = store.createMembership({ userId: user.id, role: "owner" });
|
||||
const provider = store.createProvider({ userId: user.id, provider: "github", providerUserId: "123", metadata: { login: "owner" } });
|
||||
const session = store.createSession({ userId: user.id, membershipId: membership.id, sessionToken: "tok_1", expiresAt: "2099-01-01T00:00:00.000Z" });
|
||||
|
||||
expect(store.getUser(user.id)?.email).toBe("owner@example.com");
|
||||
expect(store.listMembershipsByUser(user.id)[0]?.role).toBe("owner");
|
||||
expect(store.listProvidersByUser(user.id)[0]?.provider).toBe("github");
|
||||
expect(store.resolveActiveSessionByToken(session.sessionToken)?.id).toBe(session.id);
|
||||
expect(provider.metadata).toEqual({ login: "owner" });
|
||||
});
|
||||
|
||||
it("treats revoked and expired sessions as inactive", () => {
|
||||
const user = store.createUser({ email: "viewer@example.com" });
|
||||
const membership = store.createMembership({ userId: user.id, role: "viewer" });
|
||||
|
||||
const active = store.createSession({ userId: user.id, membershipId: membership.id, sessionToken: "tok_active", expiresAt: "2099-01-01T00:00:00.000Z" });
|
||||
const expired = store.createSession({ userId: user.id, membershipId: membership.id, sessionToken: "tok_expired", expiresAt: "2000-01-01T00:00:00.000Z" });
|
||||
|
||||
store.revokeSession(active.id);
|
||||
|
||||
expect(store.resolveActiveSessionByToken("tok_active")).toBeUndefined();
|
||||
expect(store.resolveActiveSessionByToken("tok_expired")).toBeUndefined();
|
||||
expect(expired.expiresAt).toBe("2000-01-01T00:00:00.000Z");
|
||||
});
|
||||
});
|
||||
@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
|
||||
|
||||
describe("schema version", () => {
|
||||
it("schema version is 40 after init", () => {
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11536,44 +11536,6 @@ describe("RunMutationContext", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("project auth store getter", () => {
|
||||
it("lazily returns a stable ProjectAuthStore instance", () => {
|
||||
const authStoreA = store.getProjectAuthStore();
|
||||
const authStoreB = store.getProjectAuthStore();
|
||||
|
||||
expect(authStoreA).toBe(authStoreB);
|
||||
const user = authStoreA.createUser({ email: "store-getter@example.com" });
|
||||
expect(authStoreB.getUser(user.id)?.email).toBe("store-getter@example.com");
|
||||
});
|
||||
|
||||
it("does not regress task CRUD behavior after auth store initialization", async () => {
|
||||
const authStore = store.getProjectAuthStore();
|
||||
const user = authStore.createUser({ email: "compat@example.com" });
|
||||
expect(user.id).toMatch(/^PAU-/);
|
||||
|
||||
const task = await store.createTask({ description: "auth-compat task", assigneeUserId: "user:dashboard" });
|
||||
expect(task.assigneeUserId).toBe("user:dashboard");
|
||||
|
||||
const updated = await store.updateTask(task.id, { title: "updated" });
|
||||
expect(updated?.title).toBe("updated");
|
||||
|
||||
const movedToTodo = await store.moveTask(task.id, "todo");
|
||||
expect(movedToTodo?.column).toBe("todo");
|
||||
});
|
||||
|
||||
it("persists project auth records across TaskStore reinitialization", async () => {
|
||||
store = new TaskStore(rootDir, globalDir);
|
||||
const authStore = store.getProjectAuthStore();
|
||||
const user = authStore.createUser({ email: "persist@example.com" });
|
||||
|
||||
store.close();
|
||||
store = new TaskStore(rootDir, globalDir);
|
||||
|
||||
const reloaded = store.getProjectAuthStore().getUser(user.id);
|
||||
expect(reloaded?.email).toBe("persist@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shared mesh snapshots", () => {
|
||||
it("exports and reapplies task/activity/audit snapshots deterministically", async () => {
|
||||
const task = await store.createTask({ description: "snapshot task" });
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
|
||||
|
||||
expect(tableNames.has("task_documents")).toBe(true);
|
||||
expect(tableNames.has("task_document_revisions")).toBe(true);
|
||||
expect(db.getSchemaVersion()).toBe(66);
|
||||
expect(db.getSchemaVersion()).toBe(67);
|
||||
|
||||
const index = db
|
||||
.prepare(
|
||||
|
||||
@@ -88,7 +88,7 @@ export function probeFts5(db: DatabaseSync): boolean {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 66;
|
||||
const SCHEMA_VERSION = 67;
|
||||
|
||||
function normalizeTaskComments(
|
||||
steeringComments: SteeringComment[] | undefined,
|
||||
@@ -825,59 +825,6 @@ CREATE TABLE IF NOT EXISTS todo_items (
|
||||
CREATE INDEX IF NOT EXISTS idxTodoListsProjectId ON todo_lists(projectId);
|
||||
CREATE INDEX IF NOT EXISTS idxTodoItemsListId ON todo_items(listId);
|
||||
CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder);
|
||||
|
||||
-- Project-scoped auth domain tables (FN-3515)
|
||||
CREATE TABLE IF NOT EXISTS project_auth_users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
displayName TEXT,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_auth_memberships (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_auth_providers (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
providerUserId TEXT NOT NULL,
|
||||
metadata TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE,
|
||||
UNIQUE(provider, providerUserId)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS project_auth_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
membershipId TEXT NOT NULL,
|
||||
sessionToken TEXT NOT NULL UNIQUE,
|
||||
expiresAt TEXT NOT NULL,
|
||||
revokedAt TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (membershipId) REFERENCES project_auth_memberships(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idxProjectAuthUsersEmail ON project_auth_users(email);
|
||||
CREATE INDEX IF NOT EXISTS idxProjectAuthMembershipsUserId ON project_auth_memberships(userId);
|
||||
CREATE INDEX IF NOT EXISTS idxProjectAuthMembershipsRole ON project_auth_memberships(role);
|
||||
CREATE INDEX IF NOT EXISTS idxProjectAuthProvidersUserId ON project_auth_providers(userId);
|
||||
CREATE INDEX IF NOT EXISTS idxProjectAuthSessionsUserId ON project_auth_sessions(userId);
|
||||
CREATE INDEX IF NOT EXISTS idxProjectAuthSessionsMembershipId ON project_auth_sessions(membershipId);
|
||||
CREATE INDEX IF NOT EXISTS idxProjectAuthSessionsExpiry ON project_auth_sessions(expiresAt);
|
||||
`;
|
||||
|
||||
// ── Database Class ───────────────────────────────────────────────────
|
||||
@@ -2674,66 +2621,6 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 63) {
|
||||
this.applyMigration(63, () => {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS project_auth_users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
displayName TEXT,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS project_auth_memberships (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS project_auth_providers (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
providerUserId TEXT NOT NULL,
|
||||
metadata TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE,
|
||||
UNIQUE(provider, providerUserId)
|
||||
)
|
||||
`);
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS project_auth_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
membershipId TEXT NOT NULL,
|
||||
sessionToken TEXT NOT NULL UNIQUE,
|
||||
expiresAt TEXT NOT NULL,
|
||||
revokedAt TEXT,
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES project_auth_users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (membershipId) REFERENCES project_auth_memberships(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxProjectAuthUsersEmail ON project_auth_users(email)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxProjectAuthMembershipsUserId ON project_auth_memberships(userId)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxProjectAuthMembershipsRole ON project_auth_memberships(role)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxProjectAuthProvidersUserId ON project_auth_providers(userId)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxProjectAuthSessionsUserId ON project_auth_sessions(userId)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxProjectAuthSessionsMembershipId ON project_auth_sessions(membershipId)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxProjectAuthSessionsExpiry ON project_auth_sessions(expiresAt)`);
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 64) {
|
||||
this.applyMigration(64, () => {
|
||||
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idxEvalTaskResultsRunTaskUnique ON eval_task_results(runId, taskId)`);
|
||||
@@ -2782,6 +2669,20 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 67) {
|
||||
// Drop the project_auth_* tables introduced by the old migration 63
|
||||
// (FN-3544). The pluggable project-auth feature was removed before any
|
||||
// production usage; these tables are orphaned on DBs that ran the old
|
||||
// migration. Drop sessions/providers/memberships before users so the
|
||||
// foreign-key cascade order is honored.
|
||||
this.applyMigration(67, () => {
|
||||
this.db.exec(`DROP TABLE IF EXISTS project_auth_sessions`);
|
||||
this.db.exec(`DROP TABLE IF EXISTS project_auth_providers`);
|
||||
this.db.exec(`DROP TABLE IF EXISTS project_auth_memberships`);
|
||||
this.db.exec(`DROP TABLE IF EXISTS project_auth_users`);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -82,11 +82,60 @@ export function createDistributedTaskIdAllocator(db: Database): DistributedTaskI
|
||||
};
|
||||
|
||||
const ensureStateRow = (prefix: string): void => {
|
||||
// Seed nextSequence past any pre-existing task ID for this prefix. Without
|
||||
// this, projects whose tasks were originally allocated through
|
||||
// TaskStore.allocateId() (config.nextId) would have mesh-routed task
|
||||
// creates restart at 1 and collide with historical FN-001 / FN-002 / …
|
||||
// IDs (regression introduced when the dashboard task-create route was
|
||||
// wired to reserveDistributedTaskId in FN-3450).
|
||||
//
|
||||
// We take the max of:
|
||||
// - 1 (historical default)
|
||||
// - the legacy config.nextId counter, when the configured taskPrefix
|
||||
// matches `prefix`
|
||||
// - one past the highest numeric suffix on any existing task for this
|
||||
// prefix (live tasks + archived), to handle DBs where config.nextId
|
||||
// ever drifted below the real high-water mark
|
||||
let seedSequence = 1;
|
||||
try {
|
||||
const configRow = db
|
||||
.prepare("SELECT nextId, settings FROM config WHERE id = 1")
|
||||
.get() as { nextId: number | null; settings: string | null } | undefined;
|
||||
if (configRow) {
|
||||
const settings = configRow.settings ? (JSON.parse(configRow.settings) as { taskPrefix?: string }) : null;
|
||||
const configuredPrefix = (settings?.taskPrefix ?? "KB").trim().toUpperCase();
|
||||
if (configuredPrefix === prefix && typeof configRow.nextId === "number" && configRow.nextId > seedSequence) {
|
||||
seedSequence = configRow.nextId;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best-effort: if the config row/column is missing (fresh test DB) we
|
||||
// fall back to the historical default of 1.
|
||||
}
|
||||
const idPattern = `${prefix}-%`;
|
||||
const probeTable = (table: string): void => {
|
||||
try {
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT MAX(CAST(substr(id, ${prefix.length + 2}) AS INTEGER)) AS maxSeq
|
||||
FROM ${table}
|
||||
WHERE id LIKE ? AND substr(id, ${prefix.length + 2}) GLOB '[0-9]*'`,
|
||||
)
|
||||
.get(idPattern) as { maxSeq: number | null } | undefined;
|
||||
if (row && typeof row.maxSeq === "number" && row.maxSeq + 1 > seedSequence) {
|
||||
seedSequence = row.maxSeq + 1;
|
||||
}
|
||||
} catch {
|
||||
// Table may not exist (tests, isolated DBs); ignore.
|
||||
}
|
||||
};
|
||||
probeTable("tasks");
|
||||
probeTable("archivedTasks");
|
||||
db.prepare(
|
||||
`INSERT OR IGNORE INTO distributed_task_id_state (
|
||||
prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt
|
||||
) VALUES (?, 1, 0, NULL, ?)`
|
||||
).run(prefix, new Date().toISOString());
|
||||
) VALUES (?, ?, 0, NULL, ?)`
|
||||
).run(prefix, seedSequence, new Date().toISOString());
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey, PROJECT_AUTH_ROLES, SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, ProjectAuthRole, ProjectAuthUser, ProjectAuthMembership, ProjectAuthProvider, ProjectAuthSession, ProjectAuthUserCreateInput, ProjectAuthMembershipCreateInput, ProjectAuthProviderCreateInput, ProjectAuthSessionCreateInput, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOutcome, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey, SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, MeshReplicatedTaskCreatePayload, MeshReplicatedTaskApplyResult, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOutcome, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||
export * from "./mesh-replication-protocol.js";
|
||||
export * from "./mesh-task-replication.js";
|
||||
@@ -54,7 +54,6 @@ export {
|
||||
DistributedTaskIdError,
|
||||
} from "./distributed-task-id.js";
|
||||
export type { DistributedTaskIdAllocator } from "./distributed-task-id.js";
|
||||
export { ProjectAuthStore } from "./project-auth-store.js";
|
||||
export { Database, createDatabase, toJson, toJsonNullable, fromJson } from "./db.js";
|
||||
export type { Statement } from "./db.js";
|
||||
export { ArchiveDatabase } from "./archive-db.js";
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import type { Database } from "./db.js";
|
||||
import { fromJson, toJsonNullable } from "./db.js";
|
||||
import type {
|
||||
ProjectAuthMembership,
|
||||
ProjectAuthMembershipCreateInput,
|
||||
ProjectAuthProvider,
|
||||
ProjectAuthProviderCreateInput,
|
||||
ProjectAuthRole,
|
||||
ProjectAuthSession,
|
||||
ProjectAuthSessionCreateInput,
|
||||
ProjectAuthUser,
|
||||
ProjectAuthUserCreateInput,
|
||||
} from "./types.js";
|
||||
import { PROJECT_AUTH_ROLES } from "./types.js";
|
||||
|
||||
interface ProjectAuthUserRow { id: string; email: string; displayName: string | null; active: number; createdAt: string; updatedAt: string; }
|
||||
interface ProjectAuthMembershipRow { id: string; userId: string; role: ProjectAuthRole; active: number; createdAt: string; updatedAt: string; }
|
||||
interface ProjectAuthProviderRow { id: string; userId: string; provider: string; providerUserId: string; metadata: string | null; createdAt: string; updatedAt: string; }
|
||||
interface ProjectAuthSessionRow { id: string; userId: string; membershipId: string; sessionToken: string; expiresAt: string; revokedAt: string | null; createdAt: string; updatedAt: string; }
|
||||
|
||||
export class ProjectAuthStore extends EventEmitter {
|
||||
constructor(private db: Database) { super(); }
|
||||
|
||||
private makeId(prefix: string): string { return `${prefix}-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`; }
|
||||
private now(): string { return new Date().toISOString(); }
|
||||
|
||||
private rowToUser(row: ProjectAuthUserRow): ProjectAuthUser { return { ...row, active: row.active === 1 }; }
|
||||
private rowToMembership(row: ProjectAuthMembershipRow): ProjectAuthMembership { return { ...row, active: row.active === 1 }; }
|
||||
private rowToProvider(row: ProjectAuthProviderRow): ProjectAuthProvider { return { ...row, metadata: fromJson<Record<string, unknown>>(row.metadata) }; }
|
||||
private rowToSession(row: ProjectAuthSessionRow): ProjectAuthSession { return { ...row }; }
|
||||
|
||||
createUser(input: ProjectAuthUserCreateInput): ProjectAuthUser {
|
||||
const now = this.now();
|
||||
const user: ProjectAuthUser = { id: this.makeId("PAU"), email: input.email, displayName: input.displayName ?? null, active: input.active ?? true, createdAt: now, updatedAt: now };
|
||||
this.db.prepare("INSERT INTO project_auth_users (id,email,displayName,active,createdAt,updatedAt) VALUES (?, ?, ?, ?, ?, ?)").run(user.id, user.email, user.displayName, user.active ? 1 : 0, now, now);
|
||||
this.db.bumpLastModified();
|
||||
return user;
|
||||
}
|
||||
|
||||
getUser(id: string): ProjectAuthUser | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM project_auth_users WHERE id = ?").get(id) as ProjectAuthUserRow | undefined;
|
||||
return row ? this.rowToUser(row) : undefined;
|
||||
}
|
||||
|
||||
listUsers(): ProjectAuthUser[] {
|
||||
return (this.db.prepare("SELECT * FROM project_auth_users ORDER BY createdAt ASC, id ASC").all() as ProjectAuthUserRow[]).map((row) => this.rowToUser(row));
|
||||
}
|
||||
|
||||
createMembership(input: ProjectAuthMembershipCreateInput): ProjectAuthMembership {
|
||||
if (!PROJECT_AUTH_ROLES.includes(input.role)) throw new Error(`Invalid role: ${input.role}`);
|
||||
const now = this.now();
|
||||
const membership: ProjectAuthMembership = { id: this.makeId("PAM"), userId: input.userId, role: input.role, active: input.active ?? true, createdAt: now, updatedAt: now };
|
||||
this.db.prepare("INSERT INTO project_auth_memberships (id,userId,role,active,createdAt,updatedAt) VALUES (?, ?, ?, ?, ?, ?)").run(membership.id, membership.userId, membership.role, membership.active ? 1 : 0, now, now);
|
||||
this.db.bumpLastModified();
|
||||
return membership;
|
||||
}
|
||||
|
||||
listMembershipsByUser(userId: string): ProjectAuthMembership[] {
|
||||
return (this.db.prepare("SELECT * FROM project_auth_memberships WHERE userId = ? ORDER BY createdAt ASC, id ASC").all(userId) as ProjectAuthMembershipRow[]).map((row) => this.rowToMembership(row));
|
||||
}
|
||||
|
||||
getMembership(id: string): ProjectAuthMembership | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM project_auth_memberships WHERE id = ?").get(id) as ProjectAuthMembershipRow | undefined;
|
||||
return row ? this.rowToMembership(row) : undefined;
|
||||
}
|
||||
|
||||
createProvider(input: ProjectAuthProviderCreateInput): ProjectAuthProvider {
|
||||
const now = this.now();
|
||||
const provider: ProjectAuthProvider = { id: this.makeId("PAP"), userId: input.userId, provider: input.provider, providerUserId: input.providerUserId, metadata: input.metadata, createdAt: now, updatedAt: now };
|
||||
this.db.prepare("INSERT INTO project_auth_providers (id,userId,provider,providerUserId,metadata,createdAt,updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?)").run(provider.id, provider.userId, provider.provider, provider.providerUserId, toJsonNullable(provider.metadata), now, now);
|
||||
this.db.bumpLastModified();
|
||||
return provider;
|
||||
}
|
||||
|
||||
listProvidersByUser(userId: string): ProjectAuthProvider[] {
|
||||
return (this.db.prepare("SELECT * FROM project_auth_providers WHERE userId = ? ORDER BY createdAt ASC, id ASC").all(userId) as ProjectAuthProviderRow[]).map((row) => this.rowToProvider(row));
|
||||
}
|
||||
|
||||
createSession(input: ProjectAuthSessionCreateInput): ProjectAuthSession {
|
||||
const now = this.now();
|
||||
const session: ProjectAuthSession = { id: this.makeId("PAS"), userId: input.userId, membershipId: input.membershipId, sessionToken: input.sessionToken, expiresAt: input.expiresAt, revokedAt: null, createdAt: now, updatedAt: now };
|
||||
this.db.prepare("INSERT INTO project_auth_sessions (id,userId,membershipId,sessionToken,expiresAt,revokedAt,createdAt,updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(session.id, session.userId, session.membershipId, session.sessionToken, session.expiresAt, session.revokedAt, now, now);
|
||||
this.db.bumpLastModified();
|
||||
return session;
|
||||
}
|
||||
|
||||
revokeSession(id: string): ProjectAuthSession | undefined {
|
||||
const now = this.now();
|
||||
this.db.prepare("UPDATE project_auth_sessions SET revokedAt = ?, updatedAt = ? WHERE id = ?").run(now, now, id);
|
||||
this.db.bumpLastModified();
|
||||
return this.getSession(id);
|
||||
}
|
||||
|
||||
getSession(id: string): ProjectAuthSession | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM project_auth_sessions WHERE id = ?").get(id) as ProjectAuthSessionRow | undefined;
|
||||
return row ? this.rowToSession(row) : undefined;
|
||||
}
|
||||
|
||||
resolveActiveSessionByToken(sessionToken: string, nowIso: string = this.now()): ProjectAuthSession | undefined {
|
||||
const row = this.db.prepare("SELECT * FROM project_auth_sessions WHERE sessionToken = ? AND revokedAt IS NULL AND expiresAt > ?").get(sessionToken, nowIso) as ProjectAuthSessionRow | undefined;
|
||||
return row ? this.rowToSession(row) : undefined;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import { InsightStore } from "./insight-store.js";
|
||||
import { ResearchStore } from "./research-store.js";
|
||||
import { TodoStore } from "./todo-store.js";
|
||||
import { EvalStore } from "./eval-store.js";
|
||||
import { ProjectAuthStore } from "./project-auth-store.js";
|
||||
import { BackwardCompat, ProjectRequiredError } from "./migration.js";
|
||||
import { CentralCore } from "./central-core.js";
|
||||
import { getTaskMergeBlocker } from "./task-merge.js";
|
||||
@@ -522,8 +521,6 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
private todoStore: TodoStore | null = null;
|
||||
/** Cached EvalStore instance */
|
||||
private evalStore: EvalStore | null = null;
|
||||
/** Cached ProjectAuthStore instance */
|
||||
private projectAuthStore: ProjectAuthStore | null = null;
|
||||
/** Cached distributed task-id allocator instance. */
|
||||
private distributedTaskIdAllocator: DistributedTaskIdAllocator | null = null;
|
||||
|
||||
@@ -6842,17 +6839,6 @@ ${notificationsSection}`;
|
||||
return this.evalStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ProjectAuthStore instance for project-scoped auth domain operations.
|
||||
* Lazily initializes the ProjectAuthStore on first access.
|
||||
*/
|
||||
getProjectAuthStore(): ProjectAuthStore {
|
||||
if (!this.projectAuthStore) {
|
||||
this.projectAuthStore = new ProjectAuthStore(this.db);
|
||||
}
|
||||
return this.projectAuthStore;
|
||||
}
|
||||
|
||||
// ── Verification Cache ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -1215,76 +1215,6 @@ export interface TodoListWithItems extends TodoList {
|
||||
items: TodoItem[];
|
||||
}
|
||||
|
||||
// ── Project Auth Types ───────────────────────────────────────────────────
|
||||
|
||||
export const PROJECT_AUTH_ROLES = ["owner", "admin", "editor", "viewer"] as const;
|
||||
export type ProjectAuthRole = (typeof PROJECT_AUTH_ROLES)[number];
|
||||
|
||||
export interface ProjectAuthUser {
|
||||
id: string;
|
||||
email: string;
|
||||
displayName: string | null;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectAuthMembership {
|
||||
id: string;
|
||||
userId: string;
|
||||
role: ProjectAuthRole;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectAuthProvider {
|
||||
id: string;
|
||||
userId: string;
|
||||
provider: string;
|
||||
providerUserId: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectAuthSession {
|
||||
id: string;
|
||||
userId: string;
|
||||
membershipId: string;
|
||||
sessionToken: string;
|
||||
expiresAt: string;
|
||||
revokedAt: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ProjectAuthUserCreateInput {
|
||||
email: string;
|
||||
displayName?: string | null;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectAuthMembershipCreateInput {
|
||||
userId: string;
|
||||
role: ProjectAuthRole;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface ProjectAuthProviderCreateInput {
|
||||
userId: string;
|
||||
provider: string;
|
||||
providerUserId: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ProjectAuthSessionCreateInput {
|
||||
userId: string;
|
||||
membershipId: string;
|
||||
sessionToken: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
// ── Settings Scope Types ────────────────────────────────────────────────
|
||||
//
|
||||
// Settings are split into two scopes:
|
||||
|
||||
Reference in New Issue
Block a user