diff --git a/packages/drfed/package.json b/packages/drfed/package.json
index 3be46b9..fa77024 100644
--- a/packages/drfed/package.json
+++ b/packages/drfed/package.json
@@ -53,6 +53,10 @@
"./serving": {
"types": "./dist/serving.d.mts",
"default": "./dist/serving.mjs"
+ },
+ "./query-logger": {
+ "types": "./dist/query-logger.d.mts",
+ "default": "./dist/query-logger.mjs"
}
},
"files": [
@@ -67,7 +71,8 @@
"entry": [
"src/index.ts",
"src/valueparser.ts",
- "src/serving.ts"
+ "src/serving.ts",
+ "src/query-logger.ts"
],
"dts": {
"sourcemap": true,
diff --git a/packages/drfed/src/index.ts b/packages/drfed/src/index.ts
index 75ca17a..df54f67 100644
--- a/packages/drfed/src/index.ts
+++ b/packages/drfed/src/index.ts
@@ -18,12 +18,13 @@ import { writeFile } from "node:fs/promises";
import process from "node:process";
import createFederation, { createInboundRecorder } from "@drfed/federation";
+import { KeyGenerationQueue } from "@drfed/federation/task-queue";
import { createYogaServer } from "@drfed/graphql";
import { schema } from "@drfed/graphql/schema";
import { migrate } from "@drfed/models";
import { PgliteKvStore } from "@fedify/pglite";
import { PostgresKvStore } from "@fedify/postgres";
-import { configure, getConsoleSink } from "@logtape/logtape";
+import { configure, getConsoleSink, getLogger } from "@logtape/logtape";
import { createLoggingConfig } from "@optique/logtape";
import { run } from "@optique/run";
import { SmtpTransport } from "@upyo/smtp";
@@ -50,8 +51,21 @@ async function runServer(options: ServerOptions) {
: new PostgresKvStore(credentials.client);
const federation = await createFederation(options.drizzle.db, {
kv,
+ queue: { task: new KeyGenerationQueue() },
+ taskQueueResolution: "strict",
+ manuallyStartQueue: true,
allowPrivateAddress: true,
});
+ const workerAbort = new AbortController();
+ // oxlint-disable promise/prefer-await-to-then
+ const worker = federation
+ .startQueue(undefined, { queue: "task", signal: workerAbort.signal })
+ .catch(() => {
+ getLogger(["drfed", "server"]).error(
+ "Actor key worker stopped unexpectedly.",
+ );
+ });
+ // oxlint-enable promise/prefer-await-to-then
const { emailFrom, mailer, rootOrigin, loginOrigins } = options;
const yogaServer = createYogaServer(options.drizzle.db, federation, {
@@ -73,23 +87,55 @@ async function runServer(options: ServerOptions) {
}),
hostname: options.address.host,
manual: true,
+ gracefulShutdown: false,
port: options.address.port,
});
+ let closing = false;
function shutdown() {
- if (mailer instanceof SmtpTransport) {
- mailer.closeAllConnections();
+ if (closing) {
+ process.exit(1);
}
- // oxlint-disable-next-line promise/catch-or-return promise/prefer-await-to-then
- server.close().then(async () => {
- await ("driver" in credentials
- ? credentials.client.close()
- : credentials.client.end());
- process.exit(0);
- });
+ closing = true;
+ const deadline = setTimeout(() => process.exit(1), 10_000);
+ const requests = server.close();
+ const forceClose = setTimeout(() => {
+ // A stalled upload must not prevent database cleanup on shutdown.
+ // oxlint-disable-next-line promise/prefer-await-to-then
+ void server.close(true).catch(() => process.exit(1));
+ }, 5000);
+ workerAbort.abort();
+ // The task worker awaits its active handler before resolving.
+ // oxlint-disable promise/prefer-await-to-then
+ Promise.all([requests, worker])
+ .then(async () => {
+ clearTimeout(forceClose);
+ if (mailer instanceof SmtpTransport) mailer.closeAllConnections();
+ await ("driver" in credentials
+ ? credentials.client.close()
+ : credentials.client.end());
+ clearTimeout(deadline);
+ process.exit(0);
+ })
+ .catch(() => {
+ process.exit(1);
+ });
+ }
+ // oxlint-enable promise/prefer-await-to-then
+ process.on("SIGINT", shutdown);
+ process.on("SIGTERM", shutdown);
+ try {
+ await server.serve();
+ } catch (error) {
+ process.off("SIGINT", shutdown);
+ process.off("SIGTERM", shutdown);
+ workerAbort.abort();
+ await Promise.all([server.close(), worker]);
+ if (mailer instanceof SmtpTransport) mailer.closeAllConnections();
+ await ("driver" in credentials
+ ? credentials.client.close()
+ : credentials.client.end());
+ throw new Error("Could not start the server.", { cause: error });
}
- process.once("SIGINT", shutdown);
- process.once("SIGTERM", shutdown);
- await server.serve();
}
async function runSchemaGenerator(
diff --git a/packages/drfed/src/lifecycle.test.ts b/packages/drfed/src/lifecycle.test.ts
new file mode 100644
index 0000000..10df44c
--- /dev/null
+++ b/packages/drfed/src/lifecycle.test.ts
@@ -0,0 +1,151 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import assert from "node:assert/strict";
+import { spawn } from "node:child_process";
+import { once } from "node:events";
+import { mkdtemp, rm } from "node:fs/promises";
+import { type Server, connect, createServer } from "node:net";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import process from "node:process";
+import { it } from "node:test";
+import { setTimeout as delay } from "node:timers/promises";
+import { fileURLToPath } from "node:url";
+
+const binary = fileURLToPath(
+ new URL("../bin/drfed-server.mjs", import.meta.resolve("@drfed/drfed")),
+);
+async function reservePort() {
+ const server = createServer();
+ server.listen(0, "127.0.0.1");
+ await once(server, "listening");
+ const address = server.address();
+ assert.ok(address != null && typeof address !== "string");
+ return { server, port: address.port };
+}
+async function closeServer(server: Server) {
+ const closed = once(server, "close");
+ server.close();
+ await closed;
+}
+function startServer(port: number, dataPath: string) {
+ const child = spawn(
+ process.execPath,
+ [
+ binary,
+ "--root-origin=http://drfed.test",
+ "--login-origin=http://drfed.test",
+ `--listen=127.0.0.1:${port}`,
+ `--pglite-data-path=${dataPath}`,
+ "--log-level=error",
+ ],
+ { stdio: ["ignore", "pipe", "pipe"] },
+ );
+ let stderr = "";
+ child.stderr.on("data", (chunk) => {
+ stderr += chunk.toString();
+ });
+ child.stdout.resume();
+ const exited = once(child, "close");
+ return { child, exited, stderr: () => stderr };
+}
+
+async function waitForExit(run: ReturnType) {
+ const stopTimeout = new AbortController();
+ const timeout = async () => {
+ await delay(90_000, undefined, { signal: stopTimeout.signal });
+ assert.fail(`CLI did not exit within 90 seconds: ${run.stderr()}`);
+ };
+ try {
+ return await Promise.race([run.exited, timeout()]);
+ } finally {
+ stopTimeout.abort();
+ }
+}
+
+it("preserves a listen error when server startup fails", async () => {
+ const { server, port } = await reservePort();
+ const dataPath = await mkdtemp(join(tmpdir(), "drfed-startup-"));
+ const run = startServer(port, dataPath);
+ try {
+ const [code] = await waitForExit(run);
+ assert.equal(code, 1);
+ assert.match(run.stderr(), /EADDRINUSE/u);
+ } finally {
+ run.child.kill("SIGKILL");
+ await closeServer(server);
+ await rm(dataPath, { recursive: true, force: true });
+ }
+});
+
+it(
+ "force-closes a stalled upload and exits normally on SIGTERM",
+ {
+ // Windows child.kill("SIGTERM") forcibly terminates without running handlers.
+ skip: process.platform === "win32",
+ },
+ async () => {
+ const { server, port } = await reservePort();
+ await closeServer(server);
+ const dataPath = await mkdtemp(join(tmpdir(), "drfed-shutdown-"));
+ const run = startServer(port, dataPath);
+ try {
+ let ready = false;
+ const readinessDeadline = performance.now() + 90_000;
+ while (performance.now() < readinessDeadline) {
+ assert.equal(run.child.exitCode, null, run.stderr());
+ try {
+ // oxlint-disable-next-line no-await-in-loop
+ const response = await fetch(`http://127.0.0.1:${port}/graphql`, {
+ method: "POST",
+ signal: AbortSignal.timeout(1000),
+ headers: { "content-type": "application/json" },
+ body: '{"query":"{ __typename }"}',
+ });
+ if (response.ok) {
+ ready = true;
+ break;
+ }
+ } catch {
+ // Migrations and listening have not finished yet.
+ }
+ // oxlint-disable-next-line no-await-in-loop
+ await delay(100);
+ }
+ assert.ok(ready, run.stderr());
+ const stalled = connect({ host: "127.0.0.1", port });
+ // Force-closing a stalled upload may reset its socket.
+ stalled.on("error", () => undefined);
+ try {
+ await once(stalled, "connect");
+ stalled.write(
+ "POST /graphql HTTP/1.1\r\nHost: drfed.test\r\nContent-Type: application/json\r\nContent-Length: 100\r\n\r\n{",
+ );
+ await delay(100);
+ run.child.kill("SIGTERM");
+ const [code, signal] = await waitForExit(run);
+ assert.equal(signal, null);
+ assert.equal(code, 0, run.stderr());
+ } finally {
+ stalled.destroy();
+ }
+ } finally {
+ run.child.kill("SIGKILL");
+ await rm(dataPath, { recursive: true, force: true });
+ }
+ },
+);
diff --git a/packages/drfed/src/parser.ts b/packages/drfed/src/parser.ts
index 1b0ad73..eb4def8 100644
--- a/packages/drfed/src/parser.ts
+++ b/packages/drfed/src/parser.ts
@@ -16,7 +16,6 @@
import { relations, schema } from "@drfed/models";
import { PGlite } from "@electric-sql/pglite";
-import { getLogger } from "@logtape/drizzle-orm";
import { merge, object, or } from "@optique/core/constructs";
import { message, optionNames } from "@optique/core/message";
import { map, multiple, optional, withDefault } from "@optique/core/modifiers";
@@ -31,6 +30,7 @@ import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
import postgres from "postgres";
+import { privateKeySafeLogger } from "./query-logger.ts";
import { rootOrigin } from "./valueparser.ts";
const pgliteParser = map(
@@ -54,7 +54,7 @@ const pgliteParser = map(
client,
relations,
schema,
- logger: getLogger(),
+ logger: privateKeySafeLogger(),
}),
};
},
@@ -80,7 +80,7 @@ const postgresParser = map(
client,
relations,
schema,
- logger: getLogger(),
+ logger: privateKeySafeLogger(),
}),
};
},
diff --git a/packages/drfed/src/query-logger.test.ts b/packages/drfed/src/query-logger.test.ts
new file mode 100644
index 0000000..ed1cbfd
--- /dev/null
+++ b/packages/drfed/src/query-logger.test.ts
@@ -0,0 +1,36 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import assert from "node:assert/strict";
+import { it } from "node:test";
+
+import { privateKeySafeLogger } from "@drfed/drfed/query-logger";
+
+it("never forwards key parameters to the ordinary SQL logger", () => {
+ const calls: unknown[] = [];
+ const logger = privateKeySafeLogger({
+ logQuery(query, params) {
+ calls.push([query, params]);
+ },
+ });
+ logger.logQuery('insert into "local_actor_keys" values ($1)', [
+ "PRIVATE_SECRET",
+ ]);
+ logger.logQuery('select * from "local_actor_keys"', []);
+ assert.deepEqual(calls, []);
+ logger.logQuery("select $1", [42]);
+ assert.deepEqual(calls, [["select $1", [42]]]);
+});
diff --git a/packages/drfed/src/query-logger.ts b/packages/drfed/src/query-logger.ts
new file mode 100644
index 0000000..b8b5f60
--- /dev/null
+++ b/packages/drfed/src/query-logger.ts
@@ -0,0 +1,34 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { getLogger as getQueryLogger } from "@logtape/drizzle-orm";
+import { getLogger } from "@logtape/logtape";
+import type { Logger } from "drizzle-orm/logger";
+/** Suppress parameters for SQL that reads or writes private signing material.
+ * @returns A logger that never logs private-key parameters.
+ */
+export function privateKeySafeLogger(
+ delegate: Logger = getQueryLogger(),
+): Logger {
+ const logger = getLogger(["drfed", "database"]);
+ return {
+ logQuery(query, params) {
+ if (query.includes("local_actor_keys")) {
+ logger.debug("Query: {query}", { query });
+ } else delegate.logQuery(query, params);
+ },
+ };
+}
diff --git a/packages/drfed/src/serving.test.ts b/packages/drfed/src/serving.test.ts
index 2e1933e..da48e5e 100644
--- a/packages/drfed/src/serving.test.ts
+++ b/packages/drfed/src/serving.test.ts
@@ -15,6 +15,7 @@
// along with this program. If not, see .
import assert from "node:assert/strict";
+import { it } from "node:test";
import {
createFetchHandler,
@@ -26,7 +27,8 @@ import { migrate, relations, schema } from "@drfed/models";
import { uuidV7 as uuid } from "@drfed/models/uuid";
import { PGlite } from "@electric-sql/pglite";
import { MemoryKvStore } from "@fedify/fedify";
-import { describe, it } from "@logtape/testing-node/autoload";
+import { type LogRecord, withConfig } from "@logtape/logtape";
+import { describe } from "@logtape/testing-node/autoload";
import { drizzle } from "drizzle-orm/pglite";
const rootOrigin = new URL("https://drfed.net");
@@ -294,3 +296,60 @@ it("records inbox requests only on the instance surface", async () => {
await client.close();
}
});
+
+it("logs federation failures without secrets and continues serving", async () => {
+ const records: LogRecord[] = [];
+ await withConfig(
+ {
+ sinks: { capture: (record) => records.push(record) },
+ loggers: [{ category: ["drfed", "serving"], sinks: ["capture"] }],
+ },
+ async () => {
+ let broken = true;
+ const fetch = createFetchHandler({
+ rootOrigin,
+ federation: {
+ fetch() {
+ if (broken) {
+ const error = new Error("PRIVATE_SECRET", {
+ cause: { privateKey: "PRIVATE_CAUSE" },
+ });
+ error.name = "PRIVATE_NAME";
+ throw error;
+ }
+ return Promise.resolve(new Response("ok"));
+ },
+ },
+ serveControlSurface: () => new Response("graphql"),
+ });
+ // Check the two request paths sequentially.
+ // oxlint-disable no-await-in-loop
+ for (const method of ["GET", "POST"]) {
+ const response = await fetch(
+ new Request(
+ "https://demo.drfed.net/users/x/inbox?token=PRIVATE_QUERY",
+ { method },
+ ),
+ );
+ assert.equal(response.status, 500);
+ assert.equal(await response.text(), "Internal server error");
+ }
+ broken = false;
+ assert.equal(
+ (await fetch(new Request("https://demo.drfed.net/users/x"))).status,
+ 200,
+ );
+ assert.equal(records.length, 2);
+ for (const [index, record] of records.entries()) {
+ assert.equal(record.level, "error");
+ assert.deepEqual(record.category, ["drfed", "serving"]);
+ assert.deepEqual(record.properties, {
+ method: ["GET", "POST"][index],
+ path: "/users/x/inbox",
+ errorType: "Error",
+ });
+ }
+ assert.ok(!JSON.stringify(records).includes("PRIVATE_"));
+ },
+ );
+});
diff --git a/packages/drfed/src/serving.ts b/packages/drfed/src/serving.ts
index 3c76bde..65fd26a 100644
--- a/packages/drfed/src/serving.ts
+++ b/packages/drfed/src/serving.ts
@@ -95,11 +95,24 @@ export function createFetchHandler(
}
switch (classifyHost(url, rootOrigin)) {
case "instance":
- return await federation.fetch(request, {
- onNotFound: notFound,
- onNotAcceptable: notFound,
- contextData: undefined,
- });
+ try {
+ return await federation.fetch(request, {
+ onNotFound: notFound,
+ onNotAcceptable: notFound,
+ contextData: undefined,
+ });
+ } catch (error) {
+ // Error messages, names and causes can contain private key material.
+ getLogger(["drfed", "serving"]).error(
+ "Federation request {method} {path} failed ({errorType}).",
+ {
+ method: request.method,
+ path: url.pathname,
+ errorType: error instanceof Error ? "Error" : typeof error,
+ },
+ );
+ return new Response("Internal server error", { status: 500 });
+ }
case "misdirected":
// Below the root domain but deeper than the single label an instance
// occupies, so nothing here will ever answer. Saying so is more use
diff --git a/packages/federation/package.json b/packages/federation/package.json
index 9ab59d7..086492f 100644
--- a/packages/federation/package.json
+++ b/packages/federation/package.json
@@ -57,6 +57,14 @@
"./origin": {
"types": "./dist/origin.d.mts",
"default": "./dist/origin.mjs"
+ },
+ "./actor-key": {
+ "types": "./dist/actor-key.d.mts",
+ "default": "./dist/actor-key.mjs"
+ },
+ "./task-queue": {
+ "types": "./dist/task-queue.d.mts",
+ "default": "./dist/task-queue.mjs"
}
},
"files": [
@@ -68,7 +76,9 @@
"src/index.ts",
"src/activity-delivery.ts",
"src/object.ts",
- "src/origin.ts"
+ "src/origin.ts",
+ "src/actor-key.ts",
+ "src/task-queue.ts"
],
"dts": {
"sourcemap": true,
diff --git a/packages/federation/src/actor-key-task.ts b/packages/federation/src/actor-key-task.ts
new file mode 100644
index 0000000..1b1e9ba
--- /dev/null
+++ b/packages/federation/src/actor-key-task.ts
@@ -0,0 +1,90 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import type { Database } from "@drfed/models";
+import { validateUuid } from "@drfed/models/uuid";
+import type {
+ Context,
+ Federation,
+ FederationBuilder,
+ TaskDefinition,
+} from "@fedify/fedify";
+import { getLogger } from "@logtape/logtape";
+
+import { ensureActorKeyPairs } from "./actor-key.ts";
+
+interface Payload {
+ identifier: string;
+}
+type Handle = TaskDefinition;
+const builders = new WeakMap, Handle>();
+const handles = new WeakMap, Handle>();
+const logger = getLogger(["drfed", "federation", "actor-key"]);
+const taskSchema: Handle["schema"] = {
+ "~standard": {
+ version: 1,
+ vendor: "drfed",
+ validate(value) {
+ if (
+ typeof value === "object" &&
+ value != null &&
+ "identifier" in value &&
+ typeof value.identifier === "string" &&
+ validateUuid(value.identifier)
+ ) {
+ return { value: { identifier: value.identifier } };
+ }
+ return { issues: [{ message: "Expected an actor UUID." }] };
+ },
+ },
+};
+export function registerActorKeyTask(
+ builder: FederationBuilder,
+ db: Database,
+): void {
+ const handle = builder.defineTask("drfed.ensureActorKeyPairs", {
+ schema: taskSchema,
+ retryPolicy: () => null,
+ async handler(ctx, data) {
+ await ensureActorKeyPairs(db, ctx, data.identifier);
+ },
+ });
+ builders.set(builder, handle);
+}
+export function attachActorKeyTask(
+ builder: FederationBuilder,
+ federation: Federation,
+ enabled: boolean,
+): void {
+ const handle = builders.get(builder);
+ if (enabled && handle != null) handles.set(federation, handle);
+}
+/** Schedule expendable prewarming after actor creation has committed. */
+export async function enqueueActorKeyGeneration(
+ ctx: Context,
+ identifiers: readonly string[],
+): Promise {
+ const handle = handles.get(ctx.federation);
+ if (handle == null || identifiers.length === 0) return;
+ try {
+ await ctx.enqueueTaskMany(
+ handle,
+ identifiers.map((identifier) => ({ identifier })),
+ );
+ } catch {
+ logger.warn("Could not schedule actor key prewarming.");
+ }
+}
diff --git a/packages/federation/src/actor-key.ts b/packages/federation/src/actor-key.ts
new file mode 100644
index 0000000..c0419af
--- /dev/null
+++ b/packages/federation/src/actor-key.ts
@@ -0,0 +1,182 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+// Generation is intentionally outside transactions; call this before opening
+// a transaction that will deliver an activity.
+// oxlint-disable no-await-in-loop
+import { type Database, schema } from "@drfed/models";
+import type { LocalActorKey } from "@drfed/models/schema";
+import { type Uuid, validateUuid } from "@drfed/models/uuid";
+import {
+ type Context,
+ exportJwk,
+ generateCryptoKeyPair,
+ importJwk,
+} from "@fedify/fedify";
+import { getLogger } from "@logtape/logtape";
+import { and, eq } from "drizzle-orm";
+
+import { canonicalizeAuthority } from "./origin.ts";
+
+const logger = getLogger(["drfed", "federation", "actor-key"]);
+const pending = new WeakMap>>();
+const algorithms = ["RSASSA-PKCS1-v1_5", "Ed25519"] as const;
+
+async function readPairs(rows: LocalActorKey[]): Promise {
+ const result: CryptoKeyPair[] = [];
+ for (const type of algorithms) {
+ const row = rows.find((entry) => entry.type === type);
+ if (row == null) continue;
+ const { publicKey, privateKey } = row;
+ const fields =
+ type === "Ed25519"
+ ? (["kty", "crv", "x"] as const)
+ : (["kty", "n", "e"] as const);
+ if (fields.some((field) => publicKey[field] !== privateKey[field])) {
+ throw new Error("Invalid signing key pair.");
+ }
+ const pair = {
+ publicKey: await importJwk(publicKey, "public"),
+ privateKey: await importJwk(privateKey, "private"),
+ };
+ if (
+ pair.publicKey.algorithm.name !== type ||
+ pair.privateKey.algorithm.name !== type
+ ) {
+ throw new Error("Invalid signing algorithm.");
+ }
+ const challenge = new Uint8Array([1]);
+ const signature = await crypto.subtle.sign(
+ type,
+ pair.privateKey,
+ challenge,
+ );
+ if (
+ !(await crypto.subtle.verify(type, pair.publicKey, signature, challenge))
+ ) {
+ throw new Error("Mismatched signing keys.");
+ }
+ result.push(pair);
+ }
+ return result;
+}
+
+/**
+ * Load durable local actor keys, generating missing pairs on first use.
+ * Deleted actors retain existing complete pairs for signing their Delete.
+ * @returns Persisted pairs, or an empty array for an unavailable actor.
+ * The optional generator is a test seam; production uses Fedify's defaults.
+ */
+export function ensureActorKeyPairs(
+ db: Database,
+ ctx: Context,
+ identifier: string,
+ generate: typeof generateCryptoKeyPair = generateCryptoKeyPair,
+): Promise {
+ if (!validateUuid(identifier)) return Promise.resolve([]);
+ let entries = pending.get(db);
+ if (entries == null) {
+ entries = new Map();
+ pending.set(db, entries);
+ }
+ const key = `${canonicalizeAuthority(ctx.host)}:${identifier}`;
+ const existing = entries.get(key);
+ if (existing != null) return existing;
+ // The shared promise must remain identical for joining callers.
+ // oxlint-disable promise/prefer-await-to-then
+ const operation = ensure(db, ctx, identifier as Uuid, generate)
+ .catch(() => {
+ // Drizzle errors include bound JWKs in their message and cause. Never pass
+ // those errors to Fedify's logging, tracing, or delivery recorder.
+ logger.error("Could not load signing keys for actor {identifier}.", {
+ identifier,
+ });
+ throw new Error("Could not load actor signing keys.");
+ })
+ .finally(() => {
+ entries.delete(key);
+ });
+ // oxlint-enable promise/prefer-await-to-then
+ entries.set(key, operation);
+ return operation;
+}
+
+async function ensure(
+ db: Database,
+ ctx: Context,
+ identifier: Uuid,
+ generate: typeof generateCryptoKeyPair,
+): Promise {
+ const host = canonicalizeAuthority(ctx.host);
+ const actor = await db.query.actors.findFirst({
+ where: { id: identifier, localId: { isNotNull: true }, instance: { host } },
+ });
+ if (actor?.localId == null) return [];
+ const rows = await db
+ .select()
+ .from(schema.localActorKeys)
+ .where(eq(schema.localActorKeys.localActorId, actor.localId));
+ const stored = await readPairs(rows);
+ if (stored.length === 2) return stored;
+ if (actor.deleted != null) return [];
+ const candidates: (typeof schema.localActorKeys.$inferInsert)[] = [];
+ for (const type of algorithms) {
+ if (rows.some((row) => row.type === type)) continue;
+ const pair = await generate(type);
+ candidates.push({
+ localActorId: actor.localId,
+ type,
+ publicKey: await exportJwk(pair.publicKey),
+ privateKey: await exportJwk(pair.privateKey),
+ });
+ }
+ const saved = await db.transaction(async (tx) => {
+ // Parent first: local_actors deletion cascades to actors in this order.
+ const [local] = await tx
+ .select({ id: schema.localActors.id })
+ .from(schema.localActors)
+ .where(eq(schema.localActors.id, actor.localId!))
+ .for("key share");
+ if (local == null) return [];
+ const [current] = await tx
+ .select({ deleted: schema.actors.deleted })
+ .from(schema.actors)
+ .innerJoin(
+ schema.instances,
+ eq(schema.actors.instanceId, schema.instances.id),
+ )
+ .where(
+ and(
+ eq(schema.actors.id, identifier),
+ eq(schema.actors.localId, local.id),
+ eq(schema.instances.host, host),
+ ),
+ )
+ .for("no key update", { of: schema.actors });
+ if (current == null) return [];
+ if (current.deleted == null) {
+ await tx
+ .insert(schema.localActorKeys)
+ .values(candidates)
+ .onConflictDoNothing();
+ }
+ return await tx
+ .select()
+ .from(schema.localActorKeys)
+ .where(eq(schema.localActorKeys.localActorId, local.id));
+ });
+ return saved.length === 2 ? await readPairs(saved) : [];
+}
diff --git a/packages/federation/src/actor.ts b/packages/federation/src/actor.ts
index f7bdd94..4128642 100644
--- a/packages/federation/src/actor.ts
+++ b/packages/federation/src/actor.ts
@@ -17,7 +17,7 @@
import type { Database, schema } from "@drfed/models";
import type { Actor, Resource } from "@drfed/models/schema";
import { type Uuid, validateUuid } from "@drfed/models/uuid";
-import type { Context, FederationBuilder } from "@fedify/fedify";
+import type { ActorKeyPair, Context, FederationBuilder } from "@fedify/fedify";
import {
Application,
Endpoints,
@@ -29,6 +29,7 @@ import {
Tombstone,
} from "@fedify/vocab";
+import { ensureActorKeyPairs } from "./actor-key.ts";
import { canonicalizeAuthority } from "./origin.ts";
/**
@@ -113,10 +114,19 @@ export function registerActorDispatcher(
if (actor.deleted != null) {
return new Tombstone({ id: ctx.getActorUri(identifier) });
}
- return toActorObject(ctx, identifier, actor);
+ const keyContext = ctx.federation.createContext(
+ new URL(actor.resource.iri),
+ ctx.data,
+ );
+ const keys = await keyContext.getActorKeyPairs(actor.id);
+ if (keys.length !== 2) {
+ throw new Error("Could not load actor signing keys.");
+ }
+ return toActorObject(ctx, identifier, actor, keys);
})
- // FIXME: https://github.com/fedify-dev/drfed/issues/87
- .setKeyPairsDispatcher(() => [])
+ .setKeyPairsDispatcher((ctx, identifier) =>
+ ensureActorKeyPairs(db, ctx, identifier),
+ )
.mapHandle(async (ctx, username) => {
const actor = await db.query.actors.findFirst({
where: {
@@ -128,7 +138,6 @@ export function registerActorDispatcher(
});
return actor?.id ?? null;
});
- // FIXME: https://github.com/fedify-dev/drfed/issues/87
}
// Whether a sanction is *currently* active is always determined by comparing
@@ -147,9 +156,12 @@ function toActorObject(
ctx: Context,
identifier: string,
actor: StoredActor,
+ keys: ActorKeyPair[],
): ActorObject {
return actorConstructors[actor.type]({
id: new URL(actor.resource.iri),
+ publicKey: keys[0]!.cryptographicKey,
+ assertionMethods: keys.map((key) => key.multikey),
preferredUsername: actor.username,
name: actor.name,
summary: actor.bioHtml,
diff --git a/packages/federation/src/federation.test.ts b/packages/federation/src/federation.test.ts
index 650ebe4..8b94434 100644
--- a/packages/federation/src/federation.test.ts
+++ b/packages/federation/src/federation.test.ts
@@ -19,11 +19,20 @@
import assert from "node:assert/strict";
-import createFederation, { buildFederation } from "@drfed/federation";
+import createFederation, {
+ buildFederation,
+ enqueueActorKeyGeneration,
+} from "@drfed/federation";
+import { ensureActorKeyPairs } from "@drfed/federation/actor-key";
+import { KeyGenerationQueue } from "@drfed/federation/task-queue";
import { schema } from "@drfed/models";
import { PUBLIC_IRI } from "@drfed/models/resource";
import { type Uuid, uuidV7 as uuid } from "@drfed/models/uuid";
-import { MemoryKvStore } from "@fedify/fedify";
+import {
+ MemoryKvStore,
+ exportJwk,
+ generateCryptoKeyPair,
+} from "@fedify/fedify";
import { Object as APObject, Create } from "@fedify/vocab";
import { describe, it } from "@logtape/testing-node/autoload";
import { eq, sql } from "drizzle-orm";
@@ -32,6 +41,7 @@ import { withFederation, withTemporaryDatabase } from "./harness.test.ts";
import {
localActorId,
remoteActorId,
+ seedActorKeys,
seedActors,
seedLocalActor,
seedObjects,
@@ -717,3 +727,346 @@ describe("stored collection membership and independent activity addressing", ()
});
});
});
+
+describe("durable actor signing keys", () => {
+ it("generates keys on first use and publishes only their public forms", async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db, { keys: false });
+ assert.equal(await db.$count(schema.localActorKeys), 0);
+ const response = await federation.fetch(
+ new Request(actorIri, { headers: accept }),
+ { contextData: undefined },
+ );
+ assert.equal(response.status, 200);
+ const body = await response.json();
+ assert.equal(body.publicKey.id, `${actorIri}#main-key`);
+ assert.equal(body.assertionMethod.length, 2);
+ assert.ok(body.publicKey.publicKeyPem);
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ const first = await ctx.getActorKeyPairs(localActorId);
+ assert.deepEqual(
+ first.map((pair) => pair.privateKey.algorithm.name),
+ ["RSASSA-PKCS1-v1_5", "Ed25519"],
+ );
+ const saved = await db.select().from(schema.localActorKeys);
+ assert.equal(saved.length, 2);
+ assert.equal(
+ JSON.stringify(body).includes(saved[0]!.privateKey["d"]!),
+ false,
+ );
+ const fresh = await createFederation(db, { kv: new MemoryKvStore() });
+ const again = await fresh
+ .createContext(new URL(actorIri), undefined)
+ .getActorKeyPairs(localActorId);
+ assert.deepEqual(
+ await crypto.subtle.exportKey("jwk", first[0]!.publicKey),
+ await crypto.subtle.exportKey("jwk", again[0]!.publicKey),
+ );
+ assert.deepEqual(await db.select().from(schema.localActorKeys), saved);
+ });
+ });
+ it("publishes canonical key ownership for uppercase UUID requests", async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db);
+ const id = "abcdefab-cdef-4abc-8def-abcdefabcdef" as Uuid;
+ await db.insert(schema.localActors).values({ id });
+ const iri = `https://test-instance.drfed.org/users/${id}`;
+ await seedActors(db, {
+ id,
+ localId: id,
+ iri,
+ instanceId: "00000000-0000-4000-8000-000000000101" as Uuid,
+ type: "Person",
+ username: "uppercase-test",
+ inboxUrl: `${iri}/inbox`,
+ created: Temporal.Now.instant(),
+ });
+ const response = await federation.fetch(
+ new Request(iri.replace(id, id.toUpperCase()), { headers: accept }),
+ { contextData: undefined },
+ );
+ assert.equal(response.status, 200);
+ const body = await response.json();
+ assert.equal(body.id, iri);
+ assert.equal(body.publicKey.owner, iri);
+ assert.equal(body.publicKey.id, `${iri}#main-key`);
+ for (const key of body.assertionMethod) assert.equal(key.controller, iri);
+ });
+ });
+ for (const requestOrigin of [
+ "https://test-instance.drfed.org.",
+ "https://test-instance.drfed.org:443",
+ "http://test-instance.drfed.org",
+ "http://test-instance.drfed.org:443",
+ ]) {
+ it(`publishes stored key ownership when requested from ${requestOrigin}`, async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db);
+ const response = await federation.fetch(
+ new Request(new URL(new URL(actorIri).pathname, requestOrigin), {
+ headers: accept,
+ }),
+ { contextData: undefined },
+ );
+ assert.equal(response.status, 200);
+ const body = await response.json();
+ assert.equal(body.id, actorIri);
+ assert.equal(body.publicKey.owner, actorIri);
+ assert.equal(body.publicKey.id, `${actorIri}#main-key`);
+ assert.deepEqual(
+ body.assertionMethod.map((key: { id: string }) => key.id),
+ [`${actorIri}#multikey-1`, `${actorIri}#multikey-2`],
+ );
+ for (const key of body.assertionMethod) {
+ assert.equal(key.controller, actorIri);
+ }
+ });
+ });
+ }
+ it("preserves stored keys when repairing a missing kind and when deleted", async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db);
+ const [rsa] = await db
+ .select()
+ .from(schema.localActorKeys)
+ .where(eq(schema.localActorKeys.type, "RSASSA-PKCS1-v1_5"));
+ await db
+ .delete(schema.localActorKeys)
+ .where(eq(schema.localActorKeys.type, "Ed25519"));
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ assert.equal(
+ (await ensureActorKeyPairs(db, ctx, localActorId)).length,
+ 2,
+ );
+ assert.deepEqual(
+ (
+ await db
+ .select()
+ .from(schema.localActorKeys)
+ .where(eq(schema.localActorKeys.type, "RSASSA-PKCS1-v1_5"))
+ )[0],
+ rsa,
+ );
+ await db
+ .update(schema.actors)
+ .set({ deleted: Temporal.Now.instant() })
+ .where(eq(schema.actors.id, localActorId));
+ assert.equal(
+ (await ensureActorKeyPairs(db, ctx, localActorId)).length,
+ 2,
+ );
+ await db
+ .delete(schema.localActorKeys)
+ .where(eq(schema.localActorKeys.type, "Ed25519"));
+ assert.deepEqual(await ensureActorKeyPairs(db, ctx, localActorId), []);
+ assert.equal(await db.$count(schema.localActorKeys), 1);
+ });
+ });
+ for (const deletion of ["soft", "hard", "actor row"] as const) {
+ it(`does not persist keys when ${deletion} deletion happens during generation`, async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db, { keys: false });
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ let deleted = false;
+ const pairs = await ensureActorKeyPairs(
+ db,
+ ctx,
+ localActorId,
+ async (type) => {
+ if (!deleted) {
+ deleted = true;
+ if (deletion === "soft") {
+ await db
+ .update(schema.actors)
+ .set({ deleted: Temporal.Now.instant() })
+ .where(eq(schema.actors.id, localActorId));
+ } else if (deletion === "hard") {
+ await db
+ .delete(schema.localActors)
+ .where(eq(schema.localActors.id, localActorId));
+ } else {
+ await db
+ .delete(schema.actors)
+ .where(eq(schema.actors.id, localActorId));
+ }
+ }
+ return await generateCryptoKeyPair(type);
+ },
+ );
+ assert.deepEqual(pairs, []);
+ assert.equal(await db.$count(schema.localActorKeys), 0);
+ });
+ });
+ }
+ it("rejects missing, remote and wrong-host actors without storing keys", async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db, { keys: false });
+ await seedRemoteActor(db);
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ assert.deepEqual(await ensureActorKeyPairs(db, ctx, "invalid"), []);
+ assert.deepEqual(await ensureActorKeyPairs(db, ctx, uuid()), []);
+ assert.deepEqual(await ensureActorKeyPairs(db, ctx, remoteActorId), []);
+ assert.deepEqual(
+ await ensureActorKeyPairs(
+ db,
+ federation.createContext(new URL("https://other.example"), undefined),
+ localActorId,
+ ),
+ [],
+ );
+ assert.equal(await db.$count(schema.localActorKeys), 0);
+ });
+ });
+ it("uses the local row identity and enforces key storage constraints", async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db, { keys: false });
+ const localId = uuid();
+ await db.insert(schema.localActors).values({ id: localId });
+ await db
+ .update(schema.actors)
+ .set({ localId })
+ .where(eq(schema.actors.id, localActorId));
+ await seedActorKeys(db, localId);
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ assert.equal(
+ (await ensureActorKeyPairs(db, ctx, localActorId)).length,
+ 2,
+ );
+ const [row] = await db.select().from(schema.localActorKeys);
+ assert.equal(row!.localActorId, localId);
+ await assert.rejects(db.insert(schema.localActorKeys).values(row!));
+ await assert.rejects(
+ db.update(schema.localActorKeys).set({
+ publicKey: { ...row!.publicKey, d: "secret" } as NonNullable<
+ typeof row
+ >["publicKey"],
+ }),
+ );
+ await db
+ .delete(schema.localActors)
+ .where(eq(schema.localActors.id, localId));
+ assert.equal(await db.$count(schema.localActorKeys), 0);
+ assert.deepEqual(await ensureActorKeyPairs(db, ctx, localActorId), []);
+ });
+ });
+ it("coalesces generation and returns a competing persisted winner", async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db, { keys: false });
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ const entered = Promise.withResolvers();
+ const release = Promise.withResolvers();
+ let count = 0;
+ const candidates: Record = {};
+ const candidate = async (type?: "RSASSA-PKCS1-v1_5" | "Ed25519") => {
+ count += 1;
+ entered.resolve();
+ await release.promise;
+ const pair = await generateCryptoKeyPair(type);
+ candidates[type!] = await exportJwk(pair.publicKey);
+ return pair;
+ };
+ const first = ensureActorKeyPairs(db, ctx, localActorId, candidate);
+ const second = ensureActorKeyPairs(db, ctx, localActorId, candidate);
+ await entered.promise;
+ await seedActorKeys(db, localActorId);
+ release.resolve();
+ const [one, two] = await Promise.all([first, second]);
+ assert.equal(count, 2);
+ assert.equal(one, two);
+ assert.equal(one.length, 2);
+ const rows = await db.select().from(schema.localActorKeys);
+ assert.equal(rows.length, 2);
+ for (const pair of one) {
+ const type = pair.publicKey.algorithm.name;
+ const stored = rows.find((row) => row.type === type)!;
+ assert.deepEqual(await exportJwk(pair.publicKey), stored.publicKey);
+ assert.deepEqual(await exportJwk(pair.privateKey), stored.privateKey);
+ assert.notDeepEqual(candidates[type], stored.publicKey);
+ }
+ });
+ });
+ it("sanitizes failures and clears failed in-flight generation", async () => {
+ await withFederation(async ({ db, federation }) => {
+ await seedLocalActor(db, { keys: false });
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ await assert.rejects(
+ ensureActorKeyPairs(db, ctx, localActorId, () =>
+ Promise.reject(new Error("PRIVATE_SECRET")),
+ ),
+ (error: unknown) =>
+ error instanceof Error &&
+ error.message === "Could not load actor signing keys." &&
+ error.cause == null,
+ );
+ assert.equal(await db.$count(schema.localActorKeys), 0);
+ await seedActorKeys(db, localActorId);
+ assert.equal(
+ (await ensureActorKeyPairs(db, ctx, localActorId)).length,
+ 2,
+ );
+ await db
+ .update(schema.localActorKeys)
+ .set({ privateKey: { kty: "RSA", d: "PRIVATE_SECRET" } });
+ await assert.rejects(
+ ensureActorKeyPairs(db, ctx, localActorId),
+ /Could not load actor signing keys/u,
+ );
+ });
+ });
+ it("prewarms through real task dispatch for each federation's own handle", async () => {
+ await withTemporaryDatabase(async (db) => {
+ await seedLocalActor(db, { keys: false });
+ const queue = new KeyGenerationQueue();
+ const federation = await createFederation(db, {
+ kv: new MemoryKvStore(),
+ queue: { task: queue },
+ manuallyStartQueue: true,
+ taskQueueResolution: "strict",
+ });
+ const ctx = federation.createContext(new URL(actorIri), undefined);
+ await enqueueActorKeyGeneration(ctx, [localActorId]);
+ assert.equal((await queue.getDepth()).queued, 1);
+ const abort = new AbortController();
+ const worker = federation.startQueue(undefined, {
+ queue: "task",
+ signal: abort.signal,
+ });
+ try {
+ await assertEventually(
+ async () => (await db.$count(schema.localActorKeys)) === 2,
+ );
+ await enqueueActorKeyGeneration(ctx, [localActorId]);
+ } finally {
+ abort.abort();
+ await worker;
+ }
+ const otherQueue = new KeyGenerationQueue();
+ const other = await createFederation(db, {
+ kv: new MemoryKvStore(),
+ queue: { task: otherQueue },
+ manuallyStartQueue: true,
+ });
+ await enqueueActorKeyGeneration(
+ other.createContext(new URL(actorIri), undefined),
+ [localActorId],
+ );
+ assert.equal((await otherQueue.getDepth()).queued, 1);
+ const noQueue = await createFederation(db, { kv: new MemoryKvStore() });
+ await enqueueActorKeyGeneration(
+ noQueue.createContext(new URL(actorIri), undefined),
+ [localActorId],
+ );
+ });
+ });
+});
+async function assertEventually(check: () => Promise): Promise {
+ for (let attempt = 0; attempt < 500; attempt += 1) {
+ // oxlint-disable-next-line no-await-in-loop
+ if (await check()) return;
+ // oxlint-disable-next-line no-await-in-loop
+ await new Promise((resolve) => {
+ setTimeout(resolve, 20);
+ });
+ }
+ assert.fail("Background task did not finish.");
+}
diff --git a/packages/federation/src/index.ts b/packages/federation/src/index.ts
index 5973310..310d612 100644
--- a/packages/federation/src/index.ts
+++ b/packages/federation/src/index.ts
@@ -35,6 +35,7 @@ import {
attachKv,
trackPublicKeys,
} from "./activity-delivery/tracking.ts";
+import { attachActorKeyTask, registerActorKeyTask } from "./actor-key-task.ts";
import { registerActorDispatcher } from "./actor.ts";
import { registerCollectionDispatchers } from "./collection.ts";
import { registerInboxListeners } from "./inbox.ts";
@@ -42,6 +43,7 @@ import { registerObjectDispatchers } from "./object-dispatchers.ts";
export { createInboundRecorder } from "./activity-delivery/inbound.ts";
export type { TrackedFederation } from "./activity-delivery/tracking.ts";
+export { enqueueActorKeyGeneration } from "./actor-key-task.ts";
export { deliverActivity } from "./activity-delivery/outbound.ts";
/**
@@ -55,6 +57,7 @@ export { deliverActivity } from "./activity-delivery/outbound.ts";
export function buildFederation(db: Database): FederationBuilder {
const builder = createFederationBuilder();
registerActorDispatcher(builder, db);
+ registerActorKeyTask(builder, db);
registerInboxListeners(builder);
registerObjectDispatchers(builder, db);
registerCollectionDispatchers(builder, db);
@@ -80,7 +83,8 @@ export default async function createFederation(
db: Database,
options: FederationOptions,
): Promise {
- const federation = await buildFederation(db).build({
+ const builder = buildFederation(db);
+ const federation = await builder.build({
...options,
kv: trackPublicKeys(
options.kv,
@@ -102,5 +106,12 @@ export default async function createFederation(
});
if (outboxQueue(options.queue) != null) markQueued(federation);
attachKv(federation, options.kv);
+ attachActorKeyTask(
+ builder,
+ federation,
+ options.queue != null &&
+ "task" in options.queue &&
+ options.queue.task != null,
+ );
return federation as TrackedFederation;
}
diff --git a/packages/federation/src/seed.test.ts b/packages/federation/src/seed.test.ts
index 953e29a..9213b20 100644
--- a/packages/federation/src/seed.test.ts
+++ b/packages/federation/src/seed.test.ts
@@ -26,6 +26,7 @@ import {
} from "@drfed/models";
import { type AddressingInput, PUBLIC_IRI } from "@drfed/models/resource";
import { type Uuid, uuidV7 } from "@drfed/models/uuid";
+import { exportJwk, generateCryptoKeyPair } from "@fedify/fedify";
import type { PgInsertValue } from "drizzle-orm/pg-core";
export const created = Temporal.Instant.from("2026-08-04T00:00:00.000Z");
@@ -36,26 +37,33 @@ export const remoteInstanceId = "00000000-0000-4000-8000-000000000102";
export const localActorId = "00000000-0000-4000-8000-000000000201" as const;
export const remoteActorId = "00000000-0000-4000-8000-000000000202" as const;
-export async function seedLocalActor(db: Database): Promise {
+export async function seedLocalActor(
+ db: Database,
+ options: { keys?: boolean } = {},
+): Promise {
await seedLocalInstance(db);
await db.insert(schema.localActors).values({
id: localActorId,
avatar: "avatar.png",
header: "header.png",
});
- await seedActors(db, {
- id: localActorId,
- localId: localActorId,
- instanceId: localInstanceId,
- type: "Person",
- username: "alice",
- iri: `https://test-instance.drfed.org/users/${localActorId}`,
- inboxUrl: `https://test-instance.drfed.org/users/${localActorId}/inbox`,
- avatarUrl: `https://test-instance.drfed.org/users/${localActorId}/avatar/avatar.png`,
- headerUrl: `https://test-instance.drfed.org/users/${localActorId}/header/header.png`,
- profileUrl: "https://test-instance.drfed.org/@alice",
- created,
- });
+ await seedActors(
+ db,
+ {
+ id: localActorId,
+ localId: localActorId,
+ instanceId: localInstanceId,
+ type: "Person",
+ username: "alice",
+ iri: `https://test-instance.drfed.org/users/${localActorId}`,
+ inboxUrl: `https://test-instance.drfed.org/users/${localActorId}/inbox`,
+ avatarUrl: `https://test-instance.drfed.org/users/${localActorId}/avatar/avatar.png`,
+ headerUrl: `https://test-instance.drfed.org/users/${localActorId}/header/header.png`,
+ profileUrl: "https://test-instance.drfed.org/@alice",
+ created,
+ },
+ options,
+ );
}
export async function seedLocalInstance(
@@ -108,6 +116,7 @@ type ActorSeed = PgInsertValue & {
export async function seedActors(
db: Database,
values: ActorSeed | ActorSeed[],
+ options: { keys?: boolean } = {},
): Promise {
for (const { iri, ...actor } of Array.isArray(values) ? values : [values]) {
await promoteResource(
@@ -143,6 +152,9 @@ export async function seedActors(
},
actor.id,
);
+ if (actor.localId != null && options.keys !== false) {
+ await seedActorKeys(db, actor.localId as Uuid);
+ }
}
}
type ObjectSeed = PgInsertValue & {
@@ -208,3 +220,45 @@ export async function seedObjects(
);
}
}
+
+let fixtureKeys:
+ | Promise<
+ {
+ type: "RSASSA-PKCS1-v1_5" | "Ed25519";
+ publicKey: JsonWebKey;
+ privateKey: JsonWebKey;
+ }[]
+ >
+ | undefined;
+/** In-memory synthetic fixture material; generation-specific tests opt out. */
+export async function seedActorKeys(
+ db: Database,
+ localId: Uuid,
+): Promise {
+ fixtureKeys ??= (async () => {
+ const rsa = await crypto.subtle.generateKey(
+ {
+ name: "RSASSA-PKCS1-v1_5",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: "SHA-256",
+ },
+ true,
+ ["sign", "verify"],
+ );
+ const ed = await generateCryptoKeyPair("Ed25519");
+ return await Promise.all(
+ [rsa, ed].map(async (pair) => ({
+ type: pair.privateKey.algorithm.name as "RSASSA-PKCS1-v1_5" | "Ed25519",
+ publicKey: await exportJwk(pair.publicKey),
+ privateKey: await exportJwk(pair.privateKey),
+ })),
+ );
+ })();
+ await db
+ .insert(schema.localActorKeys)
+ .values(
+ (await fixtureKeys).map((pair) => ({ ...pair, localActorId: localId })),
+ )
+ .onConflictDoNothing();
+}
diff --git a/packages/federation/src/task-queue.test.ts b/packages/federation/src/task-queue.test.ts
new file mode 100644
index 0000000..4f0add1
--- /dev/null
+++ b/packages/federation/src/task-queue.test.ts
@@ -0,0 +1,99 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import assert from "node:assert/strict";
+import { getEventListeners } from "node:events";
+import { it } from "node:test";
+
+import { KeyGenerationQueue } from "@drfed/federation/task-queue";
+
+it("bounds complete batches and drops work after shutdown", async () => {
+ const queue = new KeyGenerationQueue(2);
+ await queue.enqueueMany([1, 2, 3]);
+ assert.equal(queue.dropped, 3);
+ assert.equal((await queue.getDepth()).queued, 0);
+ await queue.enqueueMany([1, 2]);
+ const abort = new AbortController();
+ const received: unknown[] = [];
+ await queue.listen(
+ (value) => {
+ received.push(value);
+ if (received.length === 2) abort.abort();
+ },
+ { signal: abort.signal },
+ );
+ assert.deepEqual(received, [1, 2]);
+ await queue.enqueue(4);
+ assert.equal(queue.dropped, 4);
+ assert.equal(getEventListeners(abort.signal, "abort").length, 0);
+});
+it("releases wait listeners through repeated wakeups and idle abort", async () => {
+ const queue = new KeyGenerationQueue();
+ const abort = new AbortController();
+ let notification = Promise.withResolvers();
+ const worker = queue.listen(() => notification.resolve(), {
+ signal: abort.signal,
+ });
+ for (let index = 0; index < 100; index += 1) {
+ // oxlint-disable-next-line no-await-in-loop
+ await queue.enqueue(index);
+ // oxlint-disable-next-line no-await-in-loop
+ await notification.promise;
+ notification = Promise.withResolvers();
+ assert.ok(getEventListeners(abort.signal, "abort").length <= 1);
+ }
+ abort.abort();
+ await worker;
+ assert.equal(getEventListeners(abort.signal, "abort").length, 0);
+});
+it("waits for an active handler, continues after errors and rejects delays", async () => {
+ const queue = new KeyGenerationQueue();
+ const abort = new AbortController();
+ const entered = Promise.withResolvers();
+ const release = Promise.withResolvers();
+ await queue.enqueueMany([1, 2]);
+ const worker = queue.listen(
+ async (value) => {
+ if (value === 1) throw new Error("test failure");
+ entered.resolve();
+ await release.promise;
+ },
+ { signal: abort.signal },
+ );
+ await entered.promise;
+ let finished = false;
+ // oxlint-disable-next-line promise/prefer-await-to-then
+ const settled = worker.then(() => {
+ finished = true;
+ return undefined;
+ });
+ abort.abort();
+ await Promise.resolve();
+ assert.equal(finished, false);
+ release.resolve();
+ await settled;
+ await assert.rejects(
+ queue.enqueue(3, { delay: Temporal.Duration.from({ seconds: 1 }) }),
+ /delayed/u,
+ );
+});
+it("closes an already-aborted queue and clears its buffered work", async () => {
+ const queue = new KeyGenerationQueue();
+ await queue.enqueue(1);
+ await queue.listen(() => assert.fail(), { signal: AbortSignal.abort() });
+ assert.equal(queue.dropped, 1);
+ assert.equal((await queue.getDepth()).queued, 0);
+});
diff --git a/packages/federation/src/task-queue.ts b/packages/federation/src/task-queue.ts
new file mode 100644
index 0000000..f2bfb01
--- /dev/null
+++ b/packages/federation/src/task-queue.ts
@@ -0,0 +1,129 @@
+// DrFed: A web-based platform for developing and debugging ActivityPub apps
+// Copyright (C) 2026 DrFed team
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+// oxlint-disable no-await-in-loop
+import type {
+ MessageQueue,
+ MessageQueueEnqueueOptions,
+ MessageQueueListenOptions,
+} from "@fedify/fedify";
+import { getLogger } from "@logtape/logtape";
+
+const logger = getLogger(["drfed", "federation", "task-queue"]);
+
+/**
+ * Bounded, ephemeral FIFO transport for immediate key prewarming only.
+ * Fedify owns the message codec and task dispatch. Dropped warmups are repaired
+ * by lazy key generation. No polling timers or retained abort listeners.
+ */
+export class KeyGenerationQueue implements MessageQueue {
+ readonly nativeRetrial = false;
+ readonly nativeDeduplication = false;
+ readonly atomicEnqueueMany = true;
+ #messages: unknown[] = [];
+ #wake: (() => void) | undefined;
+ #closed = false;
+ #listening = false;
+ #dropped = 0;
+ readonly #capacity: number;
+ constructor(capacity = 1024) {
+ if (!Number.isSafeInteger(capacity) || capacity < 1) {
+ throw new RangeError("Invalid task queue capacity.");
+ }
+ this.#capacity = capacity;
+ }
+ /** Number of expendable messages discarded due to capacity or shutdown.
+ * @returns The cumulative dropped message count.
+ */
+ get dropped(): number {
+ return this.#dropped;
+ }
+ enqueue(
+ message: unknown,
+ options?: MessageQueueEnqueueOptions,
+ ): Promise {
+ return this.enqueueMany([message], options);
+ }
+ enqueueMany(
+ messages: readonly unknown[],
+ options?: MessageQueueEnqueueOptions,
+ ): Promise {
+ if (
+ options?.delay != null &&
+ Temporal.Duration.from(options.delay).sign !== 0
+ ) {
+ return Promise.reject(
+ new TypeError("Key prewarming does not support delayed messages."),
+ );
+ }
+ if (
+ this.#closed ||
+ this.#messages.length + messages.length > this.#capacity
+ ) {
+ this.#dropped += messages.length;
+ return Promise.resolve();
+ }
+ this.#messages.push(...messages);
+ this.#wake?.();
+ return Promise.resolve();
+ }
+ getDepth() {
+ return Promise.resolve({
+ queued: this.#messages.length,
+ ready: this.#messages.length,
+ delayed: 0,
+ });
+ }
+ async listen(
+ handler: (message: unknown) => void | Promise,
+ options?: MessageQueueListenOptions,
+ ): Promise {
+ if (this.#listening || this.#closed) {
+ throw new Error("Key task queue cannot be restarted.");
+ }
+ this.#listening = true;
+ const signal = options?.signal;
+ try {
+ // The AbortSignal is changed by the worker owner.
+ // oxlint-disable-next-line no-unmodified-loop-condition
+ while (!signal?.aborted) {
+ if (this.#messages.length === 0) {
+ const wake = Promise.withResolvers();
+ this.#wake = () => wake.resolve();
+ signal?.addEventListener("abort", this.#wake, { once: true });
+ try {
+ await wake.promise;
+ } finally {
+ signal?.removeEventListener("abort", this.#wake);
+ this.#wake = undefined;
+ }
+ continue;
+ }
+ const message = this.#messages.shift();
+ try {
+ await handler(message);
+ } catch {
+ logger.error("A key prewarming message failed.");
+ }
+ }
+ } finally {
+ this.#closed = true;
+ this.#dropped += this.#messages.length;
+ this.#messages = [];
+ this.#wake = undefined;
+ }
+ }
+}
diff --git a/packages/graphql/src/activity-delivery.test.ts b/packages/graphql/src/activity-delivery.test.ts
index a062c83..f79f618 100644
--- a/packages/graphql/src/activity-delivery.test.ts
+++ b/packages/graphql/src/activity-delivery.test.ts
@@ -129,6 +129,7 @@ it("records signed, rotated, tampered and rejected inbox deliveries with the ori
kv,
contextLoaderFactory: () => contextLoader,
documentLoaderFactory: () => documentLoader,
+ authenticatedDocumentLoaderFactory: () => documentLoader,
});
const recorder = createInboundRecorder({ db, federation, rootOrigin });
const send = async (
@@ -264,6 +265,8 @@ it("records missing signatures, failed key fetches and non-JSON bodies, and skip
contextLoaderFactory: () => contextLoader,
documentLoaderFactory: () => () =>
Promise.reject(new TypeError("offline")),
+ authenticatedDocumentLoaderFactory: () => () =>
+ Promise.reject(new TypeError("offline")),
});
const recorder = createInboundRecorder({ db, federation, rootOrigin });
assert.equal(
diff --git a/packages/graphql/src/activity-delivery/inbound.test.ts b/packages/graphql/src/activity-delivery/inbound.test.ts
index a913499..abbf43b 100644
--- a/packages/graphql/src/activity-delivery/inbound.test.ts
+++ b/packages/graphql/src/activity-delivery/inbound.test.ts
@@ -129,6 +129,7 @@ async function createRecorder(
kv,
contextLoaderFactory: () => contextLoader,
documentLoaderFactory: () => documentLoader,
+ authenticatedDocumentLoaderFactory: () => documentLoader,
});
const recorder = createInboundRecorder({ db, federation, rootOrigin });
return {
@@ -519,6 +520,19 @@ it("records a request Fedify throws on, and throws the exception again", async (
],
]),
),
+ authenticatedDocumentLoaderFactory: () =>
+ keyLoader(
+ new Map([
+ [
+ httpKeyId.href,
+ new CryptographicKey({
+ id: httpKeyId,
+ owner: actorIri,
+ publicKey: pair.publicKey,
+ }),
+ ],
+ ]),
+ ),
});
const recorder = createInboundRecorder({ db, federation, rootOrigin });
const body = JSON.stringify(
@@ -969,6 +983,19 @@ it("receives a queued activity once the queue worker runs its listener", async (
],
]),
),
+ authenticatedDocumentLoaderFactory: () =>
+ keyLoader(
+ new Map([
+ [
+ proofKeyId.href,
+ new Multikey({
+ id: proofKeyId,
+ controller: actorIri,
+ publicKey: pair.publicKey,
+ }),
+ ],
+ ]),
+ ),
});
const controller = new AbortController();
const start = () =>
@@ -1556,6 +1583,8 @@ it("orders deliveries by arrival, even when handling ends out of order", async (
contextLoaderFactory: () => contextLoader,
documentLoaderFactory: () => () =>
Promise.reject(new TypeError("offline")),
+ authenticatedDocumentLoaderFactory: () => () =>
+ Promise.reject(new TypeError("offline")),
});
const { promise: reached, resolve: enter } = Promise.withResolvers();
const { promise: gate, resolve: release } = Promise.withResolvers();
diff --git a/packages/graphql/src/actor.test.ts b/packages/graphql/src/actor.test.ts
index 19a86c0..14ffa15 100644
--- a/packages/graphql/src/actor.test.ts
+++ b/packages/graphql/src/actor.test.ts
@@ -20,6 +20,7 @@
import assert from "node:assert/strict";
+import { KeyGenerationQueue } from "@drfed/federation/task-queue";
import { schema } from "@drfed/models";
import { type Uuid, uuidV7 as uuid } from "@drfed/models/uuid";
import { faker } from "@faker-js/faker";
@@ -195,6 +196,7 @@ describe("Mutation.generateActors", () => {
),
);
+ assert.equal(await db.$count(schema.localActorKeys), 0);
const actors = await db.select().from(schema.actors);
assert.equal(actors.length, 2);
assert.equal(username.mock.callCount(), 3);
@@ -228,6 +230,64 @@ describe("Mutation.generateActors", () => {
});
});
+ it("schedules only committed actors and survives a failed prewarm enqueue", async (test) => {
+ const queue = new KeyGenerationQueue();
+ await withTestHarness(
+ async ({ db, post, federation }) => {
+ const auth = await seedAuthenticatedLocalInstance(db);
+ let observedActors: number | undefined;
+ let observedKeys: number | undefined;
+ const enqueue = test.mock.method(queue, "enqueueMany", async () => {
+ observedActors = await db.$count(schema.actors);
+ observedKeys = await db.$count(schema.localActorKeys);
+ throw new Error("Expendable queue unavailable.");
+ });
+ const response = await post(
+ {
+ query: generateActorsMutation,
+ variables: {
+ instance: globalId("Instance", localInstanceId),
+ size: 2,
+ },
+ },
+ auth,
+ );
+ const body = await response.json();
+ assert.equal(body.errors, undefined);
+ assert.equal(
+ body.data.generateActors.resultType,
+ "CreateActorsSuccess",
+ );
+ assert.equal(enqueue.mock.callCount(), 1);
+ assert.equal(observedActors, 2);
+ assert.equal(observedKeys, 0);
+ const actor = body.data.generateActors.actors[0];
+ const context = federation.createContext(new URL(actor.iri), undefined);
+ assert.equal((await context.getActorKeyPairs(actor.uuid)).length, 2);
+ assert.equal(await db.$count(schema.localActorKeys), 2);
+ const rejected = await post(
+ {
+ query: generateActorsMutation,
+ variables: {
+ instance: globalId("Instance", localInstanceId),
+ size: 10000,
+ },
+ },
+ auth,
+ );
+ assert.equal(
+ (await rejected.json()).data.generateActors.resultType,
+ "CreateActorsError",
+ );
+ assert.equal(enqueue.mock.callCount(), 1);
+ },
+ undefined,
+ undefined,
+ undefined,
+ { queue: { task: queue } },
+ );
+ });
+
it("builds actor URIs from the stored host and the root scheme", async () => {
// The stored host deliberately disagrees with what recomposing
// `${slug}.${root}` would produce, and the root origin is HTTP on a
diff --git a/packages/graphql/src/actor.ts b/packages/graphql/src/actor.ts
index f11dfdc..c27daa3 100644
--- a/packages/graphql/src/actor.ts
+++ b/packages/graphql/src/actor.ts
@@ -19,6 +19,7 @@
// Keep dependent database writes and observations sequential.
// oxlint-disable no-await-in-loop
+import { enqueueActorKeyGeneration } from "@drfed/federation";
import { type Database, promoteResource, schema } from "@drfed/models";
import { type CollectionRole, actorTypeEnum } from "@drfed/models/schema";
import { type Uuid, uuidV7 as uuid } from "@drfed/models/uuid";
@@ -284,7 +285,8 @@ builder.mutationFields((t) => ({
// Relay decodes global IDs as strings; Instance uses UUID identifiers.
const targetInstanceId = instanceId as Uuid;
- return await ctx.db.transaction(async (tx) => {
+ let createdOrigin: URL | undefined;
+ const result = await ctx.db.transaction(async (tx) => {
// Find the instance that the account is included
const [instance] = await tx
.select({
@@ -346,6 +348,7 @@ builder.mutationFields((t) => ({
};
}
// Create actors
+ createdOrigin = new URL(instanceUrl);
const fedCtx = ctx.federation.createContext(
new URL(instanceUrl),
undefined,
@@ -403,6 +406,13 @@ builder.mutationFields((t) => ({
}
return { actors: createdActors };
});
+ if ("actors" in result && createdOrigin != null) {
+ await enqueueActorKeyGeneration(
+ ctx.federation.createContext(createdOrigin, undefined),
+ result.actors.map((actor) => actor.id),
+ );
+ }
+ return result;
},
}),
}));
diff --git a/packages/graphql/src/harness.test.ts b/packages/graphql/src/harness.test.ts
index bfe919c..6f1ddeb 100644
--- a/packages/graphql/src/harness.test.ts
+++ b/packages/graphql/src/harness.test.ts
@@ -18,7 +18,11 @@ import { createYogaServer } from "@drfed/graphql";
import type { ServerContext, UserContext } from "@drfed/graphql/builder";
import { type Database, migrate, relations, schema } from "@drfed/models";
import { PGlite } from "@electric-sql/pglite";
-import { type Federation, MemoryKvStore } from "@fedify/fedify";
+import {
+ type Federation,
+ type FederationOptions,
+ MemoryKvStore,
+} from "@fedify/fedify";
import { getLogger } from "@logtape/logtape";
import { MockTransport } from "@upyo/mock";
import { drizzle } from "drizzle-orm/pglite";
@@ -191,10 +195,14 @@ export async function withTestHarness(
rootOrigin: URL = new URL("https://drfed.org"),
loginOrigins: ReadonlySet = new Set(["https://drfed.test"]),
emailFrom?: string,
+ federationOptions: Pick, "queue"> = {},
): Promise> {
return await withTemporaryDatabase(async (db) => {
const mailer = new MockTransport();
- const federation = await createFederation(db, { kv: new MemoryKvStore() });
+ const federation = await createFederation(db, {
+ kv: new MemoryKvStore(),
+ ...federationOptions,
+ });
const yoga = createYogaServer(db, federation, {
mailer,
rootOrigin,
diff --git a/packages/graphql/src/seed.test.ts b/packages/graphql/src/seed.test.ts
index adc34a3..8140dc9 100644
--- a/packages/graphql/src/seed.test.ts
+++ b/packages/graphql/src/seed.test.ts
@@ -26,6 +26,7 @@ import {
} from "@drfed/models";
import { type AddressingInput, PUBLIC_IRI } from "@drfed/models/resource";
import { type Uuid, uuidV7 } from "@drfed/models/uuid";
+import { exportJwk, generateCryptoKeyPair } from "@fedify/fedify";
import type { PgInsertValue } from "drizzle-orm/pg-core";
import { hashSecret } from "./auth/hash.ts";
@@ -81,26 +82,33 @@ export async function seedAuthenticatedLocalInstance(
return { headers: { authorization: `Bearer ${accessToken}` } };
}
-export async function seedLocalActor(db: Database): Promise {
+export async function seedLocalActor(
+ db: Database,
+ options: { keys?: boolean } = {},
+): Promise {
await seedLocalInstance(db);
await db.insert(schema.localActors).values({
id: localActorId,
avatar: "avatar.png",
header: "header.png",
});
- await seedActors(db, {
- id: localActorId,
- localId: localActorId,
- instanceId: localInstanceId,
- type: "Person",
- username: "alice",
- iri: `https://test-instance.drfed.org/users/${localActorId}`,
- inboxUrl: `https://test-instance.drfed.org/users/${localActorId}/inbox`,
- avatarUrl: `https://test-instance.drfed.org/users/${localActorId}/avatar/avatar.png`,
- headerUrl: `https://test-instance.drfed.org/users/${localActorId}/header/header.png`,
- profileUrl: "https://test-instance.drfed.org/@alice",
- created,
- });
+ await seedActors(
+ db,
+ {
+ id: localActorId,
+ localId: localActorId,
+ instanceId: localInstanceId,
+ type: "Person",
+ username: "alice",
+ iri: `https://test-instance.drfed.org/users/${localActorId}`,
+ inboxUrl: `https://test-instance.drfed.org/users/${localActorId}/inbox`,
+ avatarUrl: `https://test-instance.drfed.org/users/${localActorId}/avatar/avatar.png`,
+ headerUrl: `https://test-instance.drfed.org/users/${localActorId}/header/header.png`,
+ profileUrl: "https://test-instance.drfed.org/@alice",
+ created,
+ },
+ options,
+ );
}
export async function seedLocalInstance(
@@ -153,6 +161,7 @@ type ActorSeed = PgInsertValue & {
export async function seedActors(
db: Database,
values: ActorSeed | ActorSeed[],
+ options: { keys?: boolean } = {},
): Promise {
for (const { iri, ...actor } of Array.isArray(values) ? values : [values]) {
await promoteResource(
@@ -188,6 +197,9 @@ export async function seedActors(
},
actor.id,
);
+ if (actor.localId != null && options.keys !== false) {
+ await seedActorKeys(db, actor.localId as Uuid);
+ }
}
}
type ObjectSeed = PgInsertValue & {
@@ -253,3 +265,45 @@ export async function seedObjects(
);
}
}
+
+let fixtureKeys:
+ | Promise<
+ {
+ type: "RSASSA-PKCS1-v1_5" | "Ed25519";
+ publicKey: JsonWebKey;
+ privateKey: JsonWebKey;
+ }[]
+ >
+ | undefined;
+/** In-memory synthetic fixture material; generation-specific tests opt out. */
+export async function seedActorKeys(
+ db: Database,
+ localId: Uuid,
+): Promise {
+ fixtureKeys ??= (async () => {
+ const rsa = await crypto.subtle.generateKey(
+ {
+ name: "RSASSA-PKCS1-v1_5",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: "SHA-256",
+ },
+ true,
+ ["sign", "verify"],
+ );
+ const ed = await generateCryptoKeyPair("Ed25519");
+ return await Promise.all(
+ [rsa, ed].map(async (pair) => ({
+ type: pair.privateKey.algorithm.name as "RSASSA-PKCS1-v1_5" | "Ed25519",
+ publicKey: await exportJwk(pair.publicKey),
+ privateKey: await exportJwk(pair.privateKey),
+ })),
+ );
+ })();
+ await db
+ .insert(schema.localActorKeys)
+ .values(
+ (await fixtureKeys).map((pair) => ({ ...pair, localActorId: localId })),
+ )
+ .onConflictDoNothing();
+}
diff --git a/packages/models/drizzle/20261006122653_local_actor_keys/migration.sql b/packages/models/drizzle/20261006122653_local_actor_keys/migration.sql
new file mode 100644
index 0000000..ef19765
--- /dev/null
+++ b/packages/models/drizzle/20261006122653_local_actor_keys/migration.sql
@@ -0,0 +1,11 @@
+CREATE TYPE "local_actor_key_type" AS ENUM('RSASSA-PKCS1-v1_5', 'Ed25519');--> statement-breakpoint
+CREATE TABLE "local_actor_keys" (
+ "local_actor_id" uuid,
+ "type" "local_actor_key_type",
+ "public_key" jsonb NOT NULL,
+ "private_key" jsonb NOT NULL,
+ CONSTRAINT "local_actor_keys_pkey" PRIMARY KEY("local_actor_id","type"),
+ CONSTRAINT "local_actor_keys_public_key_check" CHECK (jsonb_typeof("public_key") = 'object' AND NOT ("public_key" ?| array['d','p','q','dp','dq','qi','oth','k']))
+);
+--> statement-breakpoint
+ALTER TABLE "local_actor_keys" ADD CONSTRAINT "local_actor_keys_local_actor_id_local_actors_id_fkey" FOREIGN KEY ("local_actor_id") REFERENCES "local_actors"("id") ON DELETE CASCADE;
\ No newline at end of file
diff --git a/packages/models/drizzle/20261006122653_local_actor_keys/snapshot.json b/packages/models/drizzle/20261006122653_local_actor_keys/snapshot.json
new file mode 100644
index 0000000..28379a3
--- /dev/null
+++ b/packages/models/drizzle/20261006122653_local_actor_keys/snapshot.json
@@ -0,0 +1,3627 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "1dbf7e97-eced-481e-8b51-850cfa40c2f8",
+ "prevIds": ["7b4b4a36-eeea-4c8c-a48f-4e4928939348"],
+ "ddl": [
+ {
+ "values": ["inbound", "outbound"],
+ "name": "activity_delivery_direction",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "received",
+ "acknowledged",
+ "unverified",
+ "rejected",
+ "queued",
+ "sent",
+ "failed",
+ "permanently_failed",
+ "abandoned"
+ ],
+ "name": "activity_delivery_status",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["http_signature", "ld_signature", "object_integrity_proof"],
+ "name": "activity_delivery_verification_mechanism",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "verified",
+ "invalid_signature",
+ "key_fetch_error",
+ "no_signature",
+ "unattempted",
+ "unobserved"
+ ],
+ "name": "activity_delivery_verification_result",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["Create"],
+ "name": "activity_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["Application", "Group", "Organization", "Person", "Service"],
+ "name": "actor_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["to", "cc", "bto", "bcc", "audience"],
+ "name": "addressing_property",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["followers", "following", "featured", "outbox"],
+ "name": "collection_role",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["Collection", "OrderedCollection"],
+ "name": "collection_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["RSASSA-PKCS1-v1_5", "Ed25519"],
+ "name": "local_actor_key_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["Article", "Note"],
+ "name": "object_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": ["actor", "object", "activity", "collection", "unknown"],
+ "name": "resource_kind",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "accounts",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "activities",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "activity_deliveries",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "activity_delivery_actor_collections",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "activity_delivery_actors",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "activity_delivery_attempts",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "actor_collection_references",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "actors",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "addressing",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "collection_items",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "collections",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instance_members",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "key_versions",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "keys",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "local_actor_keys",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "local_actors",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "local_instances",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "login_challenges",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "objects",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "resources",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "sessions",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "varchar(100)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "10",
+ "generated": null,
+ "identity": null,
+ "name": "max_instances",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "admin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "type": "activity_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "object_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "published",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "type": "json",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "document",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "instance_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "activity_delivery_direction",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "direction",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "activity_delivery_status",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "status",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "activity_delivery_verification_mechanism",
+ "typeSchema": "public",
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "verification_mechanism",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "activity_delivery_verification_result",
+ "typeSchema": "public",
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "verification_result",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 1,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "types",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "activity_iri",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "object_type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "object_iri",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "signed_key_iri",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "verification_key_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "remote_actor_iri",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "remote_host",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "inbox_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "request_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "headers",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "bytea",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "body",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "status_code",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "response_body",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "error",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "payload",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 1,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "recipient_iris",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "completed",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "delivery_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actor_collections"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actor_collections"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "collection_iri",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actor_collections"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "delivery_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "inbox_owner",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "addressed",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "addressed_directly",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "sender",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "delivery_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "succeeded",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "status_code",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "response_body",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "error",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actor_collection_references"
+ },
+ {
+ "type": "collection_role",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "role",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actor_collection_references"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "collection_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actor_collection_references"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "local_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "actor_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "username",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "instance_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "json",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "document",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "inbox_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "profile_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "avatar_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "header_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "bio_html",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "automatically_approves_followers",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "field_htmls",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "emojis",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'{}'",
+ "generated": null,
+ "identity": null,
+ "name": "tags",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "sensitive",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "suspended",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "suspended_until",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "successor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 1,
+ "default": "(ARRAY[]::text[])",
+ "generated": null,
+ "identity": null,
+ "name": "aliases",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "0",
+ "generated": null,
+ "identity": null,
+ "name": "following_count",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "0",
+ "generated": null,
+ "identity": null,
+ "name": "followers_count",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "updated",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "published",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "deleted",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "source_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "type": "addressing_property",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "property",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "position",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "target_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "collection_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "item_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "position",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "observed",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "type": "collection_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "owner_actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "total_items",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "type": "json",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "document",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "updated",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "account_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "instance_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "admin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "accepted",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "local_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "varchar(259)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "host",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "node_info_url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "software",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "software_version",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "key_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "public_key",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "fingerprint",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "first_seen",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "last_seen",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "keys"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "iri",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "keys"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "keys"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "local_actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_actor_keys"
+ },
+ {
+ "type": "local_actor_key_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_actor_keys"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "public_key",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_actor_keys"
+ },
+ {
+ "type": "jsonb",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "private_key",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_actor_keys"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "avatar",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_actors"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "header",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_actors"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "varchar(63)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "slug",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "10",
+ "generated": null,
+ "identity": null,
+ "name": "max_actors",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_challenges"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "account_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_challenges"
+ },
+ {
+ "type": "char(6)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "code",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_challenges"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_challenges"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP + INTERVAL '15 minutes'",
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_challenges"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "consumed",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "login_challenges"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "actor_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "object_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "json",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "document",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "url",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "name",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "summary",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "content_html",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "varchar(35)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "language",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "sensitive",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "published",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "updated",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "deleted",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "resources"
+ },
+ {
+ "type": "text",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "iri",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "resources"
+ },
+ {
+ "type": "resource_kind",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "kind",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "resources"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "resources"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "uuid",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "account_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "varchar(64)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "token_hash",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP",
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "CURRENT_TIMESTAMP + INTERVAL '1 month'",
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "actor_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"published\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"id\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_actor_published_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "published",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_object_published_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "instance_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"created\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"id\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_delivery_instance_created_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "actor_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_delivery_actor_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "verification_key_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_delivery_verification_key_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "activity_iri",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "inbox_url",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": "\"direction\" = 'outbound'",
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_delivery_outbound_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "actor_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"created\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"delivery_id\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_delivery_actor_created_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "delivery_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "created",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "activity_delivery_attempt_delivery_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "collection_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "actor_collection_reference_collection_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "actor_collection_references"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "instance_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "actor_instance_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "target_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "property",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "addressing_target_property_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "collection_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "position",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "collection_item_position_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ {
+ "value": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": "\"accepted\" IS NOT NULL",
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "instance_members_accountId_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ {
+ "value": "instance_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": "\"accepted\" IS NOT NULL",
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "instance_members_instanceId_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "key_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "first_seen",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "key_version_key_first_seen_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "actor_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"published\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "\"id\" desc",
+ "isExpression": true,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "object_actor_published_index",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activities_id_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["actor_id"],
+ "schemaTo": "public",
+ "tableTo": "actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activities_actorId_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["object_id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activities_objectId_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activities"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["instance_id"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activity_deliveries_instance_id_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["actor_id"],
+ "schemaTo": "public",
+ "tableTo": "actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "SET NULL",
+ "name": "activity_deliveries_actor_id_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["verification_key_id"],
+ "schemaTo": "public",
+ "tableTo": "key_versions",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "RESTRICT",
+ "name": "activity_deliveries_verification_key_id_key_versions_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "nameExplicit": true,
+ "columns": ["delivery_id", "actor_id"],
+ "schemaTo": "public",
+ "tableTo": "activity_delivery_actors",
+ "columnsTo": ["delivery_id", "actor_id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activity_delivery_actor_collections_link_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activity_delivery_actor_collections"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["delivery_id"],
+ "schemaTo": "public",
+ "tableTo": "activity_deliveries",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activity_delivery_actors_eZEmaeqml3qX_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["actor_id"],
+ "schemaTo": "public",
+ "tableTo": "actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activity_delivery_actors_actor_id_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["delivery_id"],
+ "schemaTo": "public",
+ "tableTo": "activity_deliveries",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "activity_delivery_attempts_BJqYxUSnVaXX_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["actor_id"],
+ "schemaTo": "public",
+ "tableTo": "actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "actor_collection_references_actorId_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "actor_collection_references"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["collection_id"],
+ "schemaTo": "public",
+ "tableTo": "collections",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "actor_collection_references_collectionId_collections_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "actor_collection_references"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "actors_id_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["local_id"],
+ "schemaTo": "public",
+ "tableTo": "local_actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "actors_localId_local_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["instance_id"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "actors_instanceId_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["successor_id"],
+ "schemaTo": "public",
+ "tableTo": "actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "SET NULL",
+ "name": "actors_successorId_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["source_id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "addressing_sourceId_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["target_id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "RESTRICT",
+ "name": "addressing_targetId_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["collection_id"],
+ "schemaTo": "public",
+ "tableTo": "collections",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "collection_items_collectionId_collections_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["item_id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "collection_items_itemId_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "collections_id_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["owner_actor_id"],
+ "schemaTo": "public",
+ "tableTo": "actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "collections_ownerActorId_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "collections"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["account_id"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["instance_id"],
+ "schemaTo": "public",
+ "tableTo": "instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "NO ACTION",
+ "name": "instance_members_instanceId_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["local_id"],
+ "schemaTo": "public",
+ "tableTo": "local_instances",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "instances_localId_local_instances_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "instances"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["key_id"],
+ "schemaTo": "public",
+ "tableTo": "keys",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "RESTRICT",
+ "name": "key_versions_key_id_keys_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["local_actor_id"],
+ "schemaTo": "public",
+ "tableTo": "local_actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "local_actor_keys_local_actor_id_local_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "local_actor_keys"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["account_id"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "login_tokens_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "login_challenges"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["id"],
+ "schemaTo": "public",
+ "tableTo": "resources",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "objects_id_resources_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["actor_id"],
+ "schemaTo": "public",
+ "tableTo": "actors",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "objects_actorId_actors_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "objects"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["account_id"],
+ "schemaTo": "public",
+ "tableTo": "accounts",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "name": "sessions_accountId_accounts_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "sessions"
+ },
+ {
+ "columns": ["delivery_id", "actor_id", "collection_iri"],
+ "nameExplicit": false,
+ "name": "activity_delivery_actor_collections_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "activity_delivery_actor_collections"
+ },
+ {
+ "columns": ["delivery_id", "actor_id"],
+ "nameExplicit": false,
+ "name": "activity_delivery_actors_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "columns": ["actor_id", "role"],
+ "nameExplicit": false,
+ "name": "actor_collection_references_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "actor_collection_references"
+ },
+ {
+ "columns": ["collection_id", "item_id"],
+ "nameExplicit": false,
+ "name": "collection_items_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "collection_items"
+ },
+ {
+ "columns": ["instance_id", "account_id"],
+ "nameExplicit": false,
+ "name": "instance_members_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "instance_members"
+ },
+ {
+ "columns": ["local_actor_id", "type"],
+ "nameExplicit": false,
+ "name": "local_actor_keys_pkey",
+ "entityType": "pks",
+ "schema": "public",
+ "table": "local_actor_keys"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "accounts_pkey",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "activities_pkey",
+ "schema": "public",
+ "table": "activities",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "activity_deliveries_pkey",
+ "schema": "public",
+ "table": "activity_deliveries",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "activity_delivery_attempts_pkey",
+ "schema": "public",
+ "table": "activity_delivery_attempts",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "actors_pkey",
+ "schema": "public",
+ "table": "actors",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "addressing_pkey",
+ "schema": "public",
+ "table": "addressing",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "collections_pkey",
+ "schema": "public",
+ "table": "collections",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "instances_pkey",
+ "schema": "public",
+ "table": "instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "key_versions_pkey",
+ "schema": "public",
+ "table": "key_versions",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "keys_pkey",
+ "schema": "public",
+ "table": "keys",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "local_actors_pkey",
+ "schema": "public",
+ "table": "local_actors",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "local_instances_pkey",
+ "schema": "public",
+ "table": "local_instances",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "login_tokens_pkey",
+ "schema": "public",
+ "table": "login_challenges",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "objects_pkey",
+ "schema": "public",
+ "table": "objects",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "resources_pkey",
+ "schema": "public",
+ "table": "resources",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "sessions_pkey",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "pks"
+ },
+ {
+ "nameExplicit": true,
+ "columns": ["username", "instance_id"],
+ "nullsNotDistinct": false,
+ "name": "username_key",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "nameExplicit": true,
+ "columns": ["source_id", "property", "position"],
+ "nullsNotDistinct": false,
+ "name": "addressing_source_property_position_key",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "addressing"
+ },
+ {
+ "nameExplicit": true,
+ "columns": ["key_id", "fingerprint"],
+ "nullsNotDistinct": false,
+ "name": "key_versions_key_id_fingerprint_unique",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["email"],
+ "nullsNotDistinct": false,
+ "name": "accounts_email_key",
+ "schema": "public",
+ "table": "accounts",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["local_id"],
+ "nullsNotDistinct": false,
+ "name": "actors_localId_key",
+ "schema": "public",
+ "table": "actors",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["host"],
+ "nullsNotDistinct": false,
+ "name": "instances_host_key",
+ "schema": "public",
+ "table": "instances",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["iri"],
+ "nullsNotDistinct": false,
+ "name": "keys_iri_key",
+ "schema": "public",
+ "table": "keys",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["slug"],
+ "nullsNotDistinct": false,
+ "name": "local_instances_slug_key",
+ "schema": "public",
+ "table": "local_instances",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["iri"],
+ "nullsNotDistinct": false,
+ "name": "resources_iri_key",
+ "schema": "public",
+ "table": "resources",
+ "entityType": "uniques"
+ },
+ {
+ "nameExplicit": false,
+ "columns": ["token_hash"],
+ "nullsNotDistinct": false,
+ "name": "sessions_tokenHash_key",
+ "schema": "public",
+ "table": "sessions",
+ "entityType": "uniques"
+ },
+ {
+ "value": "\"email\" ~ '^[^@]+@[^@]+\\.[^@]+$'",
+ "name": "accounts_email_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "\"max_instances\" >= 0",
+ "name": "accounts_max_instances_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "trim(both from \"name\") <> ''",
+ "name": "accounts_name_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "accounts"
+ },
+ {
+ "value": "(\"direction\" = 'inbound' AND \"status\" IN ('received', 'acknowledged', 'unverified', 'rejected')) OR (\"direction\" = 'outbound' AND \"status\" IN ('queued', 'sent', 'failed', 'permanently_failed', 'abandoned'))",
+ "name": "activity_deliveries_direction_status_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "value": "(\"completed\" IS NULL) = (\"status\" = 'queued')",
+ "name": "activity_deliveries_completed_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "value": "\"status_code\" IS NULL OR \"status_code\" BETWEEN 100 AND 599",
+ "name": "activity_deliveries_status_code_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "value": "\"direction\" <> 'outbound' OR \"verification_key_id\" IS NULL",
+ "name": "activity_deliveries_outbound_key_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "value": "(\"direction\" = 'inbound') = (\"verification_result\" IS NOT NULL)",
+ "name": "activity_deliveries_verification_result_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "value": "\"verification_mechanism\" IS NULL OR (\"direction\" = 'inbound' AND \"verification_result\" NOT IN ('unattempted', 'unobserved'))",
+ "name": "activity_deliveries_verification_mechanism_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "value": "(\"direction\" = 'inbound') = (\"body\" IS NOT NULL)",
+ "name": "activity_deliveries_body_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_deliveries"
+ },
+ {
+ "value": "\"inbox_owner\" OR \"addressed\" OR \"sender\"",
+ "name": "activity_delivery_actors_role_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "value": "NOT \"addressed_directly\" OR \"addressed\"",
+ "name": "activity_delivery_actors_addressed_directly_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_delivery_actors"
+ },
+ {
+ "value": "\"status_code\" IS NULL OR \"status_code\" BETWEEN 100 AND 599",
+ "name": "activity_delivery_attempts_status_code_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "value": "\"succeeded\" = (\"error\" IS NULL)",
+ "name": "activity_delivery_attempts_error_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "activity_delivery_attempts"
+ },
+ {
+ "value": "\"username\" NOT LIKE '%@%'",
+ "name": "actors_username_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "value": "\n \"suspended_until\" IS NULL OR (\n \"suspended\" IS NOT NULL AND\n \"suspended_until\" > \"suspended\"\n )\n ",
+ "name": "actors_suspended_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "actors"
+ },
+ {
+ "value": "\"last_seen\" >= \"first_seen\"",
+ "name": "key_versions_seen_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "value": "NOT (\"public_key\" ?| array['d','p','q','dp','dq','qi','oth','k'])",
+ "name": "key_versions_public_key_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "key_versions"
+ },
+ {
+ "value": "jsonb_typeof(\"public_key\") = 'object' AND NOT (\"public_key\" ?| array['d','p','q','dp','dq','qi','oth','k'])",
+ "name": "local_actor_keys_public_key_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_actor_keys"
+ },
+ {
+ "value": "\"slug\" ~ '^[a-z0-9][a-z0-9-]{2,61}[a-z0-9]$'\n AND (\"slug\" !~ '^..--' OR \"slug\" ~ '^xn--')",
+ "name": "local_instances_slug_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "value": "\"max_actors\" > 0",
+ "name": "instances_max_actors_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "local_instances"
+ },
+ {
+ "value": "trim(both from \"content_html\") <> ''",
+ "name": "objects_content_html_check",
+ "entityType": "checks",
+ "schema": "public",
+ "table": "objects"
+ }
+ ],
+ "renames": []
+}
diff --git a/packages/models/src/schema.ts b/packages/models/src/schema.ts
index 7cea79e..ecddcb3 100644
--- a/packages/models/src/schema.ts
+++ b/packages/models/src/schema.ts
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
+import type { webcrypto } from "node:crypto";
+
import { desc, sql } from "drizzle-orm";
import {
type AnyPgColumn,
@@ -337,6 +339,36 @@ export const localActors = pgTable("local_actors", {
export type LocalActor = typeof localActors.$inferSelect;
export type NewLocalActor = typeof localActors.$inferInsert;
+/** Signing algorithms available to local actors. */
+export const localActorKeyTypeEnum = pgEnum("local_actor_key_type", [
+ "RSASSA-PKCS1-v1_5",
+ "Ed25519",
+]);
+export type LocalActorKeyType =
+ (typeof localActorKeyTypeEnum.enumValues)[number];
+/** Private signing material; never expose this table through GraphQL. */
+export const localActorKeys = pgTable(
+ "local_actor_keys",
+ {
+ localActorId: uuid("local_actor_id")
+ .$type()
+ .notNull()
+ .references(() => localActors.id, { onDelete: "cascade" }),
+ type: localActorKeyTypeEnum().notNull(),
+ publicKey: jsonb("public_key").$type().notNull(),
+ privateKey: jsonb("private_key").$type().notNull(),
+ },
+ (table) => [
+ primaryKey({ columns: [table.localActorId, table.type] }),
+ check(
+ "local_actor_keys_public_key_check",
+ sql`jsonb_typeof(${table.publicKey}) = 'object' AND NOT (${table.publicKey} ?| array['d','p','q','dp','dq','qi','oth','k'])`,
+ ),
+ ],
+);
+export type LocalActorKey = typeof localActorKeys.$inferSelect;
+export type NewLocalActorKey = typeof localActorKeys.$inferInsert;
+
export const objectTypeEnum = pgEnum("object_type", ["Article", "Note"]);
export type ObjectType = (typeof objectTypeEnum.enumValues)[number];
/** ActivityPub objects authored by actors. */