@volter/twin-linkedin
v0.1.39
Published
Local LinkedIn twin (organization posts with image, video or article; the Images and Videos upload protocols; admin roles) with a linkedin.com company-page mirror, built on @volter/world-core.
Downloads
5,267
Readme
@volter/twin-linkedin — an organization's posts on LinkedIn
An organization's company page on LinkedIn: its posts (commentary with one video, one image or an article) through the versioned Posts API, the Images and Videos upload protocols behind them, the page's admin roles and follower count, and a linkedin.com-style company page to preview each post as linkedin.com shows it before a deploy sends it — offline, against local state.
bun packages/twin/linkedin/src/cli.ts serve --root /tmp/world # the API
bun packages/twin/linkedin/src/cli.ts mirror --root /tmp/world # the company page, on the same stateEvery /rest call needs Linkedin-Version (202510 … 202609) and a bearer the world seeded; the
official client (linkedin-api-client) reaches it through the hosts on the descriptor.
Coverage
| Area | What the twin serves |
|---|---|
| POST /rest/posts | Create an organization post: author, commentary, visibility, distribution, lifecycleState: PUBLISHED; content.media naming an AVAILABLE image or video the page owns, or content.article (source, title, description, an optional thumbnail image). 201 with the URN in x-restli-id and no body. A #hashtag is stored as LinkedIn answers it, the little-format template {hashtag\|\\#\|tag}. Missing fields are MISSING_FIELD, values outside the documented enums INVALID_VALUE_FOR_FIELD, media not yet uploaded or failed processing the Videos API's own codes (MEDIA_ASSET_WAITING_UPLOAD, MEDIA_ASSET_PROCESSING_FAILED), another page's media 403 ACCESS_DENIED. |
| GET /rest/posts/{urn} · GET /rest/posts?ids=List(…) · GET /rest/posts?q=author | Read a post (by the URN it was created under, even after a deploy re-keyed it to LinkedIn's) (the Post schema, times in ms), batch get (results / statuses / errors), and the author finder: newest first by CREATED or LAST_MODIFIED, start/count (10, at most 100) with the vendor's next link. |
| DELETE /rest/posts/{urn} | Idempotent 204; the post leaves every read. |
| POST /rest/images?action=initializeUpload + the upload PUT | An image upload: a /dms-uploads/… URL signed with the asset's own secret, whose PUT needs the URL's signature and the member's token (ADMINISTRATOR or DIRECT_SPONSORED_CONTENT_POSTER on the page, as the Images and Videos pages require of an upload); JPG, GIF or PNG, sized from its own bytes, then AVAILABLE. |
| POST /rest/videos?action=initializeUpload · part PUTs · action=finalizeUpload · GET /rest/videos/{urn} | A video upload as LinkedIn documents it: 4 MB byte-range upload instructions on signed URLs (no bearer), an ETag per part, finalize with the ETags in order, an optional thumbnail upload, then PROCESSING until two seconds of world time after finalize, AVAILABLE after — a fold of the stored instant and the request's own, so reading (a preview included) never moves it. A frozen World clock keeps a video PROCESSING: advance the clock (volter world clock advance 2s) for it to become AVAILABLE. A World finishes only the uploads it started: a part, thumbnail or finalize for an asset its origin or parent initialized is refused (409, "finish it there") and writes nothing; reads of it are unchanged. A file outside the feed's bounds (3 s–30 min, at least 75,000 bytes — the Videos API's "Between 75kb and 500MB", decimal as written — MP4) fails processing with its reason; a declared size over the feed's 500 MB is refused by name at initialize (the ads videos up to 5 GB are not modelled). |
| asset bytes | Served under the twin's base at LinkedIn's own path shapes — /dms/image/<asset>/… and /playlist/<asset>/… — through the kernel's ranged reads with HTTP Range (206/416): an open-ended range is capped at 8 MiB, and a whole file or a long range is streamed in 8 MiB reads. A video with no uploaded thumbnail answers a stand-in poster: the twin decodes no frames. |
| GET /rest/organizationAcls · GET /rest/organizations/{id} · ?q=vanityName · GET /rest/organizationsLookup?ids=List(…) · GET /rest/networkSizes/{org} | The member's roles (q=roleAssignee) and a page's (q=organization), the administered page, the public lookups by vanity name and by id, and the follower count. |
| GET /v2/userinfo | The token's member (OpenID Connect), used by the mirror's sign-in. |
| errors | {status, code, message}; the two version errors quoted from LinkedIn's docs (VERSION_MISSING, NONEXISTENT_VERSION); EMPTY_ACCESS_TOKEN / INVALID_ACCESS_TOKEN; ACCESS_DENIED for a missing scope or page role; 404 NOT_FOUND for a route or method the twin does not serve; 405 under --read-only. A feature of a served route the twin does not model (a poll, a reshare, targeting, a person author, …) is refused by name as 422 [twin gap] …, never answered as a success. |
| connector | Perform: a post crosses as POST /rest/posts; a video post first uploads its bytes through the Videos API (initialize, presigned part PUTs, finalize, a bounded wait for AVAILABLE); a delete crosses as DELETE. A post with an image does not cross: LinkedIn's image PUT needs the API token on the upload host, which a presigned upload does not carry (linkedin.connector.perform_image_upload). A perform that finalized a video and then failed keeps the vendor's video URN, so its retry posts that video without uploading it again. Refresh: the pages the member administers, their follower counts and their posts. Both charge the pack's rate budget, one ledger per sealed credential; the perform's wait on a processing video backs off (2 s doubling to 15 s) so it never spends the budget the post needs. A post the kernel tombstones is gone from every read. |
| mirror | linkedin.com's company page over the twin's own API: sign in with a page member's email (an administrator, content administrator or sponsored-content poster) and the twin-issued token; relative times read the World's clock (each answer's HTTP Date); the header (logo, name, description, website, follower line), the Posts feed with video players, images and article cards, and a single post's page. |
| /_twin/* | Twin-only control plane: seed a page (name, vanity name, website, description, follower count, a logo image it uploaded), a member, a role on a page, and an access token with its scopes. |
src/linkedin-capabilities.ts is the denominator: 122 capabilities, 76 done. What is not
modelled is enumerated there as a todo — comments, reactions and social metadata, documents, polls,
multi-image, reshares, targeting and dark posts, post updates, member posting, captions, the media
library, the statistics APIs, and the composer on the mirror — not omitted.
The rate budget (src/linkedin-budget.ts): LinkedIn publishes no figures ("Standard rate limits are
not published in documentation"), so the declaration stays at or under the kernel fallback.
