fix: wire scheduler media resolver into publishing registry

The registry accepted media hooks but nothing supplied them, so Mastodon
media posts failed uploadAll (INVALID_POST attempts). PublishingMediaResolver
in the scheduler resolves workspace-scoped media_assets rows via RLS tenant
context and reads plaintext bytes from S3 with SPENC1 open (S3_KEK), matching
the api's S3Storage frame format.
This commit is contained in:
glass beadian 2026-10-15 18:44:18 +00:00
parent 9e8653aca5
commit b8115eec09
4 changed files with 107 additions and 34 deletions

View file

@ -0,0 +1,80 @@
/**
* Scheduler-side implementation of the publishing media seam: resolves a
* workspace-scoped media asset id to its row (via RLS tenant context) and its
* plaintext bytes (S3 download + SPENC1 open, identical to the api's
* S3Storage.getObject so either side reads what the other wrote).
*/
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import type { Pool } from "pg";
import type { MediaAsset } from "@tactical-media-broadcast-hub/core";
import { openMediaFrame, mediaKekFromEnv, type PublishingMediaHooks } from "@tactical-media-broadcast-hub/connector-runtime";
interface MediaAssetRow {
id: string;
workspace_id: string;
storage_key: string;
mime: string;
size_bytes: string | number;
created_at: Date;
}
export class PublishingMediaResolver implements PublishingMediaHooks {
private readonly pool: Pool;
private readonly client: S3Client;
private readonly bucket: string;
private readonly kek: Buffer;
constructor(pool: Pool) {
this.pool = pool;
this.bucket = process.env.S3_BUCKET ?? "tactical-media-broadcast-hub-media";
this.kek = mediaKekFromEnv(process.env);
this.client = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION ?? "us-east-1",
credentials:
process.env.S3_ACCESS_KEY !== undefined && process.env.S3_SECRET_KEY !== undefined
? { accessKeyId: process.env.S3_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_KEY }
: undefined,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE !== "false",
});
}
private async loadRow(workspaceId: string, assetId: string): Promise<MediaAssetRow> {
const client = await this.pool.connect();
try {
await client.query("BEGIN");
await client.query("SELECT set_config('app.workspace_id', $1, true)", [workspaceId]);
const result = await client.query<MediaAssetRow>(
"SELECT id, workspace_id, storage_key, mime, size_bytes, created_at FROM media_assets WHERE id = $1",
[assetId],
);
await client.query("COMMIT");
const row = result.rows[0];
if (row === undefined) throw new Error(`media asset ${assetId} not found in workspace ${workspaceId}`);
return row;
} finally {
client.release();
}
}
async loadAsset(workspaceId: string, assetId: string): Promise<MediaAsset> {
const row = await this.loadRow(workspaceId, assetId);
return {
id: row.id,
workspaceId: row.workspace_id,
storageKey: row.storage_key,
mimeType: row.mime,
sizeBytes: Number(row.size_bytes),
createdAt: new Date(row.created_at),
};
}
async openAsset(workspaceId: string, asset: MediaAsset): Promise<Uint8Array> {
await this.loadRow(workspaceId, asset.id);
const res = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: asset.storageKey }));
if (res.Body === undefined) throw new Error(`empty object body for ${asset.storageKey}`);
const stored = Buffer.from(await res.Body.transformToByteArray());
const plaintext = openMediaFrame(stored, this.kek);
return new Uint8Array(plaintext);
}
}

View file

@ -9,6 +9,7 @@ import pino from "pino";
import { createPublishingRegistry } from "@tactical-media-broadcast-hub/connector-runtime";
import { PublishingExecutionRepository, assertPublishingSchema, type AttemptRef } from "@tactical-media-broadcast-hub/publishing-execution";
import { PublishActivity } from "./activities/publish.activity.js";
import { PublishingMediaResolver } from "./media-resolver.js";
import { DirectorActivity } from "./director/director.activity.js";
import { DirectorHttpClient } from "./director/http.client.js";
import { HttpDirectorStatePort, HttpDesiredStatePort } from "./director/adapters.js";
@ -61,7 +62,7 @@ export async function main(): Promise<void> {
try {
await assertPublishingSchema(pool);
const repository = new PublishingExecutionRepository(pool);
const registry = createPublishingRegistry(pool);
const registry = createPublishingRegistry(pool, new PublishingMediaResolver(pool));
native = await NativeConnection.connect({ address });
connection = await Connection.connect({ address });
const temporal = new Client({ connection, namespace });