The Instagram connector existed but was never registered, so Channels showed it as "provider unavailable" and publishing could not resolve it. - Register createInstagramConnector in the connections registry and the publishing registry; add the connector-runtime publishing policy (idempotency "none": media_publish has no key, never replay). - The Facebook app credentials identify the Hub, not a collective: read them only from INSTAGRAM_CLIENT_ID / INSTAGRAM_CLIENT_SECRET. Drop the clientId/clientSecret connect fields and stop copying the app secret into each connection's vault entry. The provider is connectable only when both are set (connectEnabled + connectNote). - Fail the connect when several Instagram accounts are granted instead of silently publishing as the first Page's account. - Fix the connector package's pre-existing lint errors (it now ships in api). - Tests: contract cases for env-only credentials and the multi-account refusal; connections integration case for provider gating and the Facebook dialog redirect. - docs/oauth-connect.md: Instagram setup, callback URI and limits. Not in this change: publishing images needs a public plaintext JPEG URL that Meta downloads, and storage is client-side encrypted, so presigned S3 URLs cannot serve it. Until a short-lived media endpoint exists, publishing to Instagram fails with an explicit error. Token refresh via ig_refresh_token on graph.facebook.com also needs checking against Facebook Login tokens.
50 lines
2.4 KiB
TypeScript
50 lines
2.4 KiB
TypeScript
import type { ConnectorConnection } from "@tactical-media-broadcast-hub/core";
|
|
/**
|
|
* Provider publishing policies, declared at the composition boundary so the
|
|
* SDK publish/deletePublication signatures stay unchanged.
|
|
*/
|
|
export interface PublishingPolicy {
|
|
/**
|
|
* "token": the provider dedupes repeated sends carrying the same
|
|
* idempotency key, so an ambiguous send may be replayed with that key.
|
|
* "none": no key and no lookup — an ambiguous send is never replayed.
|
|
*/
|
|
idempotency: "token" | "none";
|
|
/**
|
|
* How long after the attempt's first dispatch a same-key replay is trusted.
|
|
* Applies only to "token" policies and only on deployment-verified origins.
|
|
*/
|
|
dedupeWindowMs?: number;
|
|
}
|
|
|
|
/**
|
|
* Mastodon documents Idempotency-Key retention of up to one hour; 30 minutes
|
|
* from first dispatch is the deployment cutoff (forks may keep less).
|
|
*/
|
|
export const MASTODON_DEDUPE_WINDOW_MS = 30 * 60 * 1000;
|
|
|
|
export const PUBLISHING_POLICIES: Readonly<Record<string, PublishingPolicy>> = {
|
|
mastodon: { idempotency: "token", dedupeWindowMs: MASTODON_DEDUPE_WINDOW_MS },
|
|
telegram: { idempotency: "none" },
|
|
// Graph media_publish takes no idempotency key: an ambiguous send is never replayed.
|
|
instagram: { idempotency: "none" },
|
|
};
|
|
|
|
/** Unknown providers get the safe default: never replay an ambiguous send. */
|
|
export function publishingPolicyFor(provider: string): PublishingPolicy {
|
|
return PUBLISHING_POLICIES[provider] ?? { idempotency: "none" };
|
|
}
|
|
|
|
export function verifiedDedupeOrigin(origin: string, configured = process.env.MASTODON_PUBLISH_DEDUPE_ORIGINS ?? ""): boolean {
|
|
return configured.split(",").map((value) => value.trim()).filter(Boolean).some((value) => {
|
|
try { const url = new URL(value); return url.protocol === "https:" && url.origin === value && url.origin === origin; } catch { return false; }
|
|
});
|
|
}
|
|
|
|
export function isPublishingProvider(provider: string): boolean { return Object.hasOwn(PUBLISHING_POLICIES, provider); }
|
|
export function publishingDispatchPolicy(connection: ConnectorConnection): { policy: PublishingPolicy; origin: string; dedupeVerified: boolean } {
|
|
const policy = publishingPolicyFor(connection.provider);
|
|
let origin = "";
|
|
try { origin = new URL(connection.fields.instanceUrl ?? "").origin; } catch { /* invalid origins never authorize dedupe */ }
|
|
return { policy, origin, dedupeVerified: policy.idempotency === "token" && verifiedDedupeOrigin(origin) };
|
|
}
|