feat(federation): schema for outbound peering gate (direction column, subscribers, notifications)
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
CREATE TABLE `peer_approval_notifications` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`kind` text NOT NULL,
|
||||||
|
`peer_origin` text NOT NULL,
|
||||||
|
`trigger_reason` text NOT NULL,
|
||||||
|
`trigger_target` text NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`read_at` integer,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE `peer_approval_subscribers` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`request_id` text NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`trigger_reason` text NOT NULL,
|
||||||
|
`trigger_target` text NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`request_id`) REFERENCES `peer_approval_requests`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_peer_approval_notifications_user_id` ON `peer_approval_notifications` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_peer_approval_subscribers_user_id` ON `peer_approval_subscribers` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `peer_approval_subscribers_request_id_user_id_trigger_reason_trigger_target_unique` ON `peer_approval_subscribers` (`request_id`,`user_id`,`trigger_reason`,`trigger_target`);--> statement-breakpoint
|
||||||
|
-- Recreate peer_approval_requests:
|
||||||
|
-- - add direction column (default 'inbound') so existing rows classify correctly
|
||||||
|
-- - relax hmac_secret to nullable (outbound rows generate fresh on approval)
|
||||||
|
-- - drop UNIQUE(origin); add UNIQUE(origin, direction)
|
||||||
|
-- - add CHECK enforcing inbound rows always carry hmac_secret
|
||||||
|
CREATE TABLE `__new_peer_approval_requests` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`origin` text NOT NULL,
|
||||||
|
`direction` text DEFAULT 'inbound' NOT NULL,
|
||||||
|
`instance_name` text,
|
||||||
|
`hmac_secret` text,
|
||||||
|
`requested_at` integer NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`approval_token` text,
|
||||||
|
CHECK (
|
||||||
|
(direction = 'inbound' AND hmac_secret IS NOT NULL)
|
||||||
|
OR (direction = 'outbound')
|
||||||
|
)
|
||||||
|
);--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_peer_approval_requests` (`id`, `origin`, `direction`, `instance_name`, `hmac_secret`, `requested_at`, `expires_at`, `approval_token`)
|
||||||
|
SELECT `id`, `origin`, 'inbound', `instance_name`, `hmac_secret`, `requested_at`, `expires_at`, `approval_token` FROM `peer_approval_requests`;--> statement-breakpoint
|
||||||
|
DROP TABLE `peer_approval_requests`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_peer_approval_requests` RENAME TO `peer_approval_requests`;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `peer_approval_requests_origin_direction_unique` ON `peer_approval_requests` (`origin`,`direction`);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,13 @@
|
|||||||
"when": 1777196627239,
|
"when": 1777196627239,
|
||||||
"tag": "0002_peer_approval_token",
|
"tag": "0002_peer_approval_token",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1777229997210,
|
||||||
|
"tag": "0003_brave_inhumans",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -379,13 +379,41 @@ export const federationPeers = sqliteTable('federation_peers', {
|
|||||||
|
|
||||||
export const peerApprovalRequests = sqliteTable('peer_approval_requests', {
|
export const peerApprovalRequests = sqliteTable('peer_approval_requests', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
origin: text('origin').notNull().unique(),
|
origin: text('origin').notNull(),
|
||||||
|
direction: text('direction').notNull().default('inbound'),
|
||||||
instanceName: text('instance_name'),
|
instanceName: text('instance_name'),
|
||||||
hmacSecret: text('hmac_secret').notNull(),
|
hmacSecret: text('hmac_secret'),
|
||||||
requestedAt: integer('requested_at').notNull(),
|
requestedAt: integer('requested_at').notNull(),
|
||||||
expiresAt: integer('expires_at').notNull(),
|
expiresAt: integer('expires_at').notNull(),
|
||||||
approvalToken: text('approval_token'),
|
approvalToken: text('approval_token'),
|
||||||
});
|
}, (table) => ({
|
||||||
|
uniqOriginDirection: unique().on(table.origin, table.direction),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const peerApprovalSubscribers = sqliteTable('peer_approval_subscribers', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
requestId: text('request_id').notNull().references(() => peerApprovalRequests.id, { onDelete: 'cascade' }),
|
||||||
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
triggerReason: text('trigger_reason').notNull(),
|
||||||
|
triggerTarget: text('trigger_target').notNull(),
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
uniqSubscription: unique().on(table.requestId, table.userId, table.triggerReason, table.triggerTarget),
|
||||||
|
userIdx: index('idx_peer_approval_subscribers_user_id').on(table.userId),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const peerApprovalNotifications = sqliteTable('peer_approval_notifications', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||||
|
kind: text('kind').notNull(),
|
||||||
|
peerOrigin: text('peer_origin').notNull(),
|
||||||
|
triggerReason: text('trigger_reason').notNull(),
|
||||||
|
triggerTarget: text('trigger_target').notNull(),
|
||||||
|
createdAt: integer('created_at').notNull(),
|
||||||
|
readAt: integer('read_at'),
|
||||||
|
}, (table) => ({
|
||||||
|
userIdx: index('idx_peer_approval_notifications_user_id').on(table.userId),
|
||||||
|
}));
|
||||||
|
|
||||||
export const federationOutbox = sqliteTable('federation_outbox', {
|
export const federationOutbox = sqliteTable('federation_outbox', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
|
|||||||
@@ -1370,6 +1370,15 @@ export async function federationRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
return reply.code(404).send({ error: 'Approval request not found', statusCode: 404 });
|
return reply.code(404).send({ error: 'Approval request not found', statusCode: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Outbound rows have no hmac_secret and no remote /peer/denied endpoint to call.
|
||||||
|
// Direction-branched handling lands in Task 7; until then, inbound is the only path here.
|
||||||
|
if (!approvalReq.hmacSecret) {
|
||||||
|
return reply.code(400).send({
|
||||||
|
error: 'Cannot deny an outbound peering request via this endpoint yet — outbound denial handling is not implemented.',
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ourOrigin = getOurOrigin();
|
const ourOrigin = getOurOrigin();
|
||||||
const denialBody = JSON.stringify({
|
const denialBody = JSON.stringify({
|
||||||
origin: ourOrigin,
|
origin: ourOrigin,
|
||||||
|
|||||||
@@ -452,6 +452,13 @@ export async function cleanupExpiredApprovalRequests(): Promise<number> {
|
|||||||
let cleaned = 0;
|
let cleaned = 0;
|
||||||
|
|
||||||
for (const req of expired) {
|
for (const req of expired) {
|
||||||
|
// Outbound rows have no hmac_secret and no remote /peer/denied endpoint;
|
||||||
|
// their expiry handling (subscriber notifications) lands in Task 9.
|
||||||
|
// Until then, skip outbound rows here so this loop only processes inbound expirations.
|
||||||
|
if (!req.hmacSecret) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const denialBody = JSON.stringify({
|
const denialBody = JSON.stringify({
|
||||||
origin: ourOrigin,
|
origin: ourOrigin,
|
||||||
reason: 'expired' as const,
|
reason: 'expired' as const,
|
||||||
|
|||||||
Reference in New Issue
Block a user