@postdom/mcp
Version:
Postdom MCP server for agent-led social publishing and performance measurement.
3,675 lines • 160 kB
JavaScript
// src/index.ts
import { randomUUID } from "crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
// ../core/src/platform.ts
import { z } from "zod";
var corePlatformSchema = z.enum([
"tiktok",
"instagram",
"youtube",
"facebook",
"twitter",
"linkedin",
"bluesky",
"snapchat",
"threads"
]);
var publicDestinationSchema = z.enum([
"tiktok",
"instagram",
"youtube",
"linkedin",
"facebook",
"twitter",
"snapchat",
// Claimed on all three links, each cited first-party to Meta and none to the supplier.
//
// connect the Authorization Window at threads.net/oauth/authorize, taking client_id,
// redirect_uri, scope and response_type=code - an ordinary authorization-code
// redirect, recorded in connect-path-threads-2026-09-03.md.
// publish Meta's own posting reference: 300s, 0.01:1-10:1, 1920 horizontal, MOV/MP4, all
// re-read 2026-09-03, plus the 500-character `text` ceiling, which is cited AND
// enforced rather than only recorded.
// claim this line.
//
// Its nine metrics stay `unverified`, so it does not join MEASURED_DESTINATIONS and
// /product/performance does not move. That is the distinction this file already draws for
// LinkedIn: a destination with no measurable metric is claimed and says so, and the copy
// narrows the sentence rather than the destination list.
"threads",
// The ninth, and the last. Its publishing evidence is the strongest in this file - commit-pinned
// app.bsky.video.startUpload, app.bsky.embed.video and app.bsky.feed.post lexicons, machine-
// readable rather than prose - and its 300-grapheme caption ceiling is enforced, not just recorded.
//
// WHAT IS NOT ESTABLISHED, stated because it is the trade rather than an oversight: no Bluesky
// connection has been observed end to end. The hold that used to sit here required one before
// offering, and it was circular - the form only renders once bluesky is offered. Dean removed it
// knowingly. Facebook, X and Snapchat were each claimed on documentation with no observed
// connection either, and Bluesky is on stronger evidence than any of them. If the app-password
// flow fails, the first customer to try it finds out.
"bluesky"
]);
var CONNECT_DESTINATIONS = [
"instagram",
"tiktok",
"youtube",
"linkedin",
// OAuth 2.0, standard redirect, so connectStart carries it with no
// per-platform code. Verified 2026-09-03 against the provider platform page.
"facebook",
// OAuth 2.0 redirect, so connectStart carries it - but see
// CONNECT_PREREQUISITES below, because authorising is not sufficient here.
// Verified 2026-09-03 against the provider platform page.
"snapchat",
// Also OAuth 2.0 with a standard redirect. The dm.read and dm.write scopes the
// provider documents are for direct messaging and are not required to publish -
// worth recording because X's DM limits have already been mistaken for publish
// limits on this codebase once, when a 140-second ceiling that belonged to DMs
// was enforced against video posts. Verified 2026-09-03 against the provider
// platform page.
"twitter",
// OAuth 2.0 with a standard redirect, and no selection step of its own. Meta documents an
// Authorization Window at threads.net/oauth/authorize taking client_id, redirect_uri, scope and
// response_type=code, so connectStart returns an authUrl for it with no per-platform code. The
// provider's OpenAPI also carries `threads` in the path enum of GET /v1/connect/{platform} and
// exposes no /v1/connect/threads/* endpoint to complete afterwards - unlike Snapchat, which has
// a profile picker, so Threads needs no CONNECT_PREREQUISITES entry.
//
// Verified 2026-09-03 against Meta's own platform page, not against the enum: the enum alone is
// the matrix-row standard connect-paths-2026-09-03.md declined for Bluesky and Snapchat, and
// this entry was originally admitted on it. See connect-path-threads-2026-09-03.md, which also
// records why App Review and the private-profile token lifetime are not prerequisites.
//
// Connectable and deliberately not claimed. publicDestinationSchema is untouched by this
// change: Threads clears the connect and publish links of the bar in that docblock, and
// claiming it is a separate decision taken separately, which is the order every destination
// before it followed.
"threads",
// The only entry here that is not a redirect. Its connect path is a route, a provider method
// and an app-password form rather than a list entry, and all three shipped in #447; what kept
// it out of this list afterwards was a hold, not a gap.
//
// The hold was "no customer is offered it until a round trip has been observed", and it could
// never be discharged: the form only renders once bluesky is in this list, so the observation
// required the button it was gating. Dean removed it knowingly - see CONNECT_MECHANISMS below.
"bluesky"
];
var CONNECT_MECHANISMS = {
instagram: "redirect",
tiktok: "redirect",
youtube: "redirect",
linkedin: "redirect",
// Verified in #444 against the provider's own platform page.
facebook: "redirect",
// Verified in #446. The Public Profile requirement is a precondition, above.
snapchat: "redirect",
// Verified in #445 against the provider's own platform page.
twitter: "redirect",
// Verified 2026-09-03 against Meta's own Authorization Window documentation - an ordinary
// authorization-code redirect. See connect-path-threads-2026-09-03.md.
threads: "redirect",
// The only one that is not a redirect. See the note above and BUILD-25.
bluesky: "app_password"
};
function connectMechanism(destination) {
return CONNECT_MECHANISMS[destination];
}
var redirectOnly = CONNECT_DESTINATIONS.filter((destination) => connectMechanism(destination) === "redirect");
if (redirectOnly.length === 0) {
throw new Error(
"No offered destination authorises by redirect. Every surface that renders a Connect button derives its list from REDIRECT_DESTINATIONS, so an empty set would render nothing rather than fail - decide what those surfaces should show before emptying it."
);
}
var REDIRECT_DESTINATIONS = redirectOnly;
var CREDENTIAL_DESTINATIONS = CONNECT_DESTINATIONS.filter((destination) => connectMechanism(destination) !== "redirect");
var PLATFORM_LABELS = {
tiktok: "TikTok",
instagram: "Instagram",
youtube: "YouTube",
facebook: "Facebook",
twitter: "X",
linkedin: "LinkedIn",
bluesky: "Bluesky",
snapchat: "Snapchat",
threads: "Threads"
};
function platformLabel(value) {
const parsed = corePlatformSchema.safeParse(value);
if (parsed.success) return PLATFORM_LABELS[parsed.data];
return value.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
}
var policyVisibilityOptions = {
tiktok: [
"PUBLIC_TO_EVERYONE",
"MUTUAL_FOLLOW_FRIENDS",
"FOLLOWER_OF_CREATOR",
"SELF_ONLY"
],
instagram: ["account_default"],
youtube: ["public", "private", "unlisted"],
facebook: ["account_default"],
twitter: ["account_default"],
// Not the Facebook case. LinkedIn's Posts API takes visibility as a required
// field, so a value is always sent; the provider simply does not expose it,
// which means it is chosen on our behalf and we can neither set nor read it.
// "account_default" is right because it says Postdom does not choose - but
// nothing may render "this destination exposes no visibility control" for
// LinkedIn, because that is false.
linkedin: ["account_default"],
// Unlike LinkedIn, this is the genuinely-absent case. The AT Protocol post
// record carries no visibility field: an app.bsky.feed.post is public, and
// reply gating lives in separate threadgate/postgate records rather than in
// the post. So "account_default" here means there is nothing to choose, and a
// surface may say so - which it may not for LinkedIn.
bluesky: ["account_default"],
// LinkedIn's case rather than Bluesky's, and deliberately the weaker claim.
// The Spotlight endpoint documents media_id, skip_save_to_profile,
// description and locale, and no visibility parameter - so Postdom does not
// choose. That is a fact about the API's parameters, not about Snapchat: I
// have not verified that Snap exposes no visibility control anywhere, and
// "the endpoint takes no such field" does not establish "there is nothing to
// choose". Unknown is not unsupported, so nothing may render "this
// destination exposes no visibility control" for Snapchat.
snapchat: ["account_default"],
// The Threads publishing endpoint documents `topic_tag` - discoverability, not who can see the
// post - and no visibility parameter, so "account_default" says Postdom does not choose.
// See THREADS_POSTS_EVIDENCE.
//
// An earlier version of this comment said Meta documents `reply_control` here. It does not.
// That phrase comes from the provider's platform matrix, which credits Threads with "Reply
// controls" meaning *inbox management* - replying to, deleting and hiding existing replies -
// not a publish-time setting. docs/verification/destination-expansion-cost-2026-09-02.md records
// exactly that, so this file was asserting a Meta fact our own verification record refutes,
// in prose and with no link to check.
//
// Nothing may render "this destination exposes no visibility control" - but for Threads' own
// reason, not LinkedIn's and not Snapchat's, and the difference is why this is spelled out
// rather than deferred to them. LinkedIn's is a required field chosen on our behalf; Snapchat's
// is unknown-not-unsupported. Threads' is that the control exists and sits one level up: Meta
// documents public and private Threads profiles, and a post inherits its audience from the
// profile. That is Bluesky's opposite, where every app.bsky.feed.post is public and there is
// genuinely nothing to choose.
//
// docs/verification/destination-expansion-cost-2026-09-02.md put Threads in Class A - "the
// platform has no visibility concept for this surface" - alongside Bluesky. That is the
// record that is wrong, and it has been corrected there rather than deferred to here: Threads
// is Class B by structure, visibility real, inherited, and not ours to set.
threads: ["account_default"]
};
var AUTONOMY_DEFAULT_MAXIMUM_POSTS_PER_DAY = 3;
var AUTONOMY_DEFAULT_QUIET_HOURS_START = "23:00";
var AUTONOMY_DEFAULT_QUIET_HOURS_END = "07:00";
var policyVisibilityValues = [
...new Set(
Object.values(policyVisibilityOptions).flat()
)
];
if (policyVisibilityValues.length === 0) {
throw new Error(
"policyVisibilityOptions produced no visibility values. policyVisibilitySchema is derived from that map, so an empty set would accept nothing rather than fail."
);
}
var policyVisibilitySchema = z.enum(
policyVisibilityValues
);
var PLATFORM_LIMITS_VERIFIED_AT = "2026-08-21T00:00:00.000Z";
var SHORT_FORM_LIMITS_VERIFIED_AT = "2026-08-29T00:00:00.000Z";
var PLATFORM_LIMITS_EVIDENCE = "ARCHITECTURE.md#4-data-model";
var TIKTOK_MEDIA_TRANSFER_EVIDENCE = "https://developers.tiktok.com/docs/en/content-posting-api-media-transfer-guide";
var TIKTOK_CREATIVE_RECOMMENDATIONS_EVIDENCE = "https://ads.tiktok.com/resources/help/article/creative-best-practices";
var INSTAGRAM_REELS_PUBLISHING_EVIDENCE = "https://github.com/fbsamples/reels_publishing_apis/blob/main/insta_reels_publishing_api_sample/README.md";
var YOUTUBE_SHORTS_QUALIFICATION_EVIDENCE = "https://support.google.com/youtube/answer/15424877";
var YOUTUBE_ENCODING_RECOMMENDATIONS_EVIDENCE = "https://support.google.com/youtube/answer/1722171";
var YOUTUBE_SUPPORTED_FORMATS_EVIDENCE = "https://support.google.com/youtube/troubleshooter/2888402";
var FACEBOOK_REELS_EVIDENCE = "https://developers.facebook.com/docs/video-api/guides/reels-publishing";
var FACEBOOK_LIMITS_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
var LINKEDIN_VIDEOS_API_EVIDENCE = "https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/videos-api";
var LINKEDIN_PAGES_SPEC_EVIDENCE = "https://www.linkedin.com/help/linkedin/answer/a1311816";
var LINKEDIN_SHARING_SPEC_EVIDENCE = "https://www.linkedin.com/help/linkedin/answer/a548372";
var LINKEDIN_LIMITS_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
var BLUESKY_EMBED_LEXICON_EVIDENCE = "https://github.com/bluesky-social/atproto/blob/1f3f67c4b5ba/lexicons/app/bsky/embed/video.json";
var BLUESKY_UPLOAD_LEXICON_EVIDENCE = "https://github.com/bluesky-social/atproto/blob/c26b463a3612/lexicons/app/bsky/video/startUpload.json";
var BLUESKY_POST_LEXICON_EVIDENCE = "https://github.com/bluesky-social/atproto/blob/41a561e80a6c/lexicons/app/bsky/feed/post.json";
var BLUESKY_PROVIDER_MEDIA_EVIDENCE = "https://docs.zernio.com/platforms/bluesky";
var BLUESKY_PROVIDER_COMPRESSION_EVIDENCE = "https://docs.zernio.com/guides/media-uploads";
var BLUESKY_LIMITS_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
var SNAPCHAT_SPOTLIGHT_API_EVIDENCE = "https://developers.snap.com/api/marketing-api/Public-Profile-API/ProfileAssetManagement";
var SNAPCHAT_LIMITS_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
var THREADS_POSTS_EVIDENCE = "https://developers.facebook.com/docs/threads/posts";
var THREADS_LIMITS_VERIFIED_AT = "2026-09-03T00:00:00.000Z";
var TWITTER_EVIDENCE = "https://docs.x.com/fundamentals/counting-characters";
var TWITTER_MEDIA_EVIDENCE = "https://docs.x.com/x-api/media/quickstart/best-practices";
var TWITTER_LIMITS_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
var TWITTER_MEDIA_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
function verifiedLimit(value, evidence = PLATFORM_LIMITS_EVIDENCE, verifiedAt = PLATFORM_LIMITS_VERIFIED_AT) {
return {
value,
verified_at: verifiedAt,
evidence
};
}
function classifiedLimit(value, evidence, classification = "enforced", verifiedAt = SHORT_FORM_LIMITS_VERIFIED_AT) {
return {
value,
verified_at: verifiedAt,
evidence,
classification
};
}
function recommendedLimit(value, evidence, verifiedAt = SHORT_FORM_LIMITS_VERIFIED_AT) {
return classifiedLimit(value, evidence, "recommendation", verifiedAt);
}
function unpublishedLimit(evidence, verifiedAt = SHORT_FORM_LIMITS_VERIFIED_AT) {
return classifiedLimit(null, evidence, "not_published", verifiedAt);
}
var platformLimits = {
global: {
caption_max_chars: verifiedLimit(4e3),
media_max_bytes: verifiedLimit(500 * 1024 * 1024),
video_max_seconds: verifiedLimit(10 * 60),
video_mime_types: verifiedLimit(["video/mp4", "video/quicktime"])
},
tiktok: {
video_min_seconds: verifiedLimit(3),
video_max_seconds: verifiedLimit(10 * 60)
},
instagram: {
caption_max_chars: verifiedLimit(2200),
video_max_seconds: verifiedLimit(90)
},
youtube: {
title_max_chars: verifiedLimit(100),
video_max_seconds: verifiedLimit(3 * 60),
aspect_ratio: verifiedLimit("9:16")
},
facebook: {
// "3 to 90 seconds", from Meta's specification table. Was 60, from the
// provider's stale copy.
video_min_seconds: verifiedLimit(3, FACEBOOK_REELS_EVIDENCE, FACEBOOK_LIMITS_VERIFIED_AT),
video_max_seconds: verifiedLimit(90, FACEBOOK_REELS_EVIDENCE, FACEBOOK_LIMITS_VERIFIED_AT)
},
twitter: {
caption_max_chars: verifiedLimit(280, TWITTER_EVIDENCE, TWITTER_LIMITS_VERIFIED_AT),
// 20 minutes: X's "Video on a Post" row for a default account. Was 140
// seconds, which is X's row for video in a *direct message*.
//
// Well above Postdom's own 600-second global maximum, and recorded anyway.
// This snapshot states what the platform publishes; the global cap states
// what Postdom accepts. Collapsing the two would leave no way to tell a
// Postdom product decision from a platform rule, which is the distinction
// the whole file exists to keep.
video_max_seconds: verifiedLimit(20 * 60, TWITTER_MEDIA_EVIDENCE, TWITTER_MEDIA_VERIFIED_AT)
},
// No compatibility-snapshot facts for LinkedIn, and deliberately none.
//
// A commentary limit provably exists: the versioned Posts API (li-lms-2026-08,
// updated 2026-05-13) lists FIELD_LENGTH_TOO_LONG - "{field} length exceeds
// the allowed maximum. Reduce the length of the commentary" - in its error
// table. It publishes no number. The figure that circulates is 3,000, and it
// is not on that page or any other versioned one, so recording it would be a
// guess wearing a citation. Same shape as LinkedIn's video duration and
// Bluesky's: the constraint is proven, the value is not published.
//
// The global ceiling therefore binds the preflight, and approvalPreflight
// reports the row as informational rather than passed so the absence of a
// classified fact is never rendered as a verdict for LinkedIn.
linkedin: {},
bluesky: {
// Unlike LinkedIn, Bluesky publishes its caption ceiling, first-party and
// machine-readable: app.bsky.feed.post caps `text` at maxGraphemes 300 and
// maxLength 3000. Two separate ceilings, and 300 is the one that binds in
// practice.
//
// Postdom measures captions with JavaScript string length, which counts
// UTF-16 code units rather than graphemes. That mismatch is safe in exactly
// one direction and it is the safe one: a string's grapheme count is never
// greater than its UTF-16 length, so length <= 300 always satisfies both
// lexicon caps and no violating caption can pass. The cost is over-
// rejection of grapheme-dense captions - an emoji sequence counts as
// several units and one grapheme - which is the conservative failure.
//
// Recorded rather than omitted because it is reachable: the global ceiling
// is 4,000, so without this the approval card reports Bluesky unclassified
// and lets a 3,000-character caption reach a destination that takes 300.
caption_max_chars: verifiedLimit(300, BLUESKY_POST_LEXICON_EVIDENCE, BLUESKY_LIMITS_VERIFIED_AT)
},
snapchat: {
// "The spotlight's description can be up to a 160 characters", stated on the
// endpoint's own page. Tighter than every other destination Postdom carries
// and far below the 4,000 global ceiling, so recording it is what stops the
// approval card reporting Snapchat unclassified and letting a caption
// thirty times over the limit reach it.
//
// Counted in characters, and Snap does not say which unit it means. Postdom
// measures with JavaScript string length, which counts UTF-16 code units -
// the same conservative mismatch documented for Bluesky above, and safe in
// the same direction for the same reason.
caption_max_chars: verifiedLimit(
160,
SNAPCHAT_SPOTLIGHT_API_EVIDENCE,
SNAPCHAT_LIMITS_VERIFIED_AT
)
},
// Empty on purpose. Meta's Threads reference says "Text posts are limited to 500 characters",
// and says it once, in a Limitations section scoped to text posts. It never states whether a
// post carrying video shares that ceiling. Recording 500 here would read a text-post limit onto
// a media post - the same scoping error that put LinkedIn's company-Page 10-minute figure on
// personal posts and produced three false "never" claims on Facebook and X.
//
// The cost of the gap is bounded and known: the global 4,000 ceiling applies, so a caption
// between 500 and 4,000 reaches Meta and may be refused there. That is a worse failure than
// gating it, but a gate on a limit we cannot show applies is a claim we cannot support, and the
// approval card reports Threads unclassified rather than asserting a number.
threads: {}
};
var shortFormVideoLimits = {
tiktok: {
video_min_seconds: unpublishedLimit(TIKTOK_MEDIA_TRANSFER_EVIDENCE),
video_max_seconds: classifiedLimit(10 * 60, TIKTOK_MEDIA_TRANSFER_EVIDENCE),
video_mime_types: classifiedLimit(
["video/mp4", "video/quicktime"],
TIKTOK_MEDIA_TRANSFER_EVIDENCE
),
resolution_min_pixels: classifiedLimit(
{ width: 360, height: 360 },
TIKTOK_MEDIA_TRANSFER_EVIDENCE
),
resolution_max_pixels: classifiedLimit(
{ width: 4096, height: 4096 },
TIKTOK_MEDIA_TRANSFER_EVIDENCE
),
resolution_recommended: recommendedLimit(
{ quality: "720p_or_higher" },
TIKTOK_CREATIVE_RECOMMENDATIONS_EVIDENCE
),
aspect_ratio: unpublishedLimit(TIKTOK_MEDIA_TRANSFER_EVIDENCE),
account_maximum_varies: classifiedLimit(true, TIKTOK_MEDIA_TRANSFER_EVIDENCE),
aspect_ratio_recommended: recommendedLimit(
"9:16",
TIKTOK_CREATIVE_RECOMMENDATIONS_EVIDENCE
)
},
instagram: {
video_min_seconds: classifiedLimit(3, INSTAGRAM_REELS_PUBLISHING_EVIDENCE),
video_max_seconds: classifiedLimit(15 * 60, INSTAGRAM_REELS_PUBLISHING_EVIDENCE),
video_mime_types: classifiedLimit(
["video/mp4", "video/quicktime"],
INSTAGRAM_REELS_PUBLISHING_EVIDENCE
),
resolution_min_pixels: unpublishedLimit(INSTAGRAM_REELS_PUBLISHING_EVIDENCE),
resolution_max_horizontal_pixels: classifiedLimit(
1920,
INSTAGRAM_REELS_PUBLISHING_EVIDENCE
),
resolution_recommended: unpublishedLimit(INSTAGRAM_REELS_PUBLISHING_EVIDENCE),
aspect_ratio: classifiedLimit(
{ minimum: 0.01, maximum: 10 },
INSTAGRAM_REELS_PUBLISHING_EVIDENCE
),
aspect_ratio_recommended: recommendedLimit(
"9:16",
INSTAGRAM_REELS_PUBLISHING_EVIDENCE
)
},
youtube: {
video_min_seconds: unpublishedLimit(YOUTUBE_SHORTS_QUALIFICATION_EVIDENCE),
video_max_seconds: classifiedLimit(3 * 60, YOUTUBE_SHORTS_QUALIFICATION_EVIDENCE),
video_mime_types: classifiedLimit(
["video/mp4", "video/quicktime"],
YOUTUBE_SUPPORTED_FORMATS_EVIDENCE
),
resolution_min_pixels: unpublishedLimit(YOUTUBE_ENCODING_RECOMMENDATIONS_EVIDENCE),
resolution_recommended: unpublishedLimit(YOUTUBE_ENCODING_RECOMMENDATIONS_EVIDENCE),
aspect_ratio: classifiedLimit(
{ maximum_width_to_height_ratio: 1 },
YOUTUBE_SHORTS_QUALIFICATION_EVIDENCE
),
aspect_ratio_recommended: unpublishedLimit(YOUTUBE_SHORTS_QUALIFICATION_EVIDENCE),
shorts_qualification: classifiedLimit(
{
orientation: "square_or_vertical",
maximum_duration_seconds: 3 * 60,
applies_to_uploads_on_or_after: "2024-10-15"
},
YOUTUBE_SHORTS_QUALIFICATION_EVIDENCE
)
},
facebook: {
video_min_seconds: classifiedLimit(3, FACEBOOK_REELS_EVIDENCE, "enforced", FACEBOOK_LIMITS_VERIFIED_AT),
video_max_seconds: classifiedLimit(90, FACEBOOK_REELS_EVIDENCE, "enforced", FACEBOOK_LIMITS_VERIFIED_AT),
// Meta lists ".mp4 (recommended)" and no other container. It was recorded
// here as an enforced [mp4, quicktime] on the provider's page, which is two
// claims Meta does not make: that it accepts QuickTime, and that the pair is
// a rule rather than a preference. Downgraded to what Meta actually says.
//
// No gate weakens. The Facebook preflight branch reads durations only, and
// the global content_type enum - which does accept both - is what a request
// is checked against.
video_mime_types: recommendedLimit(
["video/mp4"],
FACEBOOK_REELS_EVIDENCE,
FACEBOOK_LIMITS_VERIFIED_AT
),
// Meta's published floor - "Minimum is 540 x 960 pixels" - and its aspect
// ratio bound have moved to documentedMediaFacts.facebook below. Both were
// sitting here as not_published, which was true of the provider's page and
// is false of Meta's.
//
// They move rather than change classification because this registry has no
// honest label for them: "enforced" promises the preflight reads the fact
// and it does not, "recommendation" would demote a stated requirement to a
// preference, and "not_published" is the falsehood being fixed. That is
// what documentedMediaFacts is for, and it is where X's documented maximum
// already lives. It also demands a written not_enforced_because, so the gap
// between what Meta requires and what Postdom checks is recorded rather
// than implied by an absence.
//
// Still genuinely not published: Meta gives 1080 x 1920 as "(recommended)",
// which is a target and not a ceiling. The recommendation is recorded on
// its own key rather than smuggled in here as a maximum.
resolution_max_pixels: unpublishedLimit(FACEBOOK_REELS_EVIDENCE, FACEBOOK_LIMITS_VERIFIED_AT),
resolution_recommended: recommendedLimit(
{ width: 1080, height: 1920 },
FACEBOOK_REELS_EVIDENCE,
FACEBOOK_LIMITS_VERIFIED_AT
),
aspect_ratio_recommended: recommendedLimit(
"9:16",
FACEBOOK_REELS_EVIDENCE,
FACEBOOK_LIMITS_VERIFIED_AT
)
},
twitter: {
// "Video on a Post", default accounts: "0.5 seconds-20 minutes". The old
// 140 was the provider's, and on X's own table 140 seconds is the limit for
// video in a direct message.
//
// Premium raises this to 125 minutes and is deliberately not taken. The
// former comment here justified staying at 140 by citing allowlisting for
// "Premium long video" - which is a real caveat on the provider's page and
// appears nowhere on X's. It was reasoning about the right question from
// the wrong document, and it defended a number that was wrong for an
// entirely different reason.
video_max_seconds: classifiedLimit(
20 * 60,
TWITTER_MEDIA_EVIDENCE,
"enforced",
TWITTER_MEDIA_VERIFIED_AT
),
// X publishes codecs - "H264 High Profile", "AAC LC" - and no container or
// MIME list. [mp4, quicktime] came from the provider's "MP4, MOV", so on
// first-party evidence there is nothing here to state.
//
// not_published rather than a guess: mp4 is certainly accepted in practice,
// but "we know it works" is not a published limit, and this file is a record
// of what platforms publish. The global content_type enum still gates the
// request, so nothing is let through that was not let through before.
video_mime_types: unpublishedLimit(TWITTER_MEDIA_EVIDENCE, TWITTER_MEDIA_VERIFIED_AT),
// "between 32x32 and 1280x1024". The floor is confirmed first-party and the
// preflight reads it, so this row keeps both its value and "enforced".
resolution_min_pixels: classifiedLimit(
{ width: 32, height: 32 },
TWITTER_MEDIA_EVIDENCE,
"enforced",
TWITTER_MEDIA_VERIFIED_AT
),
// No resolution_max_pixels row, though the same sentence appears to give
// one. 1280x1024 cannot be a maximum on a page that also says Premium
// accounts "can upload a 1080p video and get 1080p playback" - 1080x1920
// exceeds it. Two first-party statements that contradict each other are not
// a fact, so neither is recorded.
resolution_recommended: recommendedLimit(
{ width: 1280, height: 720 },
TWITTER_MEDIA_EVIDENCE,
TWITTER_MEDIA_VERIFIED_AT
),
// X's "between 1:3 and 3:1" has moved to documentedMediaFacts.twitter for
// the same reason as Facebook's above. A bound is published after all, so
// the not_published that stood here was false - it was true of the
// provider's page, which gives the two recommended ratios and no range.
aspect_ratio_recommended: recommendedLimit(
["16:9", "1:1"],
TWITTER_MEDIA_EVIDENCE,
TWITTER_MEDIA_VERIFIED_AT
)
},
linkedin: {
// The Videos API's own upload cap, stated on
// initializeUploadRequest.fileSizeBytes - "Maximum allowed Videos size is
// 5GB" - the strongest LinkedIn video fact we have and the only one from a
// versioned source. Corroborated by the Pages spec, which states 5 GB too.
//
// The same page also says "File size: Between 75kb and 500MB" a few
// sections up. That figure is scoped to video *ads* - the section links out
// to Video Ads Advertising Specifications - so it is not the cap on an API
// upload. Noted because the page states both numbers and a reader following
// the citation will see the 500MB one first.
//
// Not "enforced", despite being a real provider ceiling, because in this
// repo "enforced" is a promise that Postdom reads the fact before handing
// the asset to the provider - mediaDeliveryViolations on the publish path,
// and the upload preflight too when a single destination is declared.
// Our own global cap is 500 MB, so no request Postdom accepts can reach
// 5 GB: a gate here would be unreachable and a violation test for it would
// pass on the global rule while proving nothing about LinkedIn. Recorded
// with its evidence, and to be reclassified the day the global cap rises
// above it.
// Binary, and deliberately not unified with bluesky's decimal figure below.
//
// The two come from different kinds of source and the convention follows the source rather
// than a house style. Bluesky's is an exact integer in a machine-readable lexicon; rewriting
// it as a power of two would falsify a value the service validates against. LinkedIn's is the
// prose "5GB", which does not say which convention it means - and 5 * 1024^3 is the reading
// that renders back as the source's own number, where the decimal reading renders 4.66 GiB
// beside a page that says 5 GB.
//
// Nothing turns on the choice: this is a recommendation, and Postdom's own 500 MB global cap
// sits an order of magnitude below either reading, so neither can gate a request. If it ever
// becomes enforceable, take the smaller reading - understating a maximum is the safe error.
video_max_bytes: recommendedLimit(
5 * 1024 * 1024 * 1024,
LINKEDIN_VIDEOS_API_EVIDENCE,
LINKEDIN_LIMITS_VERIFIED_AT
),
// Enforced on a revocation, not on a spec value. The Pages spec removes the
// two formats the provider claims, in those words: "No longer supported:
// AVI, QuickTime, MOV", and the ads spec independently states MP4 only. Two
// of three first-party sources exclude MOV; the sharing page is the only one
// that still lists it. Excluding it can only narrow what we send, which is
// why this direction is safe to enforce from unversioned prose when a
// dimension would not be. The global content_type enum still accepts MOV for
// other destinations, so this is a real rejection rather than a restatement.
video_mime_types: classifiedLimit(
["video/mp4"],
LINKEDIN_PAGES_SPEC_EVIDENCE,
"enforced",
LINKEDIN_LIMITS_VERIFIED_AT
),
// A ceiling provably exists; no first-party page states it for organic
// posting through the API. Three values circulate and each is scoped to
// something else - 10 minutes on the Pages spec, 30 in the ads-scoped
// section of the Videos API page, 15 on the sharing troubleshooting page.
// Recording any of them would be a guess wearing a citation.
//
// The minimum is the one figure that does split by client - the sharing page
// gives 3 seconds on desktop and 2 on mobile - which is a further reason not
// to read a single duration bound off these pages.
video_max_seconds: unpublishedLimit(LINKEDIN_VIDEOS_API_EVIDENCE, LINKEDIN_LIMITS_VERIFIED_AT),
// Published, but only in undated help prose scoped to video sharing rather
// than to the API - "Resolution range: 256x144 to 4096x2304" on a page
// titled "Video sharing troubleshooting". Recorded because it is real; not
// enforced because that provenance cannot carry a rejection.
//
// Sided rather than axed. The source states 256x144 to 4096x2304, which is
// a bound on the frame, not on width and height separately: a 2304x4096
// portrait video is within it. Modelling it as { width, height } caps would
// read that portrait as over the height limit, so the pair is recorded by
// longest and shortest side.
resolution_min_pixels: recommendedLimit(
{ longest_side: 256, shortest_side: 144 },
LINKEDIN_SHARING_SPEC_EVIDENCE,
LINKEDIN_LIMITS_VERIFIED_AT
),
resolution_max_pixels: recommendedLimit(
{ longest_side: 4096, shortest_side: 2304 },
LINKEDIN_SHARING_SPEC_EVIDENCE,
LINKEDIN_LIMITS_VERIFIED_AT
),
// 1:2.4 to 2.4:1, from the same prose. The ads page uses a different model
// entirely, which is a second reason not to enforce this one.
aspect_ratio: recommendedLimit(
{ minimum: 1 / 2.4, maximum: 2.4 },
LINKEDIN_SHARING_SPEC_EVIDENCE,
LINKEDIN_LIMITS_VERIFIED_AT
)
// Deliberately absent: any personal-versus-company split. The provider
// asserts personal 10 / company 30, but both numbers appear first-party
// under other scopes and the only page carrying "10 minutes" scopes it to
// company Pages, which inverts the provider's assignment.
},
bluesky: {
// 50 MB, the provider's figure - NOT Bluesky's 300 MB. This is a transport
// limit, and the transport is what binds.
//
// Postdom does not upload to Bluesky. It stores bytes and hands a media URL
// to the provider, so the only ceiling on our path is whatever the provider
// will carry. Bluesky's lexicon says 300 MB; that is the platform's limit
// and it is recorded in documentedMediaFacts, not here.
//
// I first argued the provider's 50 MB was simply stale and could be
// disregarded. That inference was wrong, and the provider's own image
// handling disproves it: Bluesky's image blob is `maxSize: 2000000`,
// described as "May be up to 2 MB, formerly limited to 1 MB", and the
// provider states Bluesky images are recompressed "to stay under Bluesky's
// ~1MB blob limit" (https://docs.zernio.com/guides/media-uploads). So the
// provider holds a superseded Bluesky figure and actively enforces it,
// rewriting files Bluesky would have accepted. Stale and non-binding are
// not the same thing for this provider.
//
// That image figure is evidence, not a fact we record: Postdom publishes
// video, so no request can carry a Bluesky image and an entry for it would
// be a renderable limit nothing can reach.
//
// Whether it applies the same treatment to video is undocumented - the
// provider's compression note covers images only, and its Bluesky video
// figure carries no citation or date. Undocumented is not permission, so
// the smaller value binds. Decimal 50 MB rather than binary, because 50
// million is the smaller reading of an unqualified "50 MB".
//
// Reachable, so the classification is still a promise something keeps: the
// global cap is 500 MB, so mediaDeliveryViolations rejects requests the
// shared rule accepts.
video_max_bytes: classifiedLimit(
5e7,
BLUESKY_PROVIDER_MEDIA_EVIDENCE,
"enforced",
BLUESKY_LIMITS_VERIFIED_AT
),
// `accept: ["video/mp4"]` on the same blob. The global content_type enum
// still admits video/quicktime for other destinations, so this is a real
// rejection rather than a restatement of the global rule.
video_mime_types: classifiedLimit(
["video/mp4"],
BLUESKY_EMBED_LEXICON_EVIDENCE,
"enforced",
BLUESKY_LIMITS_VERIFIED_AT
),
// Both limits provably exist and neither value is published. startUpload
// declares `VideoTooLong` ("the advisory declared duration exceeds the
// limit") and `BadAspectRatio` ("the advisory declared dimensions have an
// unsupported aspect ratio"), which proves the constraints without stating
// them. That is a different fact from "no limit is documented", and the
// difference is why nothing here may be inferred from the errors' existence
// or copied from a blog.
//
// Do not read app.bsky.embed.defs#aspectRatio as the permitted range. It
// takes any width:height of at least 1, but it is a declarative field on
// the post record describing the video, not the gate. The gate is the
// upload service, which is what returns BadAspectRatio.
//
// Architecturally, both rejections arrive from an authoritative probe that
// runs *asynchronously after upload* - startUpload's own words are that the
// declared duration and dimensions are "advisory, non-authoritative ... used
// only for early failure". So a Bluesky duration or aspect failure is a
// terminal provider outcome discovered late, not validation Postdom can
// perform up front.
//
// What that means for us today, stated rather than asserted as a
// requirement nothing meets. Half of it holds and half does not:
//
// - Terminal-ness holds. A late rejection arrives on the webhook path, not
// in the response to publish, so it never reaches the provider's
// status-code mapError at all. destinationsFromPost normalises it to a
// terminal failed destination, which is correct.
// - The reason does not survive. That same function collapses every failure
// to `errorCode: "platform_rejected"` and discards the webhook's
// `platform.error`, so "video too long" and "wrong aspect ratio" are
// indistinguishable from any other refusal.
//
// Preserving the reason is a publish-path contract change, not a tidy-up:
// it needs a field on ProviderPublishDestination and a decision about
// whether raw provider error text may reach a customer-visible surface at
// all, since that text can name the provider. Deliberately not done here.
video_max_seconds: unpublishedLimit(BLUESKY_UPLOAD_LEXICON_EVIDENCE, BLUESKY_LIMITS_VERIFIED_AT),
aspect_ratio: unpublishedLimit(BLUESKY_UPLOAD_LEXICON_EVIDENCE, BLUESKY_LIMITS_VERIFIED_AT)
},
snapchat: {
// The best-sourced destination in this file. One first-party Snap page - the
// Public Profile API's Spotlight surface, which is the endpoint Postdom
// would publish through - states the duration bounds, the container format
// and the resolution floor together, in the same section, for the same
// operation. LinkedIn's facts are scattered across three help pages of
// differing scope; Bluesky's binding figure belongs to the transport rather
// than the platform.
//
// "Video Duration : 6-60 seconds". Both ends enforced: unusually for this
// file the floor is stated as plainly as the ceiling, and both are reachable
// - the global contract admits 1 second and 10 minutes, so without these a
// 4-second clip and a 3-minute clip both pass Postdom and fail at Snap.
//
// 6 is also where Snap's own pages disagree with each other: the consumer
// and advertising material says 5 seconds. The API page is the one that
// governs the call Postdom makes, and 6 is the stricter reading, so it is
// the one that binds twice over. Recorded here so a future reader who finds
// the 5 does not "correct" this downward.
video_min_seconds: classifiedLimit(
6,
SNAPCHAT_SPOTLIGHT_API_EVIDENCE,
"enforced",
SNAPCHAT_LIMITS_VERIFIED_AT
),
video_max_seconds: classifiedLimit(
60,
SNAPCHAT_SPOTLIGHT_API_EVIDENCE,
"enforced",
SNAPCHAT_LIMITS_VERIFIED_AT
),
// "Video should be in .mp4 format". A container statement, not a codec one -
// Snap does not name a codec on this page, so nothing here says H.264. The
// global content_type enum still admits video/quicktime for other
// destinations, which makes this a real rejection rather than a restatement
// of the global rule.
video_mime_types: classifiedLimit(
["video/mp4"],
SNAPCHAT_SPOTLIGHT_API_EVIDENCE,
"enforced",
SNAPCHAT_LIMITS_VERIFIED_AT
),
// "Video resolution at least 540x960px". A floor, and the axes are given
// separately in the source's own order, so it is modelled per-axis rather
// than as a frame bound - 960x540 landscape is not what this sentence
// permits, and reading it as an unsided bound would admit exactly that.
resolution_min_pixels: classifiedLimit(
{ width: 540, height: 960 },
SNAPCHAT_SPOTLIGHT_API_EVIDENCE,
"enforced",
SNAPCHAT_LIMITS_VERIFIED_AT
),
// Not stated by Snap for this endpoint, and not borrowed from anywhere else.
//
// Snap's advertising pages do state a 9:16 storage and display aspect ratio
// and a maximum resolution, and it would be easy to lift them. They describe
// a different contract - paid creative, not an organic Spotlight post - and
// publishing an ads figure under a Snapchat citation is the precise error
// that holds Facebook and X out of the public schema: thirteen of their
// facts cite documentation for something other than the thing claimed, and
// the spec-checker would label the URL "Official source" regardless.
//
// 540x960 is a floor, so the frame's shape is implied to be at least as tall
// as it is wide, but "implied by the floor" is not a published ratio and
// must not render as one.
aspect_ratio: unpublishedLimit(SNAPCHAT_SPOTLIGHT_API_EVIDENCE, SNAPCHAT_LIMITS_VERIFIED_AT),
resolution_max_pixels: unpublishedLimit(
SNAPCHAT_SPOTLIGHT_API_EVIDENCE,
SNAPCHAT_LIMITS_VERIFIED_AT
)
// Deliberately absent: video_max_bytes. Snap states "A file of upto 1 GB can
// be uploaded through the multipart upload API", but that is scoped to the
// upload mechanism rather than to Spotlight, and Postdom's own 500 MB global
// cap sits below it - so a gate here would be unreachable and a violation
// test for it would pass on the global rule while proving nothing about
// Snapchat. It is recorded as a documented fact below instead, on the same
// reasoning as LinkedIn's 5 GB.
},
threads: {
// Meta's own Threads posting reference, which states these directly rather than by reference
// to Instagram's. Deliberately not Meta's Instagram ads media requirements: a search summary
// conflated the two, and those describe paid creative on a different surface.
//
// "300 seconds (5 minutes) maximum". Reachable, because the global contract admits 10
// minutes, so without this a six-minute video passes Postdom and fails at Meta.
video_max_seconds: classifiedLimit(300, THREADS_POSTS_EVIDENCE, "enforced", THREADS_LIMITS_VERIFIED_AT),
// "between 0.01:1 and 10:1" - a real published range, and the widest any destination here
// declares, which is exactly why it is worth gating: the global contract has no aspect rule at
// all, so a 1000:1 frame reaches Meta unchallenged without this.
//
// 9:16 appears on the same page as a recommendation to avoid cropping, not as a bound, and is
// not recorded as one.
aspect_ratio: classifiedLimit(
{ minimum: 0.01, maximum: 10 },
THREADS_POSTS_EVIDENCE,
"enforced",
THREADS_LIMITS_VERIFIED_AT
),
// "Maximum horizontal pixels of 1920" - the same shape as Instagram's cap and reachable for
// the same reason: nothing global bounds width.
resolution_max_horizontal_pixels: classifiedLimit(
1920,
THREADS_POSTS_EVIDENCE,
"enforced",
THREADS_LIMITS_VERIFIED_AT
),
// "MOV or MP4 (MPEG-4 Part 14)". Both are already in the global enum, so this cannot reject a
// request the shared rule accepts. Classified enforced and gated by that enum rather than by
// a Threads-specific check, which is how every other destination records the same situation.
//
// docs/verification/destination-expansion-cost-2026-09-02.md records MP4 only for Threads.
// That is not a contradiction and must not be resolved by picking one: this value is Meta's,
// from the page cited below, and that row is the *provider's* platform matrix - what the
// transport says it carries. Bluesky has the same split on the byte axis, where the
// transport's figure is the one that binds.
//
// Inert today, because both values sit inside the global enum. The day that enum widens, the
// narrower of the two is the one a request actually has to satisfy, and it will be the
// provider's - so read its contract page, not the matrix row, before widening this.
video_mime_types: classifiedLimit(
["video/mp4", "video/quicktime"],
THREADS_POSTS_EVIDENCE,
"enforced",
THREADS_LIMITS_VERIFIED_AT
),
// Two floors, two different absences, and they are recorded differently on purpose. Read
// together they look contradictory - one paragraph argues not_published would be a false
// claim, and the next line records not_published - so the distinction is stated rather than
// left to be inferred. `not_published` asserts "the platform publishes no such bound". That is
// false for one of these and true for the other.
//
// video_min_seconds is deliberately absent from this table. Meta publishes a minimum -
// "longer than 0 seconds" - so not_published here would assert Meta publishes no minimum
// duration for Threads video, which is false. It is unenforceable rather than unpublished:
// the schema already requires a positive integer, so no request can violate it. That is what
// documentedMediaFacts.threads is for, and it is the same treatment X's half-second floor
// gets, also recorded there and not here.
//
// resolution_min_pixels is the genuinely-absent case. Meta states a maximum horizontal size
// and no floor on either axis, so there is no minimum to record and not_published is exactly
// true. Both re-read at THREADS_POSTS_EVIDENCE on 2026-09-03.
resolution_min_pixels: unpublishedLimit(THREADS_POSTS_EVIDENCE, THREADS_LIMITS_VERIFIED_AT)
// Deliberately absent: video_max_bytes at 1 GB, which sits above Postdom's 500 MB global cap
// and so could never gate a request. Recorded below as a documented fact instead.
}
};
var documentedMediaFacts = {
facebook: {
resolution_min_pixels: {
value: { width: 540, height: 960 },
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: `Meta's own floor - "Minimum is 540 x 960 pixels" - and the first Facebook resolution fact we hold from Meta rather than from the provider, which published none. Not gated because gating it would add a rejection, and this whole re-sourcing exists because Postdom was rejecting Reels Meta accepts. A floor read off one documentation line can be wrong in the direction that costs a customer a post, so it is recorded and left unread until a request is observed failing on it.`,
evidence: FACEBOOK_REELS_EVIDENCE,
verified_at: FACEBOOK_LIMITS_VERIFIED_AT
},
aspect_ratio: {
value: { minimum: 9 / 16, maximum: 16 / 9 },
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: '"Aspect ratios for videos need to be between 16x9 and 9x16", from error code 1363040 rather than from the specification table. Taken from the error table deliberately: the spec table lists "9 x 16" as the target, while the error table states what Meta actually rejects, and a rejection contract is the stronger evidence of a bound. The two do not conflict - 9:16 sits at one end of the published range. Recorded as the range rather than the target so a square video is not read as violating a limit Meta does not impose. Not gated: the Facebook preflight branch reads durations only.',
evidence: FACEBOOK_REELS_EVIDENCE,
verified_at: FACEBOOK_LIMITS_VERIFIED_AT
}
},
twitter: {
resolution_max_pixels: {
value: { width: 1280, height: 1024 },
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: `Was 1920x1200 from the provider; X's own advanced constraints say "between 32x32 and 1280x1024". Still not gated, and now for a second reason on top of the original one. The original: published as a single bound with no orientation stated, which would reject every standard vertical short, and Postdom's own control fixtures upload 1080x1920 to X. The second: the same X page says a subscribed user "can upload a 1080p video and get 1080p playback", and 1080x1920 does not fit inside 1280x1024. Two first-party statements that contradict each other do not make a limit, so this one is recorded and left unread.`,
evidence: TWITTER_MEDIA_EVIDENCE,
verified_at: TWITTER_MEDIA_VERIFIED_AT
},
aspect_ratio: {
value: { minimum: 1 / 3, maximum: 3 },
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: `"between 1:3 and 3:1", from X's advanced constraints. Sat in the enforcement snapshot as not_published, which was true of the provider's page - it gives the two recommended ratios and no range - and false of X's. Not gated because the X preflight branch reads resolution and duration, not ratio.`,
evidence: TWITTER_MEDIA_EVIDENCE,
verified_at: TWITTER_MEDIA_VERIFIED_AT
},
video_min_seconds: {
value: 0.5,
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: "mediaUploadPreflightRequestSchema declares duration_seconds as a positive integer, so no request can express half a second. An enforced minimum no request can violate is unenforceable by construction.",
evidence: TWITTER_MEDIA_EVIDENCE,
verified_at: TWITTER_MEDIA_VERIFIED_AT
}
},
bluesky: {
// Named for what it is rather than `video_max_bytes`, which
// shortFormVideoLimits.bluesky already uses for the 50 MB delivery ceiling.
// Two different numbers under one key across the two tables would let any
// consumer keying facts by bare name resolve them arbitrarily, and
// video-spec-checker special-cases exactly that name.
platform_video_max_bytes: {
value: 3e8,
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: "Bluesky's own ceiling, from the lexicon's `maxSize: 300000000`. It is not the one that binds: Postdom stores bytes and hands a media URL to the provider, so the provider is the transport and its 50 MB figure is the ceiling a request meets. Recorded so the platform fact is not lost and so the enforced 50 MB is legible as a transport limit rather than a claim about Bluesky. Raise the enforced value only on evidence the provider carries more, never on evidence Bluesky accepts more.",
evidence: BLUESKY_EMBED_LEXICON_EVIDENCE,
verified_at: BLUESKY_LIMITS_VERIFIED_AT
},
image_max_bytes: {
value: 2e6,
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: 'Postdom publishes video, so no request can carry a Bluesky image and a gate would be unreachable. Recorded because it is the evidence for the video decision above: the lexicon says "May be up to 2 MB, formerly limited to 1 MB" while the provider recompresses images to stay under ~1 MB. That is a superseded platform figure the provider still enforces, which is why its video figure could not be dismissed as merely stale. Deleting this row removes the only support for treating the 50 MB delivery ceiling as a real transport limit rather than a stale copy.',
evidence: BLUESKY_PROVIDER_COMPRESSION_EVIDENCE,
verified_at: BLUESKY_LIMITS_VERIFIED_AT
}
},
snapchat: {
video_max_bytes: {
value: 1e9,
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: `Snap's own figure - "A file of upto 1 GB can be uploaded through the multipart upload API" - but scoped to the upload mechanism rather than to Spotlight, and an order of magnitude above Postdom's 500 MB global cap. No request Postdom accepts can reach it, so a gate here would be unreachable and a violation test for it would pass on the global rule while proving nothing about Snapchat. Decimal rather than binary: the source says "1 GB" in prose with no convention stated, and the decimal reading is the smaller one, which is the safe error for a maximum. Reclassify the day the global cap rises above it.`,
evidence: SNAPCHAT_SPOTLIGHT_API_EVIDENCE,
verified_at: SNAPCHAT_LIMITS_VERIFIED_AT
}
},
threads: {
video_max_bytes: {
value: 1e9,
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: `Meta's own figure - "1 GB maximum" on the Threads posting reference - and an order of magnitude above Postdom's 500 MB global cap. No request Postdom accepts can reach it, so a gate would be unreachable and a violation test for it would pass on the global rule while proving nothing about Threads. Decimal rather than binary: the source says "1 GB" in prose with no convention stated, and the decimal reading is the smaller one, which is the safe error for a maximum. Reclassify the day the global cap rises above it.`,
evidence: THREADS_POSTS_EVIDENCE,
verified_at: THREADS_LIMITS_VERIFIED_AT
},
video_min_seconds: {
// An exclusive bound with no number behind it, recorded as one rather than rounded to a
// figure Meta does not give. `0.1` or `1` would be an invention; a bare `0` would read as
// an inclusive floor and admit the zero-length video the statement excludes.
value: { exclusive_minimum: 0 },
classification: "documented",
enforced_by_postdom: false,
not_enforced_because: `Meta publishes a minimum "longer than 0 seconds". mediaUploadPreflightRequestSchema declares duration_seconds as a positive integer, so no request can express a duration this rule would reject. An enforced minimum no request can violate is unenforceable by construction - the same reason X's half-second floor is recorded here rather than gated. This sat in shortFormVideoLimits as not_published, which asserted the opposite of what Meta's page says.`,
evidence: THREADS_POSTS_EVIDENCE,
verified_at: THREADS_LIMITS_VERIFIED_AT
}
}
};
var tiktokSettingsSchema = z.object({
privacy_level: z.enum([
"PUBLIC_TO_EVERYONE",
"MUTUAL_FOLLOW_FRIENDS",
"FOLLOWER_OF_CREATOR",
"SELF_ONLY"
]),
allow_comment: z.boolean(),
allow_duet: z.boolean(),
allow_stitch: z.boolean(),
content_preview_confirmed: z.literal(true),
express_consent_given: z.literal(true),
video_made_with_ai: z.boolean().default(true)
});
var instagramSettingsSchema = z.object({
contentType: z.literal("reel").default("reel"),
isAiGenerated: z.boolean().default(true)
});
var youtubeSettingsSchema = z.object({
title: z.string().min(1).max(platformLimits.youtube.title_max_chars.value),
visibility: z.enum(["public", "private", "unlisted"]).default("private"),
madeForKids: z.boolean(),
containsSyntheticMedia: z.boolean().default(true)
});
var facebookSettingsSchema = z.object({
contentType: z.literal("reel").default("reel")
});
var twitterSettingsSchema = z.object({}).strict();
var linkedinSettingsSchema = z.object({}).strict();
var blueskySettingsSchema = z.object({}).strict();
var snapchatSettingsSchema = z.object({}).strict();
var threadsSettingsSchema = z.object({}).strict();
var platformTargetSchema = z.discriminatedUnion("platform", [
z.object({
account_id: z.string().min(1),
platform: z.literal("tiktok"),
settings: tiktokSettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("instagram"),
settings: instagramSettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("youtube"),
settings: youtubeSettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("facebook"),
settings: facebookSettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("twitter"),
settings: twitterSettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("linkedin"),
settings: linkedinSettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("bluesky"),
settings: blueskySettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("snapchat"),
settings: snapchatSettingsSchema
}),
z.object({
account_id: z.string().min(1),
platform: z.literal("threads"),
settings: threadsSettingsSchema
})
]);
var publishRequestSchema = z.object({
post_id: z.uuid(),
caption: z.string().max(platformLimits.global.caption_max_chars.value),
// Optional as of BUILD-28: a text post is a publish with no media. The "never neither" rule
// lives on `postBodySchema`, the boundary a caller crosses - repeating it here would be a
// second predicate that gets fixed once.
media_url: z.url().optional(),
targets: z.array(platformTargetSchema).min(1),
idempotency_key: z.string().min(8).max(255)
});
// ../core/src/account-policy.ts
var PRODUCT_WORKSPACE_POLICY_DEFAULTS = {
trustLevel: 3,
paused: false,
/**
* Backend publishing-policy default; account override semantics are unchanged.
* Admin-saved UTC is indistinguishable from this default in the stored value.
* Frontend display/new-entry clocks nevertheless accept a successfully read
* workspace UTC as authoritative (BUILD-115), not an absent policy response.
*/
timezone: "UTC",
maximumPostsPerDay: AUTONOMY_DEFAULT_MAXIMUM_POSTS_PER_DAY,
quietHoursStart: AUTONOMY_DEFAULT_QUIET_HOURS_START,
quietHoursEnd: AUTONOMY_DEFAULT_QUIET_HOURS_END
};
// ../core/src/ai-budget.ts
import { z as z2 } from "zod";
var AI_AMOUNT_MAX = 9223372036854775807n;
var decimal = /^(0|[1-9][0-9]{0,18})$/;
var aiAmountSchema = z2.string().regex(decimal).refine((value) => decimal.test(value) && BigInt(value) <= AI_AMOUNT_MAX, "Amount exceeds bigint range");
var positiveAmount = aiAmountSchema.refine((value) => value !== "0");
var revision = z2.string().min(1).max(80);
var timestamp = z2.iso.datetime();
var aiRateSchema = z2.object({
numerator: aiAmountSchema,
denominator: positiveAmount
}).strict();
var aiResponsesTariffSchema = z2.object({
provider: z2.literal("openai"),
endpoint: z2.literal("responses"),
model: z2.string().min(1).max(128),
revision,
service_tier: z2.literal("default"),
currency: z2.literal("USD"),
input_micro_usd: aiRateSchema,
cached_input_micro_usd: aiRateSchema,
output_micro_usd: aiRateSchema
}).strict().refine(
(value) => aiRateSchema.safeParse(value.cached_input_micro_usd).success && aiRateSchema.safeParse(value.input_micro_usd).success && BigInt(value.cached_input_micro_usd.numerator) * BigInt(value.input_micro_usd.denominator) <= BigInt(value.input_micro_usd.numerator) * BigInt(value.cached_input_micro_usd.denominator),
"Cached input tariff cannot exceed the uncached reservation rate"
);
var aiPeriodSchema = z2.object({
id: z2.string().regex(/^\d{4}-\d{2}$/),
starts_at: timestamp,
ends_at: timestamp
}).strict().refine((v) => {
const start = new Date(v.starts_at), end = new Date(v.ends_at);
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime())) return false;
if (start.getUTCDate() !== 1 || start.getUTCHours() !== 0 || start.getUTCMinutes() !== 0 || start.getUTCSeconds() !== 0 || start.getUTCMilliseconds() !== 0 || start.toISOString().slice(0, 7) !== v.id) return false;
const next = new Date(start);
next.setUTCMonth(next.getUTCMonth() + 1);
return next.getTime() === end.getTime();
}, "AI period must be one UTC calendar month");
var aiEffectiveEntitlementSchema = z2.discriminatedUnion("kind", [
z2.object({ kind: z2.literal("none") }).strict(),
z2.object({
kind: z2.literal("paid"),
evidence_revision: revision,
subscription_status: z2.enum(["active", "canceled"]),
invoice_interval: z2.enum(["month", "year"]),
starts_at: timestamp,
ends_at: timestamp
}).strict(),
z2.object({
kind: z2.literal("trial"),
evidence_revision: revision,
verified_subject_id: z2.uuid(),
creator_pool_id: z2.uuid(),
starts_at: timestamp,
ends_at: timestamp
}).strict()
]).refine((v) => v.kind === "none" || Date.parse(v.starts_at) < Date.parse(v.ends_at), "Invalid entitlement period");
var attemptIdentity = {
attempt_id: z2.uuid(),
allocation_id: z2.uuid(),
reserved_credit_units: aiAmountSchema,
reserved_provider_micro_usd: aiAmountSchema
};
var aiRefundSchema = z2.object({ refund_id: z2.uuid(), credit_units: positiveAmount }).strict();
var aiAttemptSchema = z2.discriminatedUnion("state", [
z2.object({ ...attemptIdentity, state: z2.enum(["reserved", "started", "unknown"]) }).strict(),
z2.object({
...attemptIdentity,
state: z2.literal("settled"),
credit_units: aiAmountSchema,
provider_micro_usd: aiAmountSchema,
refunds: z2.array(aiRefundSchema).max(1e3)
}).strict(),
z2.object({ ...attemptIdentity, state: z2.literal("released") }).strict()
]);
var aiJobResourcesSchema = z2.object({
input_tokens: aiAmountSchema,
context_tokens: aiAmountSchema,
output_tokens: aiAmountSchema,
tool_steps: aiAmountSchema,
model_steps: aiAmountSchema,
attempts: aiAmountSchema,
elapsed_ms: aiAmountSchema
}).strict();
var aiFundedPolicySchema = z2.object({
enabled: z2.boolean(),
policy_revision: revision,
rate_revision: revision,
// Absent on historical synthetic policies. Never default it while parsing:
// historical policy digests and settlement rates must remain byte-stable.
responses_tariff: aiResponsesTariffSchema.optional(),
// Context and newly supplied input are billed at the input rate, not twice.
input_micro_usd: aiRateSchema,
output_micro_usd: aiRateSchema,
tool_micro_usd: aiRateSchema,
credits_per_micro_usd: aiRateSchema,
job_limits: aiJobResourcesSchema,
member_requests_per_window: aiAmountSchema,
workspace_requests_per_window: aiAmountSchema,
member_active_jobs: aiAmountSchema,
workspace_active_jobs: aiAmountSchema
}).strict();
var aiBudgetDenialSchema = z2.enum([
"not_configured",
"policy_disabled",
"not_authorized",
"not_entitled",
"entitlement_expired",
"period_inactive",
"insufficient_credits",
"operator_cap",
"job_limit",
"rate_limit",
"concurrency_limit",
"invalid_evidence",
"accounting_exception"
]);
var aiStepPreflightSchema = z2.object({
now: timestamp,
authorized: z2.boolean(),
policy: aiFundedPolicySchema.nullable(),
entitlement: aiEffectiveEntitlementSchema,
period: aiPeriodSchema,
available_credit_units: aiAmountSchema,
workspace_available_micro_usd: aiAmountSchema,
operator_available_micro_usd: aiAmountSchema,
accounting_exception: z2.boolean(),
used: aiJobResourcesSchema,
requested: aiJobResourcesSchema,
// Absence preserves legacy reservation semantics; never infer from policy fields.
pricing_family: z2.enum(["legacy", "openai-responses-v1"]).optional(),
member_window_requests: aiAmountSchema,
workspace_window_requests: aiAmountSchema,
member_active_jobs: aiAmountSchema,
workspace_active_jobs: aiAmountSchema,
// Continuing an already-counted job does not acquire a second concurrency slot.
starts_job: z2.boolean()
}).strict();
var aiBudgetSnapshotSchema = z2.object({
format: z2.literal("ai-budget-v1"),
org_id: z2.uuid(),
period: aiPeriodSchema,
entitlement_ends_at: timestamp.nullable(),
// Operator calendar window and customer allocation have different lifetimes.
allocation_kind: z2.enum(["paid_monthly", "trial"]).nullable(),
allocation_starts_at: timestamp.nullable(),
resets_at: timestamp.nullable(),
expires_at: timestamp.nullable(),
included: aiAmountSchema.nullable(),
settled: aiAmountSchema.nullable(),
reserved: aiAmountSchema.nullable(),
available: aiAmountSchema.nullable(),
unit_policy_revision: revision.nullable(),
revision: aiAmountSchema,
as_of: timestamp,
configuration: z2.enum(["configured", "not_configured"]),
can_start: z2.boolean(),
denial_reason: aiBudgetDenialSchema.nullable(),
permitted_actions: z2.array(z2.enum(["manual_work", "view_usage", "request_funded_step"])).max(3)
}).strict().superRefine((v, ctx) => {
const quantities = [v.included, v.settled, v.reserved, v.available];
const configured = v.configuration === "configured";
if (!configured && (v.allocation_kind !== null || v.allocation_starts_at !== null || v.resets_at !== null || v.expires_at !== null)) {
ctx.addIssue({ code: "custom", message: "Unavailable allocation has no reset or expiry" });
}
if (configured && (v.allocation_kind === null || v.allocation_starts_at === null || v.expires_at === null || Date.parse(v.allocation_starts_at) >= Date.parse(v.expires_at) || v.allocation_kind === "trial" && v.resets_at !== null || v.allocation_kind === "paid_monthly" && (v.resets_at !== v.period.ends_at || v.allocation_starts_at !== v.period.starts_at || v.expires_at !== v.period.ends_at))) {
ctx.addIssue({ code: "custom", message: "Allocation kind and reset/expiry disagree" });
}
if (configured ? quantities.some((x) => x === null) || v.unit_policy_revision === null : quantities.some((x) => x !== null) || v.unit_policy_revision !== null || v.denial_reason !== "not_configured") {
ctx.addIssue({ code: "custom", message: "Configuration and balance evidence disagree" });
}
if (v.can_start !== (v.denial_reason === null) || !configured && v.can_start || new Set(v.permitted_actions).size !== v.permitted_actions.length || v.permitted_actions.includes("request_funded_step") !== v.can_start) {
ctx.addIssue({ code: "custom", message: "Admission hint and actions disagree" });
}
if (quantities.every((x) => x !== null && aiAmountSchema.safeParse(x).success)) {
const remainder = BigInt(v.included) - BigInt(v.settled) - BigInt(v.reserved);
if (BigInt(v.available) !== (remainder > 0n ? remainder : 0n) || v.can_start && BigInt(v.available) === 0n) {
ctx.addIssue({ code: "custom", message: "Invalid balance projection" });
}
}
if (v.can_start && (v.entitlement_ends_at === null || v.allocation_starts_at === null || v.expires_at === null || Date.parse(v.as_of) < Date.parse(v.allocation_starts_at) || Date.parse(v.as_of) >= Date.parse(v.expires_at) || Date.parse(v.as_of) >= Date.parse(v.entitlement_ends_at) || Date.parse(v.as_of) < Date.parse(v.period.starts_at) || Date.parse(v.as_of) >= Date.parse(v.period.ends_at))) {
ctx.addIssue({ code: "custom", message: "Admission hint has no current entitlement period" });
}
});
// ../core/src/ai-budget-policy.ts
import { z as z3 } from "zod";
var trialClaimSchema = z3.object({
now: z3.iso.datetime(),
ends_at: z3.iso.datetime(),
verified_subject_id: z3.uuid().nullable(),
creator_already_claimed: z3.boolean(),
customer_already_claimed: z3.boolean(),
configured_pool_units: aiAmountSchema.nullable()
}).strict();
var settlementEvidence = z3.object({ credit_units: aiAmountSchema, provider_micro_usd: aiAmountSchema }).strict();
// ../core/src/agent-client-config.ts
var POSTDOM_MCP_PACKAGE = "@postdom/mcp@0.3.0";
var CLAUDE_STDIO_PLACEHOLDERS = {
serverUrl: "<server URL shown in Postdom Accounts>",
apiKey: "<workspace key, shown once>"
};
var CODEX_CONFIG_PLACEHOLDERS = {
serverUrl: "YOUR_SERVER_URL",
apiKey: "YOUR_WORKSPACE_KEY"
};
function agentCredentialEnv(input) {
return {
POSTDOM_API_URL: input.serverUrl,
POSTDOM_API_KEY: input.apiKey
};
}
function buildClaudeStdioConfig(input) {
const env = agentCredentialEnv(input);
return `{
"mcpServers": {
"postdom": {
"command": "npx",
"args": ["-y", "${POSTDOM_MCP_PACKAGE}"],
"env": {
"POSTDOM_API_URL": "${env.POSTDOM_API_URL}",
"POSTDOM_API_KEY": "${env.POSTDOM_API_KEY}"
}
}
}
}`;
}
var CLAUDE_STDIO_CONFIG = buildClaudeStdioConfig(CLAUDE_STDIO_PLACEHOLDERS);
function buildCodexConfigTable(input) {
const env = agentCredentialEnv(input);
return `[mcp_servers.postdom]
command = "npx"
args = ["-y", "${POSTDOM_MCP_PACKAGE}"]
[mcp_servers.postdom.env]
POSTDOM_API_KEY = "${env.POSTDOM_API_KEY}"
POSTDOM_API_URL = "${env.POSTDOM_API_URL}"`;
}
var CODEX_CONFIG_TABLE = buildCodexConfigTable(CODEX_CONFIG_PLACEHOLDERS);
function CURSOR_ENV_REFERENCE(name) {
return `\${env:${name}}`;
}
var CURSOR_CONFIG_BLOCK = JSON.stringify({
mcpServers: {
postdom: {
type: "stdio",
command: "npx",
args: ["-y", POSTDOM_MCP_PACKAGE],
env: {
POSTDOM_API_KEY: CURSOR_ENV_REFERENCE("POSTDOM_API_KEY")
}
}
}
}, null, 2);
// ../core/src/approval.ts
import { z as z4 } from "zod";
var agentContextSchema = z4.object({
identity: z4.string().trim().min(1).max(120),
intent: z4.string().trim().min(1).max(500)
});
var approvalFeedbackCategorySchema = z4.enum([
"caption",
"media",
"targeting",
"timing",
"policy",
"other"
]);
var approvalFeedbackSchema = z4.object({
category: approvalFeedbackCategorySchema,
text: z4.string().trim().min(1).max(1e3)
});
var approvalDecisionSchema = z4.enum(["changes_requested", "rejected"]);
var approvalDestinationSchema = z4.object({
account_id: z4.string().min(1),
platform: corePlatformSchema,
handle: z4.string().nullable(),
settings: z4.record(z4.string(), z4.unknown())
});
var planStatusSchema = z4.enum([
"requires_approval",
"approved",
"changes_requested",
"rejected",
"expired",
"cancelled"
]);
var planAuthorizationStatusSchema = z4.enum(["applied", "not_applied"]);
var planAuthorizationReasonSchema = z4.enum([
"agent_paused",
"publishing_disabled",
"plan_not_found",
"plan_not_approved",
"plan_not_started",
"plan_expired",
"plan_cancelled",
"target_not_in_plan",
"target_not_l2",
"target_disconnected",
"policy_not_configured",
"policy_violation",
"max_posts_exhausted",
// Not about the plan. Every value above says something is wrong with the plan; these two say the
// plan is fine and the post moved out from under its authorization - a reviewer rescheduled,
// writing a new request hash, or the schedule was missed. `posts_plan_authorization_check`
// requires a reason whenever a post with a plan is `not_applied`, so both paths previously had
// to borrow a plan fault or write NULL and fail the write.
"request_changed",
"schedule_missed"
]);
// ../core/src/brief.ts
var WORKSPACE_BRIEF_SHARED_HEADINGS = [
"Voice",
"Captions",
"Hashtags",
"Timing preferences",
"Never-do"
];
var SHARED_HEADING_LOOKUP = new Map(
WORKSPACE_BRIEF_SHARED_HEADINGS.map((heading) => [normalizeHeading(heading), heading])
);
function normalizeHeading(value) {
return value.trim().toLowerCase().replace(/[:.]+$/, "");
}
function platformHeadingAliases() {
const aliases = /* @__PURE__ */ new Map();
for (const platform of corePlatformSchema.options) {
aliases.set(platform, platform);
aliases.set(normalizeHeading(platformLabel(platform)), platform);
}
aliases.set("twitter", "twitter");
return aliases;
}
var PLATFORM_HEADING_ALIASES = platformHeadingAliases();
// ../core/src/brief-history.ts
import { z as z5 } from "zod";
var BRIEF_HISTORY_PAGE_SIZE = 10;
var briefVersionSchema = z5.number().int().positive().max(2147483647);
var briefVersionMetadataSchema = z5.object({
version: briefVersionSchema,
updated_at: z5.string().refine((value) => Number.isFinite(Date.parse(value))),
updated_by: z5.string()
}).strict();
var briefVersionDocumentSchema = briefVersionMetadataSchema.extend({ content: z5.string() });
var briefHistoryPageSchema = z5.object({
before_version: briefVersionSchema,
versions: z5.array(briefVersionMetadataSchema).max(BRIEF_HISTORY_PAGE_SIZE),
next_before_version: briefVersionSchema.nullable()
}).strict().superRefine((page, ctx) => {
let previous = page.before_version;
for (const version of page.versions) {
if (version.version >= previous) ctx.addIssue({ code: "custom", message: "History must descend strictly below the requested version" });
previous = version.version;
}
if (page.next_before_version !== null && (page.versions.length !== BRIEF_HISTORY_PAGE_SIZE || page.next_before_version !== previous || previous <= 1)) {
ctx.addIssue({ code: "custom", message: "Invalid older-page cursor" });
}
});
var briefMetadataSnapshotSchema = z5.object({
format: z5.literal("brief-metadata-v1"),
current: briefVersionDocumentSchema,
history: briefHistoryPageSchema
}).strict().superRefine((snapshot, ctx) => {
if (snapshot.history.before_version !== snapshot.current.version) ctx.addIssue({ code: "custom", message: "History must be anchored to the saved current version" });
});
// ../core/src/connect-trust.ts
function proseList(names) {
if (names.length === 0) return "";
if (names.length === 1) return names[0];
if (names.length === 2) return `${names[0]} and ${names[1]}`;
return `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
}
function connectTrustPasswordText(redirectCount = REDIRECT_DESTINATIONS.length, credentialNames = CREDENTIAL_DESTINATIONS.map((destination) => platformLabel(destination))) {
const oauth = redirectCount > 0 ? redirectCount > 0 && credentialNames.length === 0 ? "Store a platform password \u2014 you sign in on the destination." : "Store a platform password for OAuth \u2014 you sign in on the destination." : null;
if (credentialNames.length === 0) {
return oauth ?? "Store a platform password \u2014 you sign in on the destination.";
}
const named = proseList(credentialNames);
const verb = credentialNames.length === 1 ? "uses" : "use";
const credential = `${named} ${verb} an app password once to connect and do${credentialNames.length === 1 ? "es" : ""} not keep it.`;
return oauth ? `${oauth} ${credential}` : `Keep an app password. ${credential}`;
}
function connectTrustItemsFrom(passwordText) {
return [
{
id: "publish",
label: "Will",
text: "Publish the video you supply, under the policy you set."
},
{
id: "passwords",
label: "Will not",
text: passwordText
},
{
id: "revoke",
label: "You can",
text: "Revoke the connection anytime."
}
];
}
var CONNECT_TRUST_ITEMS = connectTrustItemsFrom(
connectTrustPasswordText()
);
var CONNECT_TRUST_PROSE = CONNECT_TRUST_ITEMS.map((item) => `${item.label}: ${item.text}`).join(" ");
// ../core/src/contact-sales.ts
import { z as z6 } from "zod";
var contactSalesMonthlyVideoVolumeSchema = z6.enum([
"under_50",
"50_250",
"251_1000",
"1001_5000",
"over_5000"
]);
function campaignValue(max) {
return z6.preprocess(
(value) => {
if (value === null) return void 0;
if (typeof value === "string" && value.trim() === "") return void 0;
return value;
},
z6.string().trim().max(max).optional()
);
}
var contactSalesSourceSchema = z6.object({
utm_source: campaignValue(120),
utm_medium: campaignValue(120),
utm_campaign: campaignValue(200),
utm_content: campaignValue(200),
utm_term: campaignValue(200),
/** The path they landed on, which is not always the path they submit from. */
landing_path: campaignValue(512)
}).strict();
var contactSalesSubmissionSchema = z6.object({
name: z6.string().trim().min(1).max(120),
work_email: z6.email().max(254).transform((value) => value.trim().toLowerCase()),
company: z6.string().trim().min(1).max(160),
monthly_video_volume: contactSalesMonthlyVideoVolumeSchema,
message: z6.string().trim().min(1).max(4e3),
website: z6.string().max(200).default(""),
/** Optional, and absent must stay valid forever: every enquiry that has ever been sent omits it,
* and the form on a page carrying no campaign parameters will go on omitting it. */
source: contactSalesSourceSchema.optional()
}).strict();
var contactSalesAcceptedSchema = z6.object({
accepted: z6.literal(true),
request_id: z6.uuid()
});
// ../core/src/feeds.ts
import { z as z9 } from "zod";
// ../core/src/performance.ts
import { z as z7 } from "zod";
var metricAvailabilitySchema = z7.enum([
"available",
"delayed(2-3d)",
"estimable",
"never",
"unverified"
]);
var performanceMetricSchema = z7.enum([
"views",
"likes",
"comments",
"shares",
"saves",
"watch_time_s",
"avg_watch_pct",
"completion_pct",
"follower_delta"
]);
var BASELINE_VERIFIED_AT = "2026-08-21T00:00:00.000Z";
var METRIC_EVIDENCE = "docs/verification/metric-availability.md";
var FACEBOOK_METRIC_EVIDENCE = "https://developers.facebook.com/docs/graph-api/reference/video/video_insights/";
var FACEBOOK_FOLLOWER_EVIDENCE = "https://developers.facebook.com/docs/graph-api/reference/insights/";
var FACEBOOK_METRICS_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
var TWITTER_METRIC_EVIDENCE = "https://docs.x.com/x-api/fundamentals/metrics";
var TWITTER_FOLLOWER_EVIDENCE = "https://docs.x.com/x-api/fundamentals/data-dictionary";
var TWITTER_METRICS_VERIFIED_AT = "2026-09-02T00:00:00.000Z";
function verified(state, verifiedAt = BASELINE_VERIFIED_AT, evidence = METRIC_EVIDENCE) {
return { state, verified_at: verifiedAt, evidence };
}
function unverified() {
return { state: "unverified", verified_at: null, evidence: METRIC_EVIDENCE };
}
var metricAvailabilityMetadata = {
tiktok: {
views: verified("available"),
likes: verified("available"),
comments: verified("available"),
shares: verified("available"),
saves: unverified(),
watch_time_s: verified("never"),
avg_watch_pct: verified("never"),
completion_pct: verified("never"),
follower_delta: verified("estimable")
},
instagram: {
views: verified("available"),
likes: verified("available"),
comments: verified("available"),
shares: verified("available"),
saves: unverified(),
watch_time_s: verified("available"),
avg_watch_pct: verified("estimable"),
completion_pct: verified("estimable"),
follower_delta: verified("estimable")
},
youtube: {
views: verified("available"),
likes: verified("available"),
comments: verified("available"),
shares: verified("available"),
saves: verified("never"),
watch_time_s: verified("available"),
avg_watch_pct: verified("delayed(2-3d)"),
completion_pct: verified("delayed(2-3d)"),
follower_delta: verified("estimable")
},
facebook: {
views: verified("available", FACEBOOK_METRICS_VERIFIED_AT, FACEBOOK_METRIC_EVIDENCE),
likes: verified("available", FACEBOOK_METRICS_VERIFIED_AT, FACEBOOK_METRIC_EVIDENCE),
comments: verified("available", FACEBOOK_METRICS_VERIFIED_AT, FACEBOOK_METRIC_EVIDENCE),
shares: verified("available", FACEBOOK_METRICS_VERIFIED_AT, FACEBOOK_METRIC_EVIDENCE),
// Four "never"s removed, because re-sourcing disproved three of them.
//
// "never" says the platform does not expose this. The only basis for these was the supplier's
// KPI matrix, which records what the supplier surfaces - a different claim, and the one this
// repo keeps confusing. Meta's own video_insights reference documents
// `total_video_view_total_time` ("the total time, in milliseconds, people viewed your
// videos"), `total_video_avg_time_watched`, and `total_video_complete_views` ("played for
// 97%, or more, of its length"). So watch time, average watched and completion are all
// exposed by Facebook, and we were telling customers they were not.
//
// They become unverified rather than available: the documentation establishes that Facebook
// publishes them, not that a Postdom request receives them, and claiming the second on
// evidence for the first is how the supplier citation got here in the first place. Unverified
// and never both render no number, so nothing a customer sees gets worse - but unverified
// stops asserting an absence that is false.
//
// saves is unverified for the weaker reason: it is absent from this endpoint, and absence
// from the one page we read is not evidence the platform lacks it.
saves: unverified(),
watch_time_s: unverified(),
avg_watch_pct: unverified(),
completion_pct: unverified(),
follower_delta: verified(
"estimable",
FACEBOOK_METRICS_VERIFIED_AT,
FACEBOOK_FOLLOWER_EVIDENCE
)
},
twitter: {
views: verified("available", TWITTER_METRICS_VERIFIED_AT, TWITTER_METRIC_EVIDENCE),
likes: verified("available", TWITTER_METRICS_VERIFIED_AT, TWITTER_METRIC_EVIDENCE),
comments: verified("available", TWITTER_METRICS_VERIFIED_AT, TWITTER_METRIC_EVIDENCE),
shares: verified("available", TWITTER_METRICS_VERIFIED_AT, TWITTER_METRIC_EVIDENCE),
// The provider KPI matrix lists Saves for X, so "never" would contradict our
// own cited evidence. It stays unverified until a real response returns a
// non-null saves field, the same bar TikTok and Instagram saves must clear.
saves: unverified(),
// X's own metrics page documents owner-only video playback quartiles - playback_0_count
// through playback_100_count - so a completion rate is derivable from what X exposes and
// "never" was false. It said the platform has no such thing; X documents the inputs for it.
//
// watch time and average watched are unverified for the weaker reason: X's page does not
// mention them, and a metric being absent from a page is not the platform declaring it
// absent. Both readings pointed the same way once the supplier's matrix stopped being the
// source - it was the only thing asserting these were "never".
watch_time_s: unverified(),
avg_watch_pct: unverified(),
completion_pct: unverified(),
follower_delta: verified(
"estimable",
TWITTER_METRICS_VERIFIED_AT,
TWITTER_FOLLOWER_EVIDENCE
)
},
// BUILD-8 classified LinkedIn's video facts, not its metrics, and nothing has
// been verified about what the provider returns for a LinkedIn post. Every
// metric is therefore unverified rather than guessed: "never" would claim we
// established an absence, and "available" would claim a metric we have never
// seen returned. Provider-contract research is the Marketplace lane's remit,
// so these move when that classification exists, not before.
linkedin: {
views: unverified(),
likes: unverified(),
comments: unverified(),
shares: unverified(),
saves: unverified(),
watch_time_s: unverified(),
avg_watch_pct: unverified(),
completion_pct: unverified(),
follower_delta: unverified()
},
// Same position as LinkedIn, and for a stronger reason. This PR classified
// Bluesky's media facts from the AT Protocol lexicon; it established nothing
// about what the provider returns for a Bluesky post, and the provider's own
// platform matrix rates Bluesky analytics only as "Limited" without saying
// which metrics that covers. "Limited" is not a metric list, so reading a
// "never" out of it would turn a vague word into an established absence.
// Every metric stays unverified until a real response is observed.
bluesky: {
views: unverified(),
likes: unverified(),
comments: unverified(),
shares: unverified(),
saves: unverified(),
watch_time_s: unverified(),
avg_watch_pct: unverified(),
completion_pct: unverified(),
follower_delta: unverified()
},
// Every metric unverified, for the same reason as Bluesky and on stricter
// grounds. Postdom has never read a Snapchat performance response, and the
// Spotlight publishing page says nothing about retrieval - it documents how a
// post is created, not what is later readable about it.
//
// Snap does publish insights elsewhere, which is exactly why none of these may
// be marked "never": an absence in the publishing doc is not evidence that a
// metric is unavailable, and "not documented on the page I read" is the
// weakest possible basis for telling a customer a platform does not expose
// something. Unverified until a real response is observed.
snapchat: {
views: unverified(),
likes: unverified(),
comments: unverified(),
shares: unverified(),
saves: unverified(),
watch_time_s: unverified(),
avg_watch_pct: unverified(),
completion_pct: unverified(),
follower_delta: unverified()
},
// Every metric unverified, and docs/verification/metric-availability.md records why. Meta
// documents how a Threads post is created; it says nothing about what is later readable about
// one. Creation facts are not retrieval facts, so none of them may be promoted here, and
// marking any metric "never" would turn "the page I read does not mention it" into "the
// platform does not expose it".
threads: {
views: unverified(),
likes: unverified(),
comments: unverified(),
shares: unverified(),
saves: unverified(),
watch_time_s: unverified(),
avg_watch_pct: unverified(),
completion_pct: unverified(),
follower_delta: unverified()
}
};
var metricAvailability = Object.fromEntries(
Object.entries(metricAvailabilityMetadata).map(([platform, metrics]) => [
platform,
Object.fromEntries(
Object.entries(metrics).map(([metric, metadata]) => [metric, metadata.state])
)
])
);
var nullableMetric = z7.number().nonnegative().nullable();
var postPerformanceSchema = z7.object({
// Nullable like every other metric, as of BUILD-30. These four were the only non-nullable ones,
// and the shape of the schema was doing a job that belongs to availability.
//
// Nothing published a zero - normalizePostPerformance nulls an unvouchable count and the guard
// below it dropped the whole row, so the four platforms with nothing vouchable produced no
// snapshot rather than a snapshot of noughts. The cost was the other direction: a metric a
// platform does not expose took every metric it does expose down with it.
//
// Bluesky is where that stops being hypothetical. app.bsky.feed.defs at the pinned commit gives
// postView `likeCount`, `replyCount`, `repostCount` and `quoteCount` and NO view or impression
// count - there is no `impression`, `viewCount`, `analytic` or `reach` anywhere in the file, and
// the one def that reads like one, `interactionSeen`, is a client reporting a view TO a feed
// generator rather than a count Bluesky returns. So the day Bluesky's likes and replies are
// verified, a non-nullable `views` would throw all three away to avoid publishing a number for
// a metric that structurally cannot exist.
//
// Null is the honest value for that, and the renderers state a reason for it. A zero would be a
// measurement; a dropped row is silence about metrics we do have.
views: nullableMetric,
likes: nullableMetric,
comments: nullableMetric,
shares: nullableMetric,
saves: nullableMetric,
watch_time_s: nullableMetric,
avg_watch_pct: nullableMetric,
completion_pct: nullableMetric,
follower_delta: z7.number().nullable(),
captured_at: z7.iso.datetime(),
platform: corePlatformSchema,
source: z7.enum(["webhook", "poll"]),
change_since_last: z7.record(z7.string(), z7.number().nullable()).optional()
});
// ../core/src/webhooks.ts
import { z as z8 } from "zod";
var outboundWebhookEventTypeSchema = z8.enum([
"post.published",
"post.failed",
"performance.updated"
]);
var outboundWebhookEndpointCreateSchema = z8.object({
url: z8.url(),
events: z8.array(outboundWebhookEventTypeSchema).min(1).max(3)
}).superRefine((value, context) => {
if (new Set(value.events).size !== value.events.length) {
context.addIssue({
code: "custom",
path: ["events"],
message: "Webhook event types must be unique"
});
}
});
var outboundWebhookEndpointUpdateSchema = z8.object({
url: z8.url().optional(),
events: z8.array(outboundWebhookEventTypeSchema).min(1).max(3).optional(),
enabled: z8.boolean().optional()
}).refine((value) => Object.keys(value).length > 0, {
message: "At least one webhook endpoint field is required"
}).superRefine((value, context) => {
if (value.events && new Set(value.events).size !== value.events.length) {
context.addIssue({
code: "custom",
path: ["events"],
message: "Webhook event types must be unique"
});
}
});
var outboundWebhookEndpointSchema = z8.object({
id: z8.uuid(),
url: z8.url(),
events: z8.array(outboundWebhookEventTypeSchema),
enabled: z8.boolean(),
created_at: z8.iso.datetime(),
updated_at: z8.iso.datetime()
});
var outboundWebhookEndpointCreatedSchema = outboundWebhookEndpointSchema.extend({
signing_secret: z8.string().startsWith("whsec_")
});
var outboundWebhookDeliveryStateSchema = z8.enum([
"pending",
"sending",
"retry",
"delivered",
"exhausted",
"cancelled"
]);
var outboundWebhookDeliverySchema = z8.object({
id: z8.uuid(),
event_id: z8.uuid(),
event_type: outboundWebhookEventTypeSchema,
endpoint_id: z8.uuid(),
state: outboundWebhookDeliveryStateSchema,
attempts: z8.number().int().nonnegative(),
next_attempt_at: z8.iso.datetime(),
delivered_at: z8.iso.datetime().nullable(),
last_http_status: z8.number().int().nullable(),
last_error: z8.string().nullable(),
created_at: z8.iso.datetime()
});
// ../core/src/feeds.ts
var agentPostFeedStatusSchema = z9.enum([
"draft",
"requires_approval",
"changes_requested",
"rejected",
"missed_approval",
"missed_schedule",
"scheduled",
"publishing",
"published",
"partial",
"failed",
"blocked",
"unknown"
]);
var agentFeedLimitSchema = z9.coerce.number().int().min(1).max(100).default(50);
var agentPostFeedQuerySchema = z9.object({
account_id: z9.string().min(1).max(255).optional(),
status: agentPostFeedStatusSchema.optional(),
updated_after: z9.iso.datetime().optional(),
cursor: z9.string().min(1).max(1024).optional(),
limit: agentFeedLimitSchema
});
var agentPostEventFeedQuerySchema = z9.object({
account_id: z9.string().min(1).max(255).optional(),
post_id: z9.uuid().optional(),
type: outboundWebhookEventTypeSchema.optional(),
created_after: z9.iso.datetime().optional(),
cursor: z9.string().min(1).max(1024).optional(),
limit: agentFeedLimitSchema
});
var agentPostTargetSchema = z9.object({
account_id: z9.string(),
platform: corePlatformSchema,
handle: z9.string().nullable()
});
var agentPostPublishSchema = z9.object({
id: z9.uuid(),
account_id: z9.string(),
platform: corePlatformSchema,
handle: z9.string().nullable(),
status: z9.enum(["queued", "sent", "published", "failed", "blocked", "unknown"]),
platform_post_id: z9.string().nullable(),
public_url: z9.url().nullable(),
error_code: z9.string().nullable(),
published_at: z9.iso.datetime().nullable(),
updated_at: z9.iso.datetime()
});
var agentPostFeedItemSchema = z9.object({
id: z9.uuid(),
version: z9.number().int().positive(),
status: agentPostFeedStatusSchema,
caption: z9.string(),
source: z9.string(),
scheduled_for: z9.iso.datetime().nullable(),
created_at: z9.iso.datetime(),
updated_at: z9.iso.datetime(),
plan_id: z9.uuid().nullable(),
targets: z9.array(agentPostTargetSchema),
publishes: z9.array(agentPostPublishSchema)
});
var agentPostFeedResponseSchema = z9.object({
posts: z9.array(agentPostFeedItemSchema),
next_cursor: z9.string().nullable()
});
var agentPostEventBaseSchema = z9.object({
id: z9.uuid(),
created_at: z9.iso.datetime()
});
var postOutcomeEventDataSchema = z9.object({
post_id: z9.uuid(),
status: z9.enum(["published", "partial", "failed"]),
destinations: z9.array(z9.object({
account_id: z9.string(),
platform: corePlatformSchema,
status: z9.enum(["queued", "sent", "published", "failed", "blocked", "unknown"]),
platform_post_id: z9.string().nullable(),
public_url: z9.url().nullable(),
error_code: z9.string().nullable()
}))
});
var performanceUpdatedEventDataSchema = z9.object({
post_id: z9.uuid(),
publish_id: z9.uuid(),
account_id: z9.string(),
platform: corePlatformSchema,
captured_at: z9.iso.datetime(),
metrics: postPerformanceSchema,
availability: z9.record(performanceMetricSchema, z9.object({
state: metricAvailabilitySchema,
verified_at: z9.iso.datetime().nullable(),
evidence: z9.string(),
reason: z9.string().nullable()
}))
});
var agentPostEventFeedItemSchema = z9.discriminatedUnion("type", [
agentPostEventBaseSchema.extend({
type: z9.literal("post.published"),
data: postOutcomeEventDataSchema
}),
agentPostEventBaseSchema.extend({
type: z9.literal("post.failed"),
data: postOutcomeEventDataSchema
}),
agentPostEventBaseSchema.extend({
type: z9.literal("performance.updated"),
data: performanceUpdatedEventDataSchema
})
]);
var agentPostEventFeedResponseSchema = z9.object({
events: z9.array(agentPostEventFeedItemSchema),
next_cursor: z9.string().nullable()
});
// ../core/src/invitation-operations.ts
import { z as z10 } from "zod";
var invitationOperationRequestSchema = z10.object({
operation_id: z10.uuid(),
email: z10.email().trim().toLowerCase(),
role: z10.enum(["admin", "member"])
}).strict();
var invitationAuthPhaseSchema = z10.enum(["not_started", "started", "generated", "rejected", "unknown"]);
var invitationEmailPhaseSchema = z10.enum(["not_started", "started", "accepted", "not_attempted", "rejected", "unknown"]);
var invitationOperationSchema = z10.object({
format: z10.literal("invitation-operation-v1"),
operation_id: z10.uuid(),
org_id: z10.uuid(),
actor_id: z10.uuid(),
email: z10.email(),
requested_role: z10.enum(["admin", "member"]),
invite_role: z10.enum(["admin", "member"]).nullable(),
creation: z10.enum(["created", "already_pending", "not_created"]),
reason: z10.enum(["already_member", "not_configured", "authority_changed", "invite_ineligible"]).nullable(),
invite_id: z10.uuid().nullable(),
invite_state: z10.enum(["pending", "expired", "expired_replaced", "accepted", "revoked"]).nullable(),
expires_at: z10.iso.datetime().nullable(),
auth_phase: invitationAuthPhaseSchema,
email_phase: invitationEmailPhaseSchema,
claimed_at: z10.iso.datetime().nullable(),
auth_updated_at: z10.iso.datetime(),
email_updated_at: z10.iso.datetime(),
created_at: z10.iso.datetime(),
link_recoverable: z10.literal(false)
}).strict().superRefine((value, context) => {
if (value.creation === "not_created" !== (value.invite_id === null)) context.addIssue({ code: "custom", message: "Creation and invite reference disagree" });
if (value.invite_id === null !== (value.invite_state === null) || value.invite_id === null !== (value.expires_at === null)) context.addIssue({ code: "custom", message: "Invite metadata is incomplete" });
if (value.invite_id === null !== (value.invite_role === null)) context.addIssue({ code: "custom", message: "Actual invitation role is missing or misattributed" });
if (value.email_phase === "accepted" && value.auth_phase !== "generated") context.addIssue({ code: "custom", message: "Email acceptance requires a generated auth link" });
});
var invitationOperationListSchema = z10.object({
operations: z10.array(invitationOperationSchema).max(20),
next_cursor: z10.string().max(512).nullable()
}).strict();
var invitationOperationResponseSchema = z10.object({
operation: invitationOperationSchema,
invite_url: z10.url().refine((value) => {
try {
const url = new URL(value);
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
} catch {
return false;
}
}, "Invitation links must use HTTP(S) without embedded credentials").nullable(),
replay: z10.boolean()
}).strict().superRefine((value, context) => {
if (value.invite_url !== null && (value.replay || value.operation.creation !== "created")) {
context.addIssue({ code: "custom", path: ["invite_url"], message: "Only an original newly created invitation may return its in-memory link" });
}
});
// ../core/src/billing-handoff.ts
import { z as z11 } from "zod";
var billingHandoffRequestSchema = z11.discriminatedUnion("kind", [
z11.object({
operation_id: z11.uuid(),
kind: z11.literal("checkout"),
plan: z11.enum(["starter", "growth", "scale"]),
interval: z11.enum(["month", "year"])
}).strict(),
z11.object({ operation_id: z11.uuid(), kind: z11.literal("portal") }).strict()
]);
var billingHandoffOperationSchema = z11.object({
format: z11.literal("billing-handoff-v1"),
operation_id: z11.uuid(),
org_id: z11.uuid(),
actor_id: z11.uuid(),
kind: z11.enum(["checkout", "portal"]),
plan: z11.enum(["starter", "growth", "scale"]).nullable(),
interval: z11.enum(["month", "year"]).nullable(),
migration_phase: z11.enum([
"not_required",
"not_started",
"started",
"completed",
"unknown"
]),
session_phase: z11.enum(["not_started", "started", "created", "unknown"]),
session_state: z11.enum(["open", "complete", "expired"]).nullable(),
active: z11.boolean(),
refused: z11.boolean(),
created_at: z11.iso.datetime(),
updated_at: z11.iso.datetime(),
link_recoverable: z11.boolean()
}).strict().superRefine((value, context) => {
const checkout = value.kind === "checkout";
if (checkout ? value.plan === null || value.interval === null : value.plan !== null || value.interval !== null || value.session_state !== null)
context.addIssue({
code: "custom",
message: "Handoff kind and intent differ"
});
if (value.link_recoverable && (!checkout || value.session_phase !== "created" || value.session_state !== "open" || value.refused))
context.addIssue({
code: "custom",
message: "No recoverable Checkout is confirmed"
});
if (!value.active && !value.refused && !(checkout ? ["complete", "expired"].includes(value.session_state ?? "") : value.session_phase === "created" && ["not_required", "completed"].includes(value.migration_phase)))
context.addIssue({
code: "custom",
message: "Unresolved handoff cannot be released"
});
});
var billingHandoffListSchema = z11.object({
operations: z11.array(billingHandoffOperationSchema).max(20),
next_cursor: z11.string().max(512).nullable(),
workspace_blocked: z11.boolean()
}).strict();
function isBillingHandoffUrl(value) {
try {
const url = new URL(value);
return url.protocol === "https:" && !url.username && !url.password && !url.port && ["checkout.stripe.com", "billing.stripe.com"].includes(url.hostname);
} catch {
return false;
}
}
var billingHandoffResponseSchema = z11.object({
operation: billingHandoffOperationSchema,
replay: z11.boolean(),
url: z11.string().refine(isBillingHandoffUrl).nullable()
}).strict().superRefine((value, context) => {
if (value.url && isBillingHandoffUrl(value.url) && new URL(value.url).hostname !== (value.operation.kind === "checkout" ? "checkout.stripe.com" : "billing.stripe.com"))
context.addIssue({
code: "custom",
message: "Handoff destination differs from its operation"
});
if (value.url && (value.operation.refused || value.operation.session_phase !== "created" || value.operation.kind === "portal" && value.replay || value.operation.kind === "checkout" && value.operation.session_state !== "open"))
context.addIssue({
code: "custom",
message: "This operation cannot expose a handoff URL"
});
});
// ../core/src/media.ts
import { z as z12 } from "zod";
var MEDIA_UPLOAD_URL_TTL_SECONDS = 5 * 60;
var MEDIA_UPLOAD_MAX_BYTES_PER_HOUR = platformLimits.global.media_max_bytes.value * 4;
var mediaHandleSchema = z12.string().regex(
/^pd_media_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
"media_handle must be a Postdom media handle"
);
var mediaUploadRequestSchema = z12.object({
content_type: z12.enum(platformLimits.global.video_mime_types.value),
size_bytes: z12.number().int().positive().max(platformLimits.global.media_max_bytes.value),
platforms: z12.array(corePlatformSchema).min(1).max(corePlatformSchema.options.length)
}).superRefine((value, context) => {
if (new Set(value.platforms).size !== value.platforms.length) {
context.addIssue({
code: "custom",
path: ["platforms"],
message: "Target platforms must be unique"
});
}
});
var mediaUploadVideoMetadataSchema = z12.object({
width_pixels: z12.number().int().positive(),
height_pixels: z12.number().int().positive(),
duration_seconds: z12.number().int().positive()
});
var mediaUploadPreflightRequestSchema = mediaUploadRequestSchema.safeExtend(mediaUploadVideoMetadataSchema.shape).superRefine((value, context) => {
const addIssue = (path, message) => {
context.addIssue({ code: "custom", path: [path], message });
};
if (value.platforms.includes("tiktok")) {
const minimum = shortFormVideoLimits.tiktok.resolution_min_pixels.value;
const maximum = shortFormVideoLimits.tiktok.resolution_max_pixels.value;
if (value.width_pixels < minimum.width) {
addIssue(
"width_pixels",
`TikTok requires width of at least ${minimum.width} pixels`
);
}
if (value.height_pixels < minimum.height) {
addIssue(
"height_pixels",
`TikTok requires height of at least ${minimum.height} pixels`
);
}
if (value.width_pixels > maximum.width) {
addIssue(
"width_pixels",
`TikTok requires width no greater than ${maximum.width} pixels`
);
}
if (value.height_pixels > maximum.height) {
addIssue(
"height_pixels",
`TikTok requires height no greater than ${maximum.height} pixels`
);
}
if (value.duration_seconds > shortFormVideoLimits.tiktok.video_max_seconds.value) {
addIssue(
"duration_seconds",
`TikTok videos may not exceed ${shortFormVideoLimits.tiktok.video_max_seconds.value} seconds`
);
}
}
if (value.platforms.includes("instagram")) {
const ratio = value.width_pixels / value.height_pixels;
const ratioLimit = shortFormVideoLimits.instagram.aspect_ratio.value;
if (value.width_pixels > shortFormVideoLimits.instagram.resolution_max_horizontal_pixels.value) {
addIssue(
"width_pixels",
`Instagram Reels may not exceed ${shortFormVideoLimits.instagram.resolution_max_horizontal_pixels.value} horizontal pixels`
);
}
if (ratio < ratioLimit.minimum || ratio > ratioLimit.maximum) {
addIssue(
"width_pixels",
`Instagram Reels aspect ratio must be between ${ratioLimit.minimum}:1 and ${ratioLimit.maximum}:1`
);
}
if (value.duration_seconds < shortFormVideoLimits.instagram.video_min_seconds.value) {
addIssue(
"duration_seconds",
`Instagram Reels must be at least ${shortFormVideoLimits.instagram.video_min_seconds.value} seconds`
);
}
if (value.duration_seconds > shortFormVideoLimits.instagram.video_max_seconds.value) {
addIssue(
"duration_seconds",
`Instagram Reels may not exceed ${shortFormVideoLimits.instagram.video_max_seconds.value} seconds`
);
}
}
if (value.platforms.includes("youtube")) {
if (value.width_pixels / value.height_pixels > shortFormVideoLimits.youtube.aspect_ratio.value.maximum_width_to_height_ratio) {
addIssue(
"width_pixels",
"YouTube Shorts must be square or vertical (width may not exceed height)"
);
}
if (value.duration_seconds > shortFormVideoLimits.youtube.shorts_qualification.value.maximum_duration_seconds) {
addIssue(
"duration_seconds",
`YouTube Shorts may not exceed ${shortFormVideoLimits.youtube.shorts_qualification.value.maximum_duration_seconds} seconds`
);
}
}
if (value.platforms.includes("facebook")) {
if (value.duration_seconds < shortFormVideoLimits.facebook.video_min_seconds.value) {
addIssue(
"duration_seconds",
`Facebook Reels must be at least ${shortFormVideoLimits.facebook.video_min_seconds.value} seconds`
);
}
if (value.duration_seconds > shortFormVideoLimits.facebook.video_max_seconds.value) {
addIssue(
"duration_seconds",
`Facebook Reels may not exceed ${shortFormVideoLimits.facebook.video_max_seconds.value} seconds`
);
}
}
if (value.platforms.includes("twitter")) {
const minimum = shortFormVideoLimits.twitter.resolution_min_pixels.value;
if (value.width_pixels < minimum.width) {
addIssue("width_pixels", `X requires width of at least ${minimum.width} pixels`);
}
if (value.height_pixels < minimum.height) {
addIssue("height_pixels", `X requires height of at least ${minimum.height} pixels`);
}
if (value.duration_seconds > shortFormVideoLimits.twitter.video_max_seconds.value) {
addIssue(
"duration_seconds",
`X videos may not exceed ${shortFormVideoLimits.twitter.video_max_seconds.value} seconds`
);
}
}
if (value.platforms.includes("snapchat")) {
const minimumSeconds = shortFormVideoLimits.snapchat.video_min_seconds.value;
if (value.duration_seconds < minimumSeconds) {
addIssue(
"duration_seconds",
`Snapchat Spotlight requires at least ${minimumSeconds} seconds`
);
}
const maximumSeconds = shortFormVideoLimits.snapchat.video_max_seconds.value;
if (value.duration_seconds > maximumSeconds) {
addIssue(
"duration_seconds",
`Snapchat Spotlight may not exceed ${maximumSeconds} seconds`
);
}
const minimumPixels = shortFormVideoLimits.snapchat.resolution_min_pixels.value;
if (value.width_pixels < minimumPixels.width) {
addIssue(
"width_pixels",
`Snapchat Spotlight requires width of at least ${minimumPixels.width} pixels`
);
}
if (value.height_pixels < minimumPixels.height) {
addIssue(
"height_pixels",
`Snapchat Spotlight requires height of at least ${minimumPixels.height} pixels`
);
}
}
const media = {
size_bytes: value.size_bytes,
content_type: value.content_type
};
const perDestination = value.platforms.map(
(platform) => mediaDeliveryViolations([platform], media)
);
if (perDestination.every((violations) => violations.length > 0)) {
for (const violation of perDestination.flat()) {
context.addIssue({
code: "custom",
path: [violation.fact === "video_max_bytes" ? "size_bytes" : "content_type"],
message: violation.message
});
}
}
if (value.platforms.includes("threads")) {
const maximumSeconds = shortFormVideoLimits.threads.video_max_seconds.value;
if (value.duration_seconds > maximumSeconds) {
addIssue("duration_seconds", `Threads may not exceed ${maximumSeconds} seconds`);
}
const maximumWidth = shortFormVideoLimits.threads.resolution_max_horizontal_pixels.value;
if (value.width_pixels > maximumWidth) {
addIssue("width_pixels", `Threads may not exceed ${maximumWidth} horizontal pixels`);
}
const ratio = shortFormVideoLimits.threads.aspect_ratio.value;
const actual = value.width_pixels / value.height_pixels;
if (actual < ratio.minimum || actual > ratio.maximum) {
addIssue(
"width_pixels",
`Threads aspect ratio must be between ${ratio.minimum}:1 and ${ratio.maximum}:1`
);
}
}
});
function mediaDeliveryViolations(platforms, media) {
const violations = [];
const served = media.content_type?.toLowerCase() ?? null;
for (const platform of new Set(platforms)) {
const limits = shortFormVideoLimits[platform];
const mime = "video_mime_types" in limits ? limits.video_mime_types : void 0;
if (mime?.classification === "enforced") {
const accepted = mime.value.map((type) => type.toLowerCase());
if (served !== null && !accepted.includes(served)) {
violations.push({
platform,
fact: "video_mime_types",
message: `${platformLabel(platform)} accepts ${accepted.join(", ")} only`
});
}
}
const maxBytes = "video_max_bytes" in limits ? limits.video_max_bytes : void 0;
if (maxBytes?.classification === "enforced") {
const ceiling = maxBytes.value;
if (media.size_bytes !== null && media.size_bytes > ceiling) {
violations.push({
platform,
fact: "video_max_bytes",
message: `A video larger than ${ceiling} bytes cannot be published to ${platformLabel(platform)}`
});
}
}
}
return violations;
}
var mediaUploadResponseSchema = z12.object({
media_handle: mediaHandleSchema,
upload_url: z12.url(),
method: z12.literal("PUT"),
headers: z12.object({
"Content-Type": z12.string(),
"Content-Length": z12.string().regex(/^\d+$/)
}),
expires_at: z12.iso.datetime()
});
var mediaUploadStatusSchema = z12.enum(["pending", "stored", "failed"]);
var mediaStatusResponseSchema = z12.object({
media_handle: mediaHandleSchema,
status: mediaUploadStatusSchema,
content_type: z12.enum(platformLimits.global.video_mime_types.value),
size_bytes: z12.number().int().positive(),
media_url: z12.url().nullable(),
failure_reason: z12.string().nullable()
});
var publishMediaSourceSchema = z12.object({
video_url: z12.url().optional(),
media_handle: mediaHandleSchema.optional()
}).superRefine((value, context) => {
if (Number(Boolean(value.video_url)) + Number(Boolean(value.media_handle)) !== 1) {
context.addIssue({
code: "custom",
path: ["media_handle"],
message: "Provide exactly one of media_handle or video_url"
});
}
});
var postMediaSourceSchema = z12.object({
media_url: z12.url().optional(),
media_handle: mediaHandleSchema.optional()
}).superRefine((value, context) => {
if (value.media_url && value.media_handle) {
context.addIssue({
code: "custom",
path: ["media_handle"],
message: "Provide at most one of media_handle or media_url"
});
}
});
var mediaOperations = {
createUpload: {
method: "POST",
path: "/media/uploads",
request: mediaUploadPreflightRequestSchema,
response: mediaUploadResponseSchema
},
getUpload: {
method: "GET",
path: "/media/{media_handle}",
params: z12.object({ media_handle: mediaHandleSchema }),
response: mediaStatusResponseSchema
}
};
// ../core/src/policy-reasons.ts
import { z as z13 } from "zod";
var policyReasonSchema = z13.enum([
"publishing_disabled",
"policy_unavailable",
"policy_invalid",
"target_mismatch",
"agents_paused",
"authorization_changed",
"prior_submission_uncertain",
"execution_interrupted",
"media_invalid",
"account_disconnected",
"trust_level_too_low"
]);
var POLICY_REASONS = policyReasonSchema.options;
// ../core/src/rest.ts
import { z as z14 } from "zod";
var postBodySchema = z14.object({
caption: z14.string().max(4e3),
targets: z14.array(platformTargetSchema).min(1),
publish_at: z14.iso.datetime().optional(),
plan_id: z14.uuid().optional(),
agent_context: agentContextSchema.optional()
}).and(postMediaSourceSchema).superRefine((value, context) => {
const hasMedia = Boolean(value.media_url) || Boolean(value.media_handle);
if (!hasMedia && value.caption.trim().length === 0) {
context.addIssue({
code: "custom",
path: ["caption"],
message: "A post needs media, text, or both. This request has neither."
});
}
});
var planBodySchema = z14.object({
title: z14.string().trim().min(1).max(120),
objective: z14.string().trim().min(1).max(1e3),
starts_at: z14.iso.datetime(),
ends_at: z14.iso.datetime(),
max_posts: z14.number().int().min(1).max(20),
brief_version: z14.number().int().positive().optional(),
targets: z14.array(z14.object({
account_id: z14.string().min(1),
platform: corePlatformSchema
})).min(1),
agent_context: agentContextSchema.optional()
}).refine((value) => new Date(value.starts_at) < new Date(value.ends_at), {
path: ["ends_at"],
message: "ends_at must be after starts_at"
}).refine(
(value) => new Date(value.ends_at).getTime() - new Date(value.starts_at).getTime() <= 14 * 24 * 60 * 60 * 1e3,
{ path: ["ends_at"], message: "Plan windows cannot exceed 14 days" }
).superRefine((value, context) => {
const keys = value.targets.map((target) => `${target.account_id}:${target.platform}`);
if (new Set(keys).size !== keys.length) {
context.addIssue({
code: "custom",
path: ["targets"],
message: "Plan targets must be unique"
});
}
});
var connectBodySchema = z14.object({
platform: corePlatformSchema,
redirect_url: z14.url()
});
var connectCredentialsBodySchema = z14.object({
identifier: z14.string().trim().min(1),
app_password: z14.string().min(1)
});
var performanceWindowSchema = z14.enum(["7d", "30d"]);
var bestPostsQuerySchema = z14.object({
metric: performanceMetricSchema,
window: performanceWindowSchema.default("7d")
});
// ../core/src/state-machine.ts
var postTransitions = {
draft: ["requires_approval", "rejected", "scheduled", "publishing"],
requires_approval: ["changes_requested", "rejected", "scheduled", "publishing", "failed"],
changes_requested: ["requires_approval"],
rejected: [],
scheduled: ["publishing", "failed", "blocked", "unknown"],
publishing: ["published", "partial", "failed", "blocked", "unknown"],
published: [],
partial: ["publishing", "published", "failed"],
failed: ["scheduled", "publishing"],
blocked: [],
unknown: ["published", "partial", "failed"]
};
var TERMINAL_POST_STATUSES = Object.freeze(
Object.keys(postTransitions).filter((status) => postTransitions[status].length === 0)
);
var MODELLED_POST_STATUSES = Object.freeze(Object.keys(postTransitions));
// ../core/src/timezone.ts
import { z as z15 } from "zod";
var accountTimezoneSourceSchema = z15.enum(["fallback", "configured"]);
function isIanaTimezone(value) {
try {
new Intl.DateTimeFormat("en", { timeZone: value }).format();
return true;
} catch {
return false;
}
}
var ianaTimezoneSchema = z15.string().trim().min(1).max(100).refine(isIanaTimezone, "Use a valid IANA timezone such as Australia/Sydney.");
// ../core/src/conditional-defaults.ts
import { z as z16 } from "zod";
var quietTime = z16.string().regex(/^([01][0-9]|2[0-3]):[0-5][0-9]$/);
var body = z16.object({
trust_level: z16.number().int().min(0).max(3),
paused: z16.boolean(),
timezone: ianaTimezoneSchema,
maximum_posts_per_day: z16.number().int().min(1).max(100),
quiet_hours_start: quietTime.nullable().default(PRODUCT_WORKSPACE_POLICY_DEFAULTS.quietHoursStart),
quiet_hours_end: quietTime.nullable().default(PRODUCT_WORKSPACE_POLICY_DEFAULTS.quietHoursEnd)
});
var paired = (value) => value.quiet_hours_start === null === (value.quiet_hours_end === null);
var workspaceDefaultsBodySchema = body.refine(paired, {
message: "quiet-hours start and end must both be set or both be null"
});
var conditionalDefaultsWriteSchema = body.extend({
expected_version: z16.number().int().positive().max(2147483647).nullable(),
expected_org_id: z16.uuid()
}).strict().refine(paired, { message: "quiet-hours start and end must both be set or both be null" });
var conditionalDefaultsSnapshotSchema = z16.object({
format: z16.literal("workspace-defaults-conditional-v1"),
org_id: z16.uuid(),
persisted_version: z16.number().int().positive().max(2147483647).nullable(),
defaults: z16.object({
trustLevel: z16.union([z16.literal(0), z16.literal(1), z16.literal(2), z16.literal(3)]),
paused: z16.boolean(),
timezone: ianaTimezoneSchema,
maximumPostsPerDay: z16.number().int().min(1).max(100),
quietHoursStart: quietTime.nullable(),
quietHoursEnd: quietTime.nullable(),
version: z16.number().int().positive().max(2147483647),
updatedAt: z16.iso.datetime({ offset: true })
}).strict().refine((value) => value.quietHoursStart === null === (value.quietHoursEnd === null))
}).strict().refine((value) => value.persisted_version === null || value.persisted_version === value.defaults.version);
// ../core/src/pre-submit-artifact.ts
import { z as z17 } from "zod";
var PRE_SUBMIT_CONTENT_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
var identifier = z17.string().regex(/^[a-zA-Z0-9_-]{1,256}$/);
var revisionPattern = /^[1-9][0-9]{0,18}$/;
var artifactRevisionSchema = z17.string().regex(revisionPattern).refine(
(value) => revisionPattern.test(value) && BigInt(value) <= 9223372036854775807n
);
var preSubmitDraftFieldsSchema = z17.object({
caption: z17.string().max(5e4),
youtubeTitle: z17.string().max(5e3),
youtubeDescription: z17.string().max(5e4),
selectedIds: z17.array(identifier).max(100),
visibility: z17.record(identifier, z17.string().max(128)).refine((value) => Object.keys(value).length <= 100),
whenLocal: z17.string().max(32),
timezone: z17.string().max(128).refine(isIanaTimezone)
}).strict();
var preSubmitArtifactScopeSchema = z17.object({
orgId: z17.uuid(),
actorId: z17.uuid(),
artifactId: z17.uuid()
}).strict();
var mutation = { mutationId: z17.uuid() };
var content = {
fields: preSubmitDraftFieldsSchema,
mediaHandle: mediaHandleSchema.nullable()
};
var createPreSubmitArtifactSchema = preSubmitArtifactScopeSchema.extend({ ...mutation, ...content }).strict();
var updatePreSubmitArtifactSchema = createPreSubmitArtifactSchema.extend({ expectedRevision: artifactRevisionSchema }).strict();
var discardPreSubmitArtifactSchema = preSubmitArtifactScopeSchema.extend({ ...mutation, expectedRevision: artifactRevisionSchema }).strict();
var preSubmitMediaEvidenceSchema = z17.object({
handle: mediaHandleSchema,
source: z17.literal("stored_media_record"),
checked_at: z17.iso.datetime(),
content_type: z17.enum(["video/mp4", "video/quicktime"]),
size_bytes: artifactRevisionSchema,
width_pixels: z17.number().int().positive().nullable(),
height_pixels: z17.number().int().positive().nullable(),
duration_seconds: z17.number().positive().nullable()
}).strict();
var preSubmitArtifactPayloadSchema = z17.object({
fields: preSubmitDraftFieldsSchema,
media: preSubmitMediaEvidenceSchema.nullable()
}).strict();
var preSubmitArtifactSchema = z17.object({
format: z17.literal("pre-submit-artifact-v1"),
id: z17.uuid(),
org_id: z17.uuid(),
creator_id: z17.uuid(),
revision: artifactRevisionSchema,
state: z17.enum(["editable", "submitted", "deleted"]),
submitted_post_id: z17.uuid().nullable().default(null),
deleted_reason: z17.enum(["discarded", "expired"]).nullable(),
payload: preSubmitArtifactPayloadSchema.nullable(),
created_at: z17.iso.datetime(),
updated_at: z17.iso.datetime(),
expires_at: z17.iso.datetime()
}).strict().superRefine((value, context) => {
if (value.state === "editable" && (value.payload === null || value.deleted_reason !== null || value.submitted_post_id !== null) || value.state === "submitted" && (value.payload !== null || value.deleted_reason !== null || value.submitted_post_id === null) || value.state === "deleted" && (value.payload !== null || value.deleted_reason === null)) {
context.addIssue({
code: "custom",
message: "Deleted content must be erased"
});
}
if (Date.parse(value.created_at) > Date.parse(value.updated_at) || Date.parse(value.expires_at) <= Date.parse(value.created_at)) {
context.addIssue({
code: "custom",
message: "Invalid artifact lifetime"
});
}
});
var preSubmitMutationOutcomeSchema = z17.enum([
"created",
"updated",
"discarded",
"revision_conflict",
"not_editable",
"artifact_exists"
]);
var preSubmitMutationResultSchema = z17.object({
replay: z17.boolean(),
outcome: preSubmitMutationOutcomeSchema,
outcome_revision: artifactRevisionSchema,
// Replay reports the original outcome but only the CURRENT authorized row.
// Receipts never contain an older payload that could resurrect erased text.
artifact: preSubmitArtifactSchema
}).strict();
// ../core/src/pre-submit-artifact-http.ts
import { z as z18 } from "zod";
var serverScope = { orgId: true, actorId: true };
var preSubmitArtifactCommandSchema = z18.discriminatedUnion(
"operation",
[
createPreSubmitArtifactSchema.omit(serverScope).extend({ operation: z18.literal("create") }).strict(),
updatePreSubmitArtifactSchema.omit(serverScope).extend({ operation: z18.literal("update") }).strict(),
discardPreSubmitArtifactSchema.omit(serverScope).extend({ operation: z18.literal("discard") }).strict()
]
);
// ../core/src/composer-target.ts
var YOUTUBE_TITLE_MAX = platformLimits.youtube.title_max_chars.value;
// ../core/src/pre-submit-submission.ts
import { z as z19 } from "zod";
var artifactSubmissionIntentSchema = z19.enum([
"submit_for_review",
"request_authorized_scheduling"
]);
var artifactSubmissionPreviewRequestSchema = preSubmitArtifactScopeSchema.extend({
expectedRevision: artifactRevisionSchema,
intent: artifactSubmissionIntentSchema
}).strict();
var artifactSubmissionRequestSchema = artifactSubmissionPreviewRequestSchema.extend({
mutationId: z19.uuid(),
previewDigest: z19.string().regex(/^[0-9a-f]{64}$/)
}).strict();
var artifactSubmissionCommandSchema = artifactSubmissionRequestSchema.omit({ orgId: true, actorId: true }).strict();
var artifactSubmissionPreviewCommandSchema = artifactSubmissionPreviewRequestSchema.omit({ orgId: true, actorId: true }).strict();
var artifactSubmissionLinkSchema = z19.object({
artifact_id: z19.uuid(),
org_id: z19.uuid(),
creator_id: z19.uuid(),
submitted_revision: artifactRevisionSchema,
mutation_id: z19.uuid(),
intent: artifactSubmissionIntentSchema,
preview_digest: z19.string().regex(/^[0-9a-f]{64}$/),
post_id: z19.uuid(),
post_version: z19.number().int().positive(),
created_at: z19.iso.datetime()
}).strict();
var artifactSubmissionResultSchema = z19.object({
replay: z19.boolean(),
submission: artifactSubmissionLinkSchema,
// Immutable submission is not a claim about today's canonical post state.
current_post: z19.object({
id: z19.uuid(),
version: z19.number().int().positive(),
status: agentPostFeedStatusSchema,
scheduled_for: z19.iso.datetime().nullable()
}).strict()
}).strict().superRefine((value, context) => {
if (value.current_post.id !== value.submission.post_id || value.current_post.version < value.submission.post_version)
context.addIssue({
code: "custom",
path: ["current_post"],
message: "Current post must match the immutable submission and cannot precede it"
});
});
var artifactSubmissionReconciliationSchema = z19.discriminatedUnion("outcome", [
z19.object({
outcome: z19.literal("submitted"),
command: artifactSubmissionRequestSchema,
result: artifactSubmissionResultSchema
}).strict(),
z19.object({
outcome: z19.literal("fenced"),
command: artifactSubmissionRequestSchema,
revision: artifactRevisionSchema
}).strict(),
z19.object({
outcome: z19.literal("terminal"),
command: artifactSubmissionRequestSchema,
revision: artifactRevisionSchema,
state: z19.enum(["deleted", "expired", "submitted"])
}).strict()
]).superRefine((value, context) => {
if (value.outcome === "fenced" && BigInt(value.revision) <= BigInt(value.command.expectedRevision))
context.addIssue({
code: "custom",
message: "A fence must follow the original revision"
});
if (value.outcome === "submitted") {
const a = value.command, b = value.result.submission;
if (a.orgId !== b.org_id || a.actorId !== b.creator_id || a.artifactId !== b.artifact_id || a.expectedRevision !== b.submitted_revision || a.mutationId !== b.mutation_id || a.intent !== b.intent || a.previewDigest !== b.preview_digest)
context.addIssue({
code: "custom",
message: "Reconciliation must match the original submission"
});
}
});
var artifactSubmissionPreviewSchema = z19.object({
format: z19.literal("artifact-submission-preview-v1"),
artifact_id: z19.uuid(),
revision: artifactRevisionSchema,
intent: artifactSubmissionIntentSchema,
digest: z19.string().regex(/^[0-9a-f]{64}$/),
proposed_at: z19.iso.datetime().nullable(),
canonical: postBodySchema,
application_assertions: z19.array(
z19.object({
account_id: z19.string(),
field: z19.string(),
value: z19.union([z19.string(), z19.boolean()])
}).strict()
),
notes: z19.array(z19.string())
}).strict();
// ../core/src/native-job.ts
import { z as z21 } from "zod";
// ../core/src/workspace-learning.ts
import { z as z20 } from "zod";
var LEARNING_LIMITS = Object.freeze({
posts: 20,
tailSnapshots: 5,
minimumGroup: 3,
captionBoundary: 80,
minimumAgeHours: 72,
maximumAgeHours: 78,
retentionDays: 90,
cadenceMs: 24 * 60 * 6e4
});
var learningDefinitionSchema = z20.literal("tiktok-reported-views-v1");
var learningCandidateSchema = z20.enum(["shorten_caption", "abstain"]);
var learningReasonSchema = z20.enum([
"observed_association",
"insufficient_data",
"customer_constraint",
"prior_trial_not_supportive",
"no_directional_association",
"source_unavailable"
]);
var digest = z20.string().regex(/^[a-f0-9]{64}$/);
var revision2 = z20.string().regex(/^[1-9][0-9]*$/);
var learningSnapshotSchema = z20.object({
id: revision2,
hash: digest,
capturedAt: z20.iso.datetime(),
normalized: postPerformanceSchema
}).strict();
var learningSourcePostSchema = z20.object({
postId: z20.uuid(),
publishId: z20.uuid(),
accountId: z20.uuid(),
orgId: z20.uuid(),
platform: z20.string(),
format: z20.enum(["video", "text"]),
caption: z20.string().max(16384),
postVersion: revision2,
handoffVersion: revision2.nullable(),
requestHash: digest,
executionEvidence: z20.string(),
publishedAt: z20.iso.datetime().nullable(),
selectedSnapshot: learningSnapshotSchema.nullable(),
latestSnapshots: z20.array(learningSnapshotSchema).max(LEARNING_LIMITS.tailSnapshots)
}).strict();
var learningContextSchema = z20.object({
rejectedShortCaption: z20.boolean(),
priorTrialNotSupportive: z20.boolean(),
excludedPosts: z20.array(
z20.object({
postId: z20.uuid(),
reason: z20.enum([
"customer_promoted",
"customer_excluded",
"source_conflict"
])
}).strict()
).max(LEARNING_LIMITS.posts)
}).strict();
var learningAnalysisInputSchema = z20.object({
orgId: z20.uuid(),
accountId: z20.uuid(),
scopeId: z20.uuid(),
scopeRevision: revision2,
authorizationVersion: revision2,
enabled: z20.boolean(),
sourceConnected: z20.boolean(),
definition: learningDefinitionSchema,
asOf: z20.iso.datetime(),
posts: z20.array(learningSourcePostSchema).max(LEARNING_LIMITS.posts),
retained: learningContextSchema
}).strict().superRefine((v, ctx) => {
for (const field of ["postId", "publishId"]) {
if (new Set(v.posts.map((p) => p[field])).size !== v.posts.length)
ctx.addIssue({
code: "custom",
path: ["posts"],
message: `Duplicate ${field}`
});
}
});
var learningModelResultSchema = z20.object({
candidate: learningCandidateSchema,
reason: learningReasonSchema,
sourcePostIds: z20.array(z20.uuid()).max(LEARNING_LIMITS.posts)
}).strict();
// ../core/src/native-job.ts
var NATIVE_JOB_LIMITS = Object.freeze({
items: 5,
attempts: 3,
deadlineMs: 10 * 6e4,
leaseMs: 3e4,
contextBytes: 64 * 1024,
messageBytes: 8 * 1024,
resultBytes: 128 * 1024,
messages: 100,
eventsPerJob: 100,
retentionMs: 30 * 24 * 60 * 6e4
});
var utf8 = new TextEncoder();
var boundedText = (bytes) => z21.string().refine((value) => utf8.encode(value).byteLength <= bytes, {
message: `UTF-8 content exceeds ${bytes} bytes`
});
var nativeDigestSchema = z21.string().regex(/^[a-f0-9]{64}$/);
var nativeScopeSchema = z21.object({ orgId: z21.uuid(), actorId: z21.uuid(), conversationId: z21.uuid() }).strict();
var nativeJobScopeSchema = nativeScopeSchema.extend({ jobId: z21.uuid() }).strict();
var nativeOwnerSchema = nativeJobScopeSchema.extend({ ownerId: z21.uuid(), generation: artifactRevisionSchema }).strict();
var nativeItemInputSchema = z21.object({
itemId: z21.uuid(),
artifactId: z21.uuid(),
mutationId: z21.uuid(),
fields: preSubmitDraftFieldsSchema,
mediaHandle: mediaHandleSchema.nullable()
}).strict();
var nativeCreateJobSchema = nativeJobScopeSchema.extend({
commandId: z21.uuid(),
stepId: z21.uuid(),
messageId: z21.uuid(),
expectedRevision: artifactRevisionSchema,
operation: z21.literal("draft_captions"),
message: boundedText(NATIVE_JOB_LIMITS.messageBytes).refine(
(value) => value.trim().length > 0,
"A deliberate user instruction is required"
),
context: boundedText(NATIVE_JOB_LIMITS.contextBytes),
// Internal learning-service binding; ordinary dashboard bodies exclude it.
learning: z21.object({
scopeId: z21.uuid(),
scopeRevision: artifactRevisionSchema,
recommendationId: z21.uuid(),
inputFingerprint: nativeDigestSchema,
experimentId: z21.uuid()
}).strict().optional(),
learningContext: z21.object({
scopeId: z21.uuid(),
scopeRevision: artifactRevisionSchema,
revisionId: z21.uuid(),
inputFingerprint: nativeDigestSchema
}).strict().optional(),
items: z21.array(nativeItemInputSchema).min(1).max(NATIVE_JOB_LIMITS.items),
resources: aiJobResourcesSchema.refine(
(value) => value.tool_steps === "0" && value.model_steps === "1" && value.attempts === "1" && BigInt(value.elapsed_ms) > 0n && BigInt(value.elapsed_ms) <= BigInt(NATIVE_JOB_LIMITS.deadlineMs),
"Exactly one bounded model attempt and no payable tools"
)
}).strict().superRefine((value, context) => {
for (const key of ["itemId", "artifactId", "mutationId"]) {
if (new Set(value.items.map((item) => item[key])).size !== value.items.length)
context.addIssue({
code: "custom",
path: ["items"],
message: `Duplicate ${key}`
});
}
if (utf8.encode(JSON.stringify(value)).byteLength > NATIVE_JOB_LIMITS.resultBytes)
context.addIssue({
code: "custom",
message: "Command exceeds storage bound"
});
});
var nativeCreateLearningJobSchema = nativeJobScopeSchema.extend({
commandId: nativeCreateJobSchema.shape.commandId,
stepId: nativeCreateJobSchema.shape.stepId,
messageId: nativeCreateJobSchema.shape.messageId,
expectedRevision: artifactRevisionSchema,
operation: z21.literal("analyze_learning"),
message: z21.literal("Analyze the opted-in account's canonical evidence."),
context: z21.literal(""),
resources: nativeCreateJobSchema.shape.resources,
learningScopeId: z21.uuid(),
inputFingerprint: nativeDigestSchema,
resultId: z21.uuid(),
kind: z21.enum(["hypothesis", "evaluation"]),
experimentId: z21.uuid().nullable(),
analysis: learningAnalysisInputSchema
}).strict().superRefine((v, ctx) => {
if (v.orgId !== v.analysis.orgId || v.learningScopeId !== v.analysis.scopeId)
ctx.addIssue({
code: "custom",
message: "Learning command scope mismatch"
});
if (v.kind === "evaluation" !== (v.experimentId !== null))
ctx.addIssue({
code: "custom",
message: "Evaluation requires its experiment"
});
if (utf8.encode(JSON.stringify(v)).byteLength > NATIVE_JOB_LIMITS.resultBytes)
ctx.addIssue({
code: "custom",
message: "Learning command exceeds storage bound"
});
});
var nativeAnyCreateJobSchema = z21.union([
nativeCreateJobSchema,
nativeCreateLearningJobSchema
]);
var draftText = preSubmitDraftFieldsSchema.pick({
caption: true,
youtubeTitle: true,
youtubeDescription: true
});
var nativeModelItemSchema = draftText.extend({ itemId: z21.uuid() }).strict();
var nativeModelResultSchema = z21.object({
items: z21.array(nativeModelItemSchema).max(NATIVE_JOB_LIMITS.items)
}).strict().refine(
(value) => utf8.encode(JSON.stringify(value)).byteLength <= NATIVE_JOB_LIMITS.resultBytes,
"Model result exceeds storage bound"
);
var nativeJobStateSchema = z21.enum([
"queued",
"running",
"awaiting_user",
"completed",
"failed",
"cancelled"
]);
var nativeJobStageSchema = z21.enum([
"queued",
"preparing_budget",
"reserved",
"handoff_started",
"response_recorded",
"settling",
"materializing_results",
"terminal"
]);
var nativeCleanupStateSchema = z21.enum([
"none",
"pending",
"fenced",
"awaiting_settlement",
"reconciliation_required"
]);
var nativeItemOutcomeSchema = z21.enum([
"created",
"reused",
"invalid",
"reference_unavailable",
"conflict"
]);
// ../core/src/native-dashboard.ts
import { z as z22 } from "zod";
var nativeDashboardCapabilitiesSchema = z22.object({
execution: z22.enum(["synthetic", "openai"]).nullable()
}).strict();
var command = nativeCreateJobSchema.shape;
var nativeDashboardStartSchema = z22.object({
conversationId: command.conversationId,
jobId: command.jobId,
commandId: command.commandId,
stepId: command.stepId,
messageId: command.messageId,
expectedRevision: command.expectedRevision,
message: command.message,
context: command.context,
items: command.items
}).strict();
var nativeDashboardRefSchema = z22.object({ conversationId: z22.uuid(), jobId: z22.uuid() }).strict();
var nativeDashboardResumeSchema = nativeDashboardRefSchema.extend({ stepId: z22.uuid(), expectedRevision: z22.string().regex(/^[1-9][0-9]*$/) }).strict();
var nativeDashboardViewSchema = z22.object({
id: z22.uuid(),
revision: z22.string().regex(/^\d+$/),
state: nativeJobStateSchema,
stepId: z22.uuid(),
attemptNumber: z22.number().int().min(1).max(3),
canResume: z22.boolean(),
stage: nativeJobStageSchema,
reason: z22.string().nullable(),
active: z22.boolean(),
cancelRequested: z22.boolean(),
cleanupState: nativeCleanupStateSchema,
conversationState: z22.enum(["active", "deleted", "expired"]),
messages: z22.array(z22.object({
id: z22.uuid(),
sequence: z22.string(),
source: z22.enum(["human", "native_agent"]),
content: z22.string(),
initiating_human_id: z22.uuid(),
executing_agent: z22.string().nullable()
})).max(100),
items: z22.array(z22.object({ itemId: z22.uuid(), outcome: nativeItemOutcomeSchema })).max(5),
artifacts: z22.array(z22.object({
itemId: z22.uuid(),
artifactId: z22.uuid(),
creationRevision: z22.string(),
currentRevision: z22.string(),
currentState: z22.enum(["editable", "deleted", "expired", "submitted"])
})).max(5),
events: z22.array(z22.object({
sequence: z22.string(),
kind: z22.string(),
reason: z22.string().nullable(),
job_revision: z22.string(),
step_id: z22.uuid().nullable(),
artifact_id: z22.uuid().nullable()
})).max(100)
}).strict();
// ../core/src/workspace-learning-api.ts
import { z as z23 } from "zod";
var revision3 = z23.string().regex(/^[1-9][0-9]*$/);
var digest2 = z23.string().regex(/^[a-f0-9]{64}$/);
var group = z23.object({
n: z23.number().int().min(0).max(20),
median: z23.number().nonnegative().nullable(),
minimum: z23.number().nonnegative().nullable(),
maximum: z23.number().nonnegative().nullable()
}).strict();
var learningExclusionSchema = z23.enum([
"wrong_scope",
"wrong_format",
"source_unavailable",
"outside_cohort_window",
"content_revision_unproven",
"missing_observation",
"wrong_observation_age",
"missing_measurement",
"source_conflict",
"customer_promoted",
"customer_excluded"
]);
var learningEvidenceSchema = z23.object({
state: z23.enum(["available", "insufficient_data", "unavailable"]),
definition: learningDefinitionSchema,
unit: z23.literal("reported_view_count"),
observedAt: z23.iso.datetime(),
short: group,
long: group,
candidate: learningModelResultSchema,
caveats: z23.array(z23.string().max(400)).max(10),
sources: z23.array(
z23.object({
postId: z23.uuid(),
publishId: z23.uuid(),
postVersion: revision3,
requestHash: digest2,
publishedAt: z23.iso.datetime().nullable(),
captionCodePoints: z23.number().int().nonnegative().nullable(),
value: z23.number().nonnegative().nullable(),
exclusion: learningExclusionSchema.nullable(),
snapshot: z23.object({
id: revision3,
hash: digest2,
capturedAt: z23.iso.datetime(),
ageHours: z23.number()
}).strict().nullable(),
detectionSnapshots: z23.array(
z23.object({
id: revision3,
hash: digest2,
capturedAt: z23.iso.datetime()
}).strict()
).max(LEARNING_LIMITS.tailSnapshots)
}).strict()
).max(LEARNING_LIMITS.posts)
}).strict();
var learningReadRequestSchema = z23.object({ scopeId: z23.uuid() }).strict();
var learningConfigureRequestSchema = learningReadRequestSchema.extend({ accountId: z23.uuid(), definition: learningDefinitionSchema }).strict();
var learningApplyRequestSchema = learningReadRequestSchema.extend({
commandId: z23.uuid(),
expectedRevision: revision3,
recommendationId: z23.uuid(),
fields: preSubmitDraftFieldsSchema,
mediaHandle: mediaHandleSchema.nullable(),
instruction: z23.string().trim().min(1).max(2048)
}).strict();
var learningExperimentRequestSchema = learningReadRequestSchema.extend({ experimentId: z23.uuid() }).strict();
var learningDecisionRequestSchema = learningReadRequestSchema.extend({
commandId: z23.uuid(),
expectedRevision: revision3,
operation: z23.enum(["dismiss", "correct", "adjust"]),
marker: z23.enum([
"reject_short_caption",
"customer_promoted",
"customer_excluded",
"source_conflict"
]),
postId: z23.uuid().nullable(),
active: z23.boolean().default(true),
instruction: z23.string().max(2048)
}).strict();
var learningForgetRequestSchema = learningReadRequestSchema.extend({ expectedRevision: revision3 }).strict();
var learningResumeRequestSchema = learningReadRequestSchema.extend({ commandId: z23.uuid(), expectedRevision: revision3 }).strict();
var learningRevisionRefusalSchema = z23.object({
code: z23.literal("learning_revision_rejected"),
operation: z23.enum(["apply", "decide", "resume", "forget"]),
scopeId: z23.uuid(),
commandId: z23.uuid().nullable(),
expectedRevision: revision3,
currentRevision: revision3
}).strict().refine((v) => BigInt(v.currentRevision) > BigInt(v.expectedRevision)).refine(
(v) => v.operation === "forget" ? v.commandId === null : v.commandId !== null
);
var learningEvaluationSchema = z23.object({
state: z23.enum(["evaluated", "inconclusive"]),
reason: z23.string().regex(/^[a-z_]{1,80}$/),
value: z23.number().nonnegative().nullable()
}).strict();
var learningStoredResultSchema = z23.object({
candidate: learningModelResultSchema,
evaluation: learningEvaluationSchema.optional()
}).strict();
var learningReadResponseSchema = z23.object({
scopeId: z23.uuid(),
accountId: z23.uuid().nullable(),
revision: revision3.nullable(),
state: z23.enum([
"not_configured",
"waiting",
"ready",
"invalidated",
"unavailable",
"forgotten",
"analysis_deferred"
]),
reason: z23.string().regex(/^[a-z_]{1,80}$/).nullable(),
checkedAt: z23.iso.datetime(),
nextScanAt: z23.iso.datetime().nullable(),
latest: z23.object({
id: z23.uuid(),
version: revision3,
kind: z23.enum(["hypothesis", "evaluation"]),
createdAt: z23.iso.datetime(),
expiresAt: z23.iso.datetime(),
evidence: learningEvidenceSchema,
result: learningStoredResultSchema
}).strict().nullable(),
// No private conversation, artifact, media handle, draft text or creator ID.
experiment: z23.object({
state: z23.enum([
"creating_draft",
"draft",
"awaiting_publication",
"awaiting_observations",
"evaluated",
"inconclusive"
]),
reason: z23.string().nullable(),
postId: z23.uuid().nullable(),
publishId: z23.uuid().nullable()
}).strict().nullable(),
retained: z23.object({
rejectedShortCaption: z23.boolean(),
priorTrialNotSupportive: z23.boolean(),
exclusions: z23.array(
z23.object({
postId: z23.uuid(),
reason: z23.enum([
"customer_promoted",
"customer_excluded",
"source_conflict"
])
}).strict()
).max(20)
}).strict().nullable()
}).strict();
var learningReadQuerySchema = learningReadRequestSchema.partial();
var learningReadEnvelopeSchema = z23.object({ learning: learningReadResponseSchema.nullable() }).strict();
var learningAccountSchema = z23.object({
id: z23.uuid(),
providerAccountId: z23.string().min(1),
handle: z23.string().nullable()
}).strict();
var learningAccountsResponseSchema = z23.object({ accounts: z23.array(learningAccountSchema).max(100) }).strict();
var learningPrivateExperimentSchema = z23.object({
state: z23.string(),
reason: z23.string().nullable(),
draft: z23.object({
conversationId: z23.uuid(),
jobId: z23.uuid(),
artifactId: z23.uuid()
}).strict().nullable()
}).strict();
// src/index.ts
import { z as z24 } from "zod";
var POSTDOM_MCP_VERSION = "0.4.0";
var POSTDOM_MCP_PUBLISHED_VERSIONS = ["0.1.0", "0.2.0", "0.3.0"];
var performanceMetricSchema2 = z24.enum([
"views",
"likes",
"comments",
"shares",
"saves",
"watch_time_s",
"avg_watch_pct",
"completion_pct",
"follower_delta"
]);
var POSTDOM_MCP_TOOL_NAMES = [
"get_workspace_status",
"get_brief",
"get_digest",
"get_learning",
"list_accounts",
"connect_account",
"upload_media",
"get_media",
"publish_video",
"submit_plan",
"get_plan",
"get_publish",
"get_performance",
"get_best_posts"
];
var PostdomApiError = class extends Error {
constructor(message, status, code, destinations) {
super(message);
this.status = status;
this.code = code;
this.destinations = destinations;
this.name = "PostdomApiError";
}
status;
code;
destinations;
};
var PostdomClient = class {
constructor(options) {
this.options = options;
this.baseUrl = options.baseUrl ?? "https://api.postdom.com/v1";
this.fetch = options.fetch ?? globalThis.fetch;
}
options;
baseUrl;
fetch;
async request(path, init = {}) {
const headers = new Headers(init.headers);
headers.set("Authorization", `Bearer ${this.options.apiKey}`);
headers.set("Accept", "application/json");
if (init.body) headers.set("Content-Type", "application/json");
const response = await this.fetch(`${this.baseUrl}${path}`, { ...init, headers });
const payload = await response.json();
if (!response.ok) {
const problem = typeof payload === "object" && payload !== null ? payload : {};
const code = typeof problem.code === "string" ? ` ${problem.code}` : "";
const detail = typeof problem.detail === "string" ? `: ${problem.detail}` : "";
const destinations = Array.isArray(problem.destinations) ? problem.destinations.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.platform === "string" && typeof entry.fact === "string").map(({ platform, fact }) => ({ platform, fact })) : [];
const blocked = destinations.length > 0 ? ` Destinations that cannot receive this video: ${JSON.stringify(destinations)}.` : "";
throw new PostdomApiError(
`Postdom API request failed with HTTP ${response.status}${code}${detail}${blocked}`,
response.status,
typeof problem.code === "string" ? problem.code : null,
destinations
);
}
return payload;
}
async listAccounts() {
const response = await this.request("/accounts");
return response.accounts;
}
getWorkspaceStatus() {
return this.request("/workspace/status");
}
connectAccount(platform) {
return this.request("/accounts/connect", {
method: "POST",
body: JSON.stringify({
platform,
redirect_url: "https://app.postdom.com/accounts"
})
});
}
async createMediaUpload(input) {
const request = mediaOperations.createUpload.request.parse(input);
const response = await this.request(mediaOperations.createUpload.path, {
method: mediaOperations.createUpload.method,
body: JSON.stringify(request)
});
return mediaUploadResponseSchema.parse(response);
}
async getMedia(mediaHandle) {
const { media_handle } = mediaOperations.getUpload.params.parse({
media_handle: mediaHandle
});
const response = await this.request(
mediaOperations.getUpload.path.replace(
"{media_handle}",
encodeURIComponent(media_handle)
)
);
return mediaStatusResponseSchema.parse(response);
}
async publishVideo(input) {
const mediaSource = publishMediaSourceSchema.parse({
video_url: input.videoUrl,
media_handle: input.mediaHandle
});
const accounts = await this.listAccounts();
const byId = new Map(accounts.map((account) => [account.providerAccountId, account]));
const targets = input.accountIds.map((accountId) => {
const account = byId.get(accountId);
if (!account) throw new Error(`Postdom account ${accountId} is not connected`);
const platform = corePlatformSchema.safeParse(account.platform);
if (!platform.success) {
throw new Error(`Postdom account ${accountId} is connected to an unrecognised destination`);
}
return defaultTarget(platform.data, accountId, input.caption);
});
return this.request("/posts", {
method: "POST",
headers: {
"Idempotency-Key": input.idempotencyKey ?? randomUUID(),
"X-Postdom-Source": "mcp"
},
body: JSON.stringify({
caption: input.caption,
...mediaSource.video_url ? { media_url: mediaSource.video_url } : {},
...mediaSource.media_handle ? { media_handle: mediaSource.media_handle } : {},
publish_at: input.publishAt,
plan_id: input.planId,
targets,
agent_context: {
identity: this.options.agentIdentity ?? input.agentIdentity ?? "AI agent via MCP",
intent: input.intent
}
})
});
}
async submitPlan(input) {
const accounts = await this.listAccounts();
const byId = new Map(accounts.map((account) => [account.providerAccountId, account]));
const targets = input.accountIds.map((accountId) => {
const account = byId.get(accountId);
if (!account) throw new Error(`Postdom account ${accountId} is not connected`);
return { account_id: accountId, platform: account.platform };
});
return this.request("/plans", {
method: "POST",
headers: {
"Idempotency-Key": input.idempotencyKey ?? randomUUID(),
"X-Postdom-Source": "mcp"
},
body: JSON.stringify({
title: input.title,
objective: input.objective,
starts_at: input.startsAt,
ends_at: input.endsAt,
max_posts: input.maxPosts,
brief_version: input.briefVersion,
targets,
agent_context: {
identity: this.options.agentIdentity ?? input.agentIdentity ?? "AI agent via MCP",
intent: input.intent
}
})
});
}
getPlan(planId) {
return this.request(`/plans/${encodeURIComponent(planId)}`);
}
getBrief() {
return this.request("/brief");
}
getDigest() {
return this.request("/digest");
}
async getLearning(raw = {}) {
const input = learningReadQuerySchema.parse(raw);
const query = input.scopeId ? `?scopeId=${encodeURIComponent(input.scopeId)}` : "";
return learningReadEnvelopeSchema.parse(await this.request(`/learning${query}`));
}
getPublish(postId) {
return this.request(`/posts/${encodeURIComponent(postId)}`);
}
getPostPerformance(postId) {
return this.request(`/posts/${encodeURIComponent(postId)}/performance`);
}
getAccountPerformance(accountId, window) {
return this.request(
`/accounts/${encodeURIComponent(accountId)}/performance?window=${window}`
);
}
getBestPosts(accountId, metric, window) {
return this.request(
`/accounts/${encodeURIComponent(accountId)}/best-posts?metric=${encodeURIComponent(metric)}&window=${window}`
);
}
};
function defaultTarget(platform, accountId, caption) {
if (platform === "tiktok") {
return {
account_id: accountId,
platform,
settings: {
privacy_level: "SELF_ONLY",
allow_comment: false,
allow_duet: false,
allow_stitch: false,
content_preview_confirmed: true,
express_consent_given: true,
video_made_with_ai: true
}
};
}
if (platform === "instagram") {
return {
account_id: accountId,
platform,
settings: { contentType: "reel", isAiGenerated: true }
};
}
if (platform === "youtube") {
return {
account_id: accountId,
platform,
settings: {
title: caption.split("\n")[0]?.slice(0, 100) || "Postdom video",
visibility: "private",
madeForKids: false,
containsSyntheticMedia: true
}
};
}
if (platform === "facebook") {
return {
account_id: accountId,
platform,
settings: { contentType: "reel" }
};
}
if (platform === "twitter") {
return {
account_id: accountId,
platform,
settings: {}
};
}
if (platform === "linkedin") {
return {
account_id: accountId,
platform,
settings: {}
};
}
if (platform === "bluesky") {
return {
account_id: accountId,
platform,
settings: {}
};
}
if (platform === "snapchat") {
return {
account_id: accountId,
platform,
settings: {}
};
}
if (platform === "threads") {
return {
account_id: accountId,
platform,
settings: {}
};
}
const unsupported = platform;
throw new Error(`publish_video does not support ${String(unsupported)}`);
}
function toolResult(value) {
return {
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
structuredContent: value
};
}
function createPostdomMcpServer(client) {
const server = new McpServer({ name: "postdom", version: POSTDOM_MCP_VERSION });
server.registerTool("get_workspace_status", {
title: "Get workspace status",
description: "Start here. Read connected-account health, autonomy and policy state, brief version, activity counts, and the connection gate.",
inputSchema: {},
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async () => toolResult(await client.getWorkspaceStatus()));
server.registerTool("get_brief", {
title: "Get workspace brief",
description: "Read the current workspace-owned brand guidance before planning or writing content.",
inputSchema: {},
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async () => toolResult(await client.getBrief()));
server.registerTool("get_digest", {
title: "Get latest learning digest",
description: "Read the latest completed weekly workspace digest before planning. It describes observed outcomes, populations, cadence endings, and coverage gaps; it does not recommend actions.",
inputSchema: {},
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async () => toolResult(await client.getDigest()));
server.registerTool("get_learning", {
title: "Read shared account learning",
description: "Read saved, source-linked account observations and exploratory advice. Missing or invalidated advice is not evidence of growth. This cannot apply, correct, fund, approve or publish anything; private drafts are never returned.",
inputSchema: learningReadQuerySchema.shape,
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async (input) => toolResult(await client.getLearning(input)));
server.registerTool("list_accounts", {
title: "List connected accounts",
description: "List the destination accounts available inside this workspace.",
inputSchema: {},
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async () => toolResult({ accounts: await client.listAccounts() }));
server.registerTool("connect_account", {
title: "Connect social account",
description: "Create a platform OAuth URL for a destination that authorises by redirect. Hand the returned URL to the human; never ask for or handle their social password. Destinations that authorise with an app password instead are connected by a human in the Postdom dashboard and are deliberately absent from the platform enum - there is no credential tool here, and there should not be.",
// Offered, not accepted. A tool schema is the menu an agent reads, and an agent is the
// customer here - so it lists what a customer can actually connect, the same narrowing the
// dashboard, Make, n8n and Zapier pickers already have. corePlatformSchema stays the wire
// contract: POST /v1/accounts/connect still accepts every core platform, because refusing one
// the API would take is a client bug. Offering one with no Connect path is a guaranteed
// failure the agent cannot diagnose - it would hand a human a URL for a destination that
// cannot exist.
// REDIRECT_DESTINATIONS, not CONNECT_DESTINATIONS, and the comment above is why. This read
// the offered list, which was the same set until Bluesky was claimed - and then this tool
// offered an agent a destination it cannot connect: connectAccount returns an OAuth URL, and
// Bluesky has none. The agent would hand a human a URL for a screen that does not exist, or
// surface a provider error it has no way to interpret. That is precisely the "guaranteed
// failure the agent cannot diagnose" this comment already warned about, arriving through the
// one list that had not learned to ask how a destination connects.
inputSchema: { platform: z24.enum(REDIRECT_DESTINATIONS) },
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
}, async ({ platform }) => toolResult(await client.connectAccount(platform)));
server.registerTool("upload_media", {
title: "Upload media",
description: "Create a short-lived, workspace-scoped PUT URL for finished video bytes. Supply measured width_pixels, height_pixels and duration_seconds as positive integers; never guess or default them. Upload the exact declared bytes directly to the returned URL, then call get_media until the handle is stored.",
inputSchema: mediaOperations.createUpload.request.shape,
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
}, async (input) => toolResult(await client.createMediaUpload(input)));
server.registerTool("get_media", {
title: "Get media",
description: "Verify one workspace media handle after its direct PUT. Continue only when status is stored; preserve pending or failed exactly.",
inputSchema: { media_handle: mediaHandleSchema },
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async ({ media_handle }) => toolResult(await client.getMedia(media_handle)));
server.registerTool("publish_video", {
title: "Publish video",
description: "Create a short-form video publish with each provider's supported safety defaults, optionally scheduled in UTC. Posts flow automatically inside an account's policy or await review on review-mode accounts.",
inputSchema: {
account_ids: z24.array(z24.string()).min(1),
video_url: z24.url().optional(),
media_handle: mediaHandleSchema.optional(),
caption: z24.string().max(4e3),
intent: z24.string().trim().min(1).max(500),
agent_identity: z24.string().trim().min(1).max(120).optional(),
publish_at: z24.string().datetime().optional(),
plan_id: z24.string().uuid().optional(),
idempotency_key: z24.string().min(1).optional()
},
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
}, async ({ account_ids, video_url, media_handle, caption, intent, agent_identity, publish_at, plan_id, idempotency_key }) => {
publishMediaSourceSchema.parse({ video_url, media_handle });
return toolResult(await client.publishVideo({
accountIds: account_ids,
videoUrl: video_url,
mediaHandle: media_handle,
caption,
intent,
agentIdentity: agent_identity,
publishAt: publish_at,
planId: plan_id,
idempotencyKey: idempotency_key
}));
});
server.registerTool("submit_plan", {
title: "Submit publication plan",
description: "Submit a time-bounded L2 publication plan for one human approval.",
inputSchema: {
account_ids: z24.array(z24.string()).min(1),
title: z24.string().trim().min(1).max(120),
objective: z24.string().trim().min(1).max(1e3),
starts_at: z24.string().datetime(),
ends_at: z24.string().datetime(),
max_posts: z24.number().int().min(1).max(20),
brief_version: z24.number().int().positive().optional(),
intent: z24.string().trim().min(1).max(500),
agent_identity: z24.string().trim().min(1).max(120).optional(),
idempotency_key: z24.string().min(1).optional()
},
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
}, async ({ account_ids, title, objective, starts_at, ends_at, max_posts, brief_version, intent, agent_identity, idempotency_key }) => toolResult(
await client.submitPlan({
accountIds: account_ids,
title,
objective,
startsAt: starts_at,
endsAt: ends_at,
maxPosts: max_posts,
briefVersion: brief_version,
intent,
agentIdentity: agent_identity,
idempotencyKey: idempotency_key
})
));
server.registerTool("get_plan", {
title: "Get publication plan",
description: "Read plan status and structured feedback from the human reviewer.",
inputSchema: { plan_id: z24.string().uuid() },
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async ({ plan_id }) => toolResult(await client.getPlan(plan_id)));
server.registerTool("get_publish", {
title: "Get publish",
description: "Read publish state and any structured approval feedback returned by the human reviewer.",
inputSchema: { post_id: z24.string().min(1) },
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async ({ post_id }) => toolResult(await client.getPublish(post_id)));
server.registerTool("get_performance", {
title: "Get performance",
description: "Read normalized performance snapshots for one post or connected account.",
inputSchema: {
post_id: z24.string().optional(),
account_id: z24.string().optional(),
window: z24.enum(["7d", "30d"]).default("7d")
},
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async ({ post_id, account_id, window }) => {
if (Boolean(post_id) === Boolean(account_id)) {
throw new Error("Provide exactly one of post_id or account_id");
}
const result = post_id ? await client.getPostPerformance(post_id) : await client.getAccountPerformance(account_id, window);
return toolResult(result);
});
server.registerTool("get_best_posts", {
title: "Get best posts",
description: "Rank one connected account's posts by an evidence-backed metric over the requested window. Null observations are excluded with reasons, never ranked as zero.",
inputSchema: {
account_id: z24.string().min(1),
metric: performanceMetricSchema2,
window: z24.enum(["7d", "30d"]).default("7d")
},
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
}, async ({ account_id, metric, window }) => toolResult(
await client.getBestPosts(account_id, metric, window)
));
return server;
}
async function handlePostdomMcpHttpRequest(request, options) {
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: void 0,
enableJsonResponse: true
});
const server = createPostdomMcpServer(new PostdomClient(options));
await server.connect(transport);
return transport.handleRequest(request);
}
export {
POSTDOM_MCP_VERSION,
POSTDOM_MCP_PUBLISHED_VERSIONS,
POSTDOM_MCP_TOOL_NAMES,
PostdomApiError,
PostdomClient,
createPostdomMcpServer,
handlePostdomMcpHttpRequest
};