Skip to content

Releases: XXX1694/steerpost

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 11:00
51b6870

Added

  • Sign in with Google or GitHub, API side (D-023; the web screens come next). GET /auth/providers, GET /auth/oauth/{provider}/start?next=, GET /auth/oauth/{provider}/callback, GET /auth/oauth/pending and POST /auth/oauth/complete {display_name, accept_terms}. The callback binds the browser to the flow with a state cookie (HttpOnly, SameSite=Lax, path /api/v1/auth/oauth, 10 min) and requires query state = cookie = an unused row of that provider; the PKCE verifier is stored encrypted with the ENCRYPTION_KEY cipher; the redirect URI comes from API_PUBLIC_URL, never from Host; next goes through one shared allow-list (domain/redirect, also used by the account-connect flow). Every callback outcome is a 302 to the web app (/login?error=<code> on failure). A new user gets no account until they accept the Terms at /signup/complete (D-016); the ticket is redeemed in the same transaction as the user, so a refused form does not burn it. Disabled accounts are refused; an account waiting for deletion can sign in with its provider to cancel the deletion, but never gains a new link by email match, and an automatic link sends the owner an identity_linked notice mail. Start, callback and complete sit behind the auth rate limiter, the worker deletes expired flows hourly, and the audit log records user.registered / user.login with {method} and user.identity_linked. No tokens or addresses reach logs or audit metadata. Setup steps for the GitHub OAuth App (and Google, once a domain exists) are in docs/OWNER-SETUP.md section 2.0.
  • Account deletion for users without a password: a session created within the last 10 minutes is accepted instead of a password (the typed email is still required); an older one gets 403 REAUTH_REQUIRED, so deletion stays available and never fails with a 500. Sessions now record created_at from the injected clock.
  • GET /api/v1/version (also /version): a public, unauthenticated answer to "what runs in production" without SSH, {"version":"0.4.0","commit":"abc1234","built_at":"2026-10-10T12:00:00Z"}, cacheable for 60 seconds. Release images carry the tag version (a main build says sha-<7 hex>), the short commit and the build time, linked in from build arguments that release.yml passes to backend/Dockerfile; a local build falls back to the Go VCS stamp, then dev / unknown. Nothing about the host or the toolchain is exposed. The Uptime workflow prints the deployed version in its log.
  • Pause motion in the app (Settings, Preferences), as the landing has had since D-017: it stops every animation and view transition in this browser, on top of the system's reduced-motion setting, and applies before the first paint.
  • Manage sign-in methods, API side (D-023; the Settings screen comes next). GET /auth/identities, POST /auth/identities/{provider}/link (returns authorize_url; the provider calls the ordinary sign-in callback, which finishes the link and returns to /settings), DELETE /auth/identities/{provider} (refused with 409 when it is the last way to sign in) and POST /auth/password/set for accounts without a password (session younger than 10 minutes, else 403 REAUTH_REQUIRED). /me gains user.has_password and user.login_methods. Linking and unlinking re-authenticate (current_password, or a session under 10 minutes old for users without a password), are audited and are mailed to the owner. A provider that is switched off does not count as the remaining way to sign in. The demo answers the new routes without pretending to link anything. Failed sign-in and link redirects carry provider=<id> (and, for sign-in, the sanitised next).
  • Sign in with Google or GitHub, web side (D-023). /login and /register share one screen: "Continue with Google / GitHub" buttons on top (only the providers GET /auth/providers lists; the block and its divider are hidden when there are none, and a failed list says so and keeps the email form), then the email form. The buttons are links to /api/v1/auth/oauth/{provider}/start?next=, so next survives the provider round trip. New page /signup/complete: the provider's verified email read-only, the name prefilled and editable, an explicit Terms checkbox (D-016) and "Create account"; with no waiting sign-up (a 404) it goes back to /login?error=signup_expired. /login?error=<code> shows plain-language copy for every code the API sends (oauth_cancelled, oauth_state_invalid, oauth_provider_error, email_unverified, account_exists, account_unavailable; identity_in_use goes to /settings); an optional provider=github|google in that URL names the provider. Deleting the account with a session too old for a password-less user (403 REAUTH_REQUIRED) now offers "Sign in again with {provider}" and returns to Settings; the password field there is optional for people who signed up with a provider. The demo and scripts/mock-api.mjs answer every new endpoint (GitHub and Google, a simulated redirect to /signup/complete). 28 new strings in every enabled locale and kk (machine drafts).
  • Manage sign-in methods, web side (D-023). Settings has a "Sign-in methods" section: the password (set or not; "Set a password" for accounts made with Google or GitHub) and each provider the server offers, connected (email and date, Disconnect) or not (Connect). Connect and Disconnect ask for the current password when the account has one, then go to the provider or disconnect; a wrong password shows at the field, the last remaining method is refused with "keep at least one way to sign in", and a session too old for a password-less account (403 REAUTH_REQUIRED) offers "Sign in again with {provider}" for the providers that account is linked to. A provider the admin switched off keeps only Disconnect. /settings?error=<code>&provider=<id> shows plain copy for the codes the link callback sends (anything else reads as a generic sentence and is never echoed) and is cleared from the address; a successful connect carries no flag, so the list is simply read fresh. The delete-account dialog hides the password field for password-less accounts and offers sign-in-again buttons for their linked providers only; the change-password form is hidden for them. The demo has no provider to visit and says so instead of connecting. scripts/mock-api.mjs models password-less accounts (an empty password never signs in). Strings in every enabled locale and kk (machine drafts).

Changed

  • A vX.Y.Z tag build no longer pushes sha-<7 hex>: that tag is pushed only by the main (or manual) build of the commit, so its digest never changes and deploys pinned to it stay reproducible. A tag build pushes X.Y.Z, X.Y and X; a tag that is not vMAJOR.MINOR.PATCH[-pre] now fails the run instead of pushing only the sha tag.
  • Uptime monitoring is sturdier, and the owner guide sets up an external monitor. GitHub ran the */15 schedule only every 3 to 7 hours, so a 70-minute outage on 2026-10-09 went unnoticed. The workflow now fires at :07, :22, :37 and :52 (off-peak minutes) and can also be started by repository_dispatch (uptime-check) or by hand with its own urls. Each URL gets 4 attempts with 5/10/15 s backoff before it counts as down (was 3). Incident issues carry the labels incident and uptime, name the last HTTP code and link the run, and the run summary lists every target. Recovery still comments and closes the issue. OWNER-SETUP section 6 sets up UptimeRobot Free step by step: four monitors (app /api/v1/health, API /api/v1/ready, MCP /health, web /login) with keywords, 5-minute checks, email and Discord alerts, and a public status page. Telegram alerts need UptimeRobot's paid Solo plan or HetrixTools Free; the guide says so. No account is created by the repository.
  • CI checks first-load JS (issue #180). After the production build, npm run budget (frontend/scripts/bundle-budget.mjs) sums the gzip (level 6) of each route's scripts from .next/app-build-manifest.json: root main files, the layout, template, error, loading and not-found entries of every level above the page, and the page itself (the same scripts the prerendered HTML lists, polyfills aside) (Next's table omits them), polyfills excluded. frontend/bundle-budget.json holds the limits for /login, /register, /verify-email, /dashboard, /compose, /posts and /settings (measured size rounded up plus 2 kB). The job also fails when skipToContent (shell.json) is back in the root layout chunk or Radix Dialog is in an auth route's chunks. Raising a budget means editing the JSON in the same PR with a reason; lower it when a route shrinks. No new dependency or action.
  • Each route loads only the message namespaces it uses (step 3 of #145; text and behaviour unchanged). The root layout carries common and errors; (auth) adds auth and legal; (app) adds nav, shell, language and legal; each page adds its own (src/i18n/scopes/*.tsx, mapping in docs/copy/translation-process.md). The demo banner brings shell in the demo build only. First-load JS (gzip of the page and its layout chunks, from .next/app-build-manifest.json; Next's table leaves the layout chunk out) went from 169.7 to 162.0 kB on /login, 162.0 to 154.3 on /verify-email, 175.7 to 167.7 on /dashboard, 185.4 to 177.9 on /compose, 179.6 to 171.2 on /accounts, 182.4 to 176.1 on /settings and 173.0 to 164.3 on /posts; the shared baseline stays 104 kB. LegacyMessagesScope is gone. In development, asking for a namespace no scope above the component loaded logs MISSING_SCOPE: <key> once and renders the key; tests/i18n-route-scopes.test.tsx fails when a page's components use a namespace its scopes do not load, and the site smoke test fails when a demo screen shows a raw key in English or Russian.
  • Message loading runs through scopes (step 2 of #145; text and behaviour unchanged). MessagesScope (`src/i18...
Read more

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 15:35
9bfa542

Added

  • Sign-in provider adapters for Google (generic OpenID Connect) and GitHub (OAuth 2.0), not wired to any endpoint yet (D-023). Authorization Code with PKCE (S256); the ID token is verified with coreos/go-oidc (signature, algorithm allow-list, issuer, audience, expiry, authorized party) and our code checks the nonce; GitHub uses the primary address and only when GitHub marks it verified. New settings GOOGLE_CLIENT_ID/SECRET and GITHUB_CLIENT_ID/SECRET (passed through both compose files): a provider is off while its pair is empty, and half a pair stops the start. New dependencies: coreos/go-oidc/v3 and its go-jose/v4 (Apache-2.0) and golang.org/x/oauth2 (BSD-3-Clause).
  • Groundwork for social sign-in (D-023; no endpoint yet): migration 00008 adds user_identities and auth_oauth_flows and makes users.password_hash nullable, with Postgres repositories and the pure linking function domain/identity.Decide (never auto-links an unverified local email). A user without a password cannot sign in with one, the login answer and its timing match a wrong password, and changing a password is refused with a pointer to setting one.
  • Russian and Kazakh catalogs for the dashboard and the demo (frontend/messages/ru.json, kk.json, 687 keys each, ICU plurals per CLDR). Machine drafts for native review: neither locale is enabled yet (separate change), docs/copy/review/{ru,kk}.md list the terms and strings to check. The post-list row wraps its retry button and fits longer Edit labels at 320 px.
  • App translations for es, pt-BR, de, fr and id (D-021): complete catalogs in frontend/messages/<locale>.json (676 keys each, CLDR plural categories, glossary terms), machine drafts with the choices and open questions in docs/copy/review/<locale>.md (section "App"). The locales are not enabled yet, so the language switcher is unchanged.
  • Japanese (ja) and Simplified Chinese (zh-CN) catalogs for the dashboard and the demo: all 676 keys in frontend/messages/ja.json and zh-CN.json, machine drafts that follow docs/copy/glossary.csv and the landing translations (CJK punctuation, other-only plurals, no "Claude" claim in zh-CN copy). Neither locale is enabled yet (frontend/src/i18n/locales.ts is unchanged), so users still see English until a separate change enables them. docs/copy/review/ja.md and zh-CN.md have an "App" section with the terminology and the strings that need a native check. The audit-log filter buttons wrap on narrow screens.
  • Landing page in 9 languages (D-021): ru, es, pt-BR, de, fr, id, ja, zh-CN and ar, besides English, at /{locale}/ (English stays at /, /en/ forwards to it). site/build.mjs renders the page from site/i18n/landing.<locale>.json (ICU plurals and rich tags through intl-messageformat), with <html lang dir>, localized title, description and Open Graph tags, hreflang for every locale plus x-default, and the locales in the sitemap. Docs and the demo stay English and are linked with "(EN)". Every translation is a machine draft labelled "Beta translation" in the footer; docs/copy/review/<locale>.md lists what needs a native look. kk is built hidden (noindex, not linked) until a native reviewer signs it.
  • A compact language menu in the nav and a plain link list in the footer, both with endonyms, no flags, one line at 320 px. A first-visit suggestion on the English page follows navigator.languages (same matching as the app, uk and zh-Hant get English); it is a small dismissible card, never a redirect, and the choice is remembered in localStorage (steerpost_locale, shared with the demo).
  • Right-to-left layout for ar through logical CSS properties; CJK and Arabic use system fonts with phrase-aware line breaking and no negative tracking; Cyrillic uses Inter's own Cyrillic subsets (fetched only by pages that need them). "Pause motion" and reduced motion behave the same in every language.
  • npm run check verifies a page, a complete catalog and complete hreflang for every locale; npm run smoke loads each locale at 320, 390 and 1440 px and asserts no horizontal overflow, a one-line nav, the motion switch and the suggestion.
  • Internationalization infrastructure (D-021; no translated copy yet). A client-side locale provider that works the same in the server build and the static demo (no middleware, no locale routes): the locale is resolved as user setting (later users.locale) → localStorage (steerpost_locale) → navigator.languages → English, <html lang> and dir follow it (right-to-left only for ar), and uk, zh-TW, zh-HK and zh-Hant browsers get English. Message catalogs live in frontend/messages/<locale>.json with en as the source and empty files for the target locales; a missing key falls back to English. ICU MessageFormat comes from a small in-house runtime (src/i18n/icu.ts, plural, select, ordinal, rich tags) with typed keys; it is checked against FormatJS in tests. next-intl 4.14 supports Next 15 but added about 14 kB gzipped to every route (measured: first-load JS of / 114.8 kB → 128.7 kB), so it was not used; the runtime adds about 2 kB (114.8 kB → 116.9 kB, the Next build table stays at 103 kB shared).
  • Language switcher in Settings → Preferences and a compact one in the sidebar. It lists only enabled locales by their own name and marks machine-drafted ones "Beta translation". Only English is enabled so far; the pseudo-locale en-XA (accented, about 35 % longer) is listed in dev builds and the demo, to find hard-coded strings and overflow.
  • Intl formatting helpers for numbers, dates, times and relative times that honour the locale and the Settings timezone (src/i18n/format.ts, useFormat()). Arabic keeps Latin digits and the Gregorian calendar.
  • npm run i18n:check (part of npm run lint): every catalog key must exist in English, every message must be valid ICU with the same placeholders and tags, enabled locales must be complete with all plural categories, code may only use keys English defines; unused keys and missing meta.json descriptions are warnings.
  • Every user-visible string of the dashboard and the demo now comes from the English catalog (frontend/messages/en.json, 674 keys in 17 namespaces (nav, common, errors, shell, auth, dashboard, posts, composer, calendar, accounts, approvals, settings, developer, media, analytics, legal and language). The English text is unchanged. Counts and states are ICU plural and select messages (no more string concatenation or hand-made "s"), dates, numbers, sizes and relative times follow the locale and the Settings time zone, and every key has a note for translators in messages/meta.json, including the ambiguous ones (Draft, Schedule, Expired, Never, Cancel vs Cancel post vs Keep post, Retry vs Try again, Connect, Requested by, Approved: action, "Post this code there", "{n} min left" and others).
  • npm run i18n:literals (part of npm run lint, and tests/i18n-literals.test.ts) fails on hard-coded English in JSX text, in aria-label, title, placeholder, alt and the copy props of components, and in string literals that read like UI sentences. The allow-list (frontend/scripts/i18n-literals.allow.json) names a reason for every exception: the Terms and Privacy text, static metadata and the web manifest, the demo seed and engine, API fallback messages. tests/i18n-extract.test.tsx renders screens in en-XA and fails if plain English is left on screen.
  • <T k="..." /> for server components, useErrorText() and nodes() (rich messages with links) in src/i18n; lib/* functions take the translator as a parameter and stay pure.
  • The sidebar navigation labels are the first strings read from the catalog.
  • Reconnect button on accounts that need reconnecting (OAuth and token networks).
  • Glossary: approvals, deny, trusted key, main text, operator, open-source and the approval statuses; docs/copy/style-guide.md section 7 describes server-side approval.
  • App: a "Skip to content" link as the first tab stop and a focusable main landmark; failed posts on the dashboard get a Retry button; the calendar "+N more" and the mobile post count open that day.
  • Data export (D-018): Settings, "Your data", builds a ZIP with your profile, posts with targets and attempts, connected accounts (no credentials), API key names, approvals, the audit log and your media files. The worker streams it to storage within its memory cap; the download link works for 5 minutes, the file is deleted after EXPORT_RETENTION_DAYS (default 7). One export at a time, one per 24 hours. New endpoints POST/GET /account/exports and GET /account/exports/{id} (browser session only), migration 00005 (indexes).
  • Account deletion (D-019): Settings, "Your data", "Delete account". It asks for your password and your email typed out, signs out every session and revokes every API key and MCP connection at once, sets scheduled posts back to drafts, and deletes everything after ACCOUNT_DELETION_GRACE_DAYS (default 7). Until then you can sign in, see a banner and cancel. The worker removes posts, media (S3 objects too), connected accounts and their stored credentials, keys, approvals and the audit log in batches; only a record of counts without personal data stays. New endpoints POST /account/delete and POST /account/delete/cancel (browser session only), user.deletion_scheduled_at and deletion_grace_days in /me, an email when deletion is scheduled, migrations 00006 and 00007.
  • 429 RATE_LIMITED answers can carry a specific Retry-After.

Changed

  • The dashboard and the demo are now offered in Russian, Spanish, Brazilian Portuguese, German, French, Indonesian, Japanese and Simplified Chinese (D-021): the eight locales are enabled in frontend/src/i18n/locales.ts and listed by their own names in the language switcher, all labelled "Beta translation" until a native review. Kazakh stays hidden until it is reviewed and A...
Read more

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 09:54
b184b6a

Added

  • Steerpost brand identity: new mark, wordmark and lockups (light, dark, currentColor), favicon and app icons, web manifest, social preview and README banners, a teal palette in tokens.css with the AA test extended to the brand pairs, --ease-steer and --duration-path motion tokens, the logo in the app sidebar, and a landing hero whose flow lanes steer through one approval gate. See docs/BRAND.md; copy is renamed separately.
  • Edit drafts and scheduled posts in the web UI. "Edit" on the Posts list and on the post page opens the composer (/compose?post=<id>) prefilled with the title, text, per-network overrides, media and schedule (shown in the Settings timezone); saving calls PATCH /posts/{id}. A scheduled post keeps its time unless you change it; a draft can be saved and scheduled in one step. Other statuses show why they cannot be edited. Unsaved edits are guarded (browser prompt on reload or close, a dialog on in-app links). The API has no ETag or If-Match, so before saving the UI re-reads the post and, if its updated_at or status moved, shows a conflict message with "Load the latest version" and "Save mine anyway" (best effort: the check and the write are not atomic).
  • The demo engine and mock-api answer PATCH /posts/{id} (text, per-network text, accounts, media, time), so the Pages demo supports editing.
  • Dashboard "Get started" checklist for new users: connect a network, write a first post, connect an AI agent (an MCP connection or an API key) and, optionally, review an approval. Each step ticks off from existing data (accounts, posts, keys, connections, approvals), has a one-line explanation and a direct link, and the list can be dismissed (remembered in this browser). The empty Accounts, Posts and Developer screens link back to the setup steps. The demo reflects its own state.

Changed

  • Rename shims for Steerpost (D-020). The MCP server reads STEERPOST_API_URL, STEERPOST_API_KEY and STEERPOST_TIMEOUT_MS first and falls back to the SOCIALOS_* names; both compose files set both URL variables. Releases publish the images under steerpost-{backend,mcp,frontend} as well as socialos-* (same digest and tags). deploy/README.md lists the legacy identifiers that keep the socialos name and the steps to make the new GHCR packages public.
  • autoupdate.sh follows redirects when it asks GitHub for the latest release (a renamed repository answers 301), reads GITHUB_REPO from the environment as well as .env, and logs a warning for any answer other than 200 instead of an info line.
  • Renamed to Steerpost (formerly SocialOS). Product copy, the MCP server name and the generated client config key (steerpost) changed. Stored and host identifiers keep the socialos name (cookies, headers, Redis keys, /opt/socialos, systemd units); see "Legacy identifiers" in deploy/README.md. Generated stdio configs keep the SOCIALOS_AUTH_HEADER variable so configs users already pasted keep working.

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 09 Oct 08:04
5afde53

Added

  • Agent request limit QUOTA_AGENT_RPM: requests per minute for all API keys and MCP connections of a user together, answered with 429 RATE_LIMITED. Off by default (-1), like the other plan limits (D-014).
  • MCP tool get_usage (the 14th, scope analytics:read) shows the plan, the period and what is used against each limit.
  • Settings shows a "Plan & usage" card: the plan, the period and what is used against each limit (from GET /account/usage), with plain-English copy for QUOTA_EXCEEDED.

Fixed

  • Host-proxy mode: MinIO's 80m memory cap made it thrash its page cache (327 GB read from disk in 3 hours, host IO stall, API 503). The default is now 192m with GOMEMLIMIT=144MiB, and socialos-guard.sh detects a thrashing container (memory at its cap plus fast workingset_refault_file growth), alerts naming it and restarts it once per cooldown instead of shedding services that cannot help (D-012). The README notes that scheduled GitHub workflows are best-effort and recommends an external uptime monitor.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 07:43
b7a7eee

Security

  • The generated Claude Desktop config no longer runs npx -y socialos-mcp: that npm package does not exist, and whoever registered the name would have received users' API keys. Configs, the MCP connections page and the docs now use Claude Desktop connectors or the mcp-remote bridge pinned to an exact version, and a regression test rejects socialos-mcp and unpinned npx packages.

Added

  • Plan limits (D-014): opt-in limits for one free plan: connected accounts, scheduled or published posts per UTC month and media storage, all unlimited by default so updates never cap an existing install. A public instance sets them with QUOTA_ACCOUNTS, QUOTA_POSTS_PER_MONTH and QUOTA_MEDIA_MB; -1 switches a limit off. Over a limit the action is refused with 403 QUOTA_EXCEEDED and nothing changes. Drafts are free, a post counts once per month, reconnecting an account you already have is free. Counting is race-free, so parallel requests cannot overshoot.

  • GET /account/usage (scope analytics:read) shows the plan, the period and what you have used against each limit.

  • Dashboard motion: route transitions through the View Transitions API (with a CSS fade-and-rise fallback), a sidebar highlight that slides between items, dialog and toast enter/exit, staggered lists on first load, skeleton shimmer, button press feedback and card hover lift. All motion uses transform and opacity with tokens from tokens.css and is disabled under prefers-reduced-motion. Empty states gained an icon and page headers a clearer hierarchy.

  • Public pages /privacy and /terms (no sign-in, linked from the sign-in and register forms and from the app sidebar and Settings), written for a self-hosted, single-operator instance and usable as the LinkedIn app's Privacy Policy URL. They are a template, not legal advice. The operator's name and contact come from the frontend container's OPERATOR_NAME and OPERATOR_CONTACT; without them the pages say so.

  • Breaking: POST /auth/register requires accept_terms: true (otherwise 400 VALIDATION_ERROR with fields.accept_terms). The register form has the matching checkbox. The accepted version and time are stored. Existing accounts are not blocked (D-016).

  • A new landing page: full-viewport hero with an animated flow of posts from agent to approval to networks, a real recorded video of the demo (WebM and MP4, light and dark, poster only on phones and with reduced motion), a sticky "agent, approval, network" walkthrough, parallax screenshots, true-fact counters, a marquee of the networks that publish today and an honest list of those that do not. Cross-document View Transitions between the landing page and the docs. Docs keep their own calm layout. See D-017.

  • Email verification, password reset and password change. Mailed links carry a single-use token in the URL fragment (48 h to verify, 30 min to reset). A reset signs out every session; a change keeps the current one. API keys and MCP connections are revoked only if you tick "Also revoke all API keys and MCP connections".

  • When MAIL_PROVIDER=smtp, unverified accounts get 403 EMAIL_NOT_VERIFIED on connecting networks, scheduling, publishing, editing scheduled posts and creating API keys. With the log provider nothing is restricted and the dashboard says mail is off. Existing accounts start unverified.

  • New pages /verify-email, /forgot-password and /reset-password, an email banner, and a Password section in Settings.

  • Database migration 00003 also prepares plans, quotas, data export and account deletion; it needs no manual step.

  • Connect a network with a pasted credential: POST /api/v1/social/accounts/token and the provider capability fields connect_fields, max_image_bytes and requires_title. It needs the new critical API-key scope social:connect, which is never in a default set (D-009).

  • "Connect with a token" on the Accounts page: Discord, Mastodon and Bluesky open a form built from the provider's connect_fields (secrets as password inputs with show/hide, never stored or put in a URL), with the how-to linked and plain-English errors per field. The demo and mock API answer the same fields and the token endpoint.

  • Posts are checked against the stricter of the network limits and the account's own limits, and networks that need a title reject posts without one.

  • SSRF-safe HTTP client for hosts users supply (D-010).

  • Discord: connect a channel with its webhook URL and publish text and up to 10 images, or delete a post. The URL is stored encrypted and never returned; a timeout after sending goes to review instead of risking a duplicate. See docs/integrations/discord.md.

  • Connect fields have a secret flag (connect_fields[].secret in GET /social/providers) for credentials that are not of kind secret, such as a webhook URL; forms must render them as password inputs.

  • Mastodon (and API-compatible Fediverse servers): connect with an instance URL and an access token, publish text and images (public), delete. Limits are read from the instance. A repeated publish after a timeout is safe because of the Idempotency-Key. See docs/integrations/mastodon.md.

  • Bluesky: connect with a handle and an app password, publish text with link and hashtag facets and up to 4 images, delete, and resolve unknown outcomes through a deterministic record key. See docs/integrations/bluesky.md.

  • Stub notes for Reddit, Medium and Hashnode; the X note now says the API is pay-per-use.

  • Sharing a host safely (host-proxy mode): every SocialOS container runs in the systemd slice socialos.slice (1 CPU, 664 MB, no swap, 512 tasks for the whole stack), may not swap and has a process cap; drop-ins cap dockerd and containerd; a guard timer stops the SocialOS worker, then the worker, MCP and UI, only when the host is under pressure and SocialOS is a real contributor (anonymous memory, not page cache, or its own CPU or IO), and resumes them once the host is calm; it also alerts on disk, data budget and the other services' health. A Caddy pre-check keeps a broken SocialOS snippet from stopping the host's Caddy. Runbook: deploy/host-proxy/apply-guardrails.md.

  • Approvals for dangerous actions (D-013): publishing now, retrying now, deleting a post, disconnecting an account, connecting with a pasted token, or scheduling less than 5 minutes ahead (AGENT_MIN_SCHEDULE_LEAD) with an API key or an MCP connection answers 428 APPROVAL_REQUIRED and waits for you. Approve or deny in the dashboard under Approvals (GET /approvals, POST /approvals/{id}/approve|deny); the agent repeats the same call with X-Approval-Id and it works once. New settings APPROVAL_TTL, APPROVAL_MAX_PENDING (per key), APPROVAL_RETENTION, AGENT_MIN_SCHEDULE_LEAD (also gates edits of posts that run soon); new audit entries approval.requested|approved|denied|used.

  • API keys have a dangerous_policy: approve (default, also for existing keys) or trusted (set when creating the key, skips approvals).

Changed

  • Breaking for API-key integrations: dangerous actions made with a key now need your approval (see above). The MCP tools publish_post, delete_post and disconnect_account no longer take confirm; they take an optional approval_id, and schedule_post and update_post take one too. An agent that only schedules at least 5 minutes ahead is not affected.
  • Password hashing is bounded in memory: at most PASSWORD_HASH_CONCURRENCY (default 2) hashes run at once within PASSWORD_HASH_MEMORY_MIB (default 48) of argon2 memory, new hashes use argon2id m=19 MiB, t=2, p=1 (OWASP), and existing hashes are upgraded at the next successful login. Under a burst, logins and registrations may get 429 RATE_LIMITED and should retry.
  • The SSRF guard also blocks site-local fec0::/10, IPv4-compatible ::/96 and local-use NAT64 64:ff9b:1::/48 (the whole range is blocked).
  • Browser sessions now hold ten scopes (the new social:connect); the "Dangerous" list in the dashboard shows it.
  • Host-proxy mode: memory caps rebalanced to fit the slice (backend and worker 160m with GOMEMLIMIT=100MiB, frontend 160m, postgres 112m, minio 80m, mcp 56m, redis 32m, migrate 64m). Automatic updates do not deploy while the guard has shed load.
  • One shared table for post attempts, analytics, API usage, API keys and the audit log. On phones each row becomes a stacked card showing every column (the Error column is no longer hidden), /developer no longer scrolls sideways at 390 px, and wide tables can be scrolled with the keyboard.

Fixed

  • Large uploads no longer exhaust memory on a small host: media is streamed to object storage in 5 MiB parts (no temp files, no whole-file buffering), the size and type limits are enforced while streaming and the partial object is removed on failure, and at most MEDIA_UPLOAD_CONCURRENCY (2) uploads run at once, the rest waiting 5 s and then getting 429 RATE_LIMITED with Retry-After. The web UI uploads directly to the API host, so videos are no longer cut at the Next.js proxy's 10 MB (D-015).
  • Cancelling, unscheduling or publishing a post while the worker publishes it can no longer deadlock and fail with a 500: the API and the worker now lock rows in the same order (post, then target). A database deadlock or serialization failure that still happens is reported as a retryable CONFLICT (#38).
  • Deploying or restarting the worker during a publish no longer sends that post to review: on SIGTERM the worker stops taking tasks, lets in-flight publishes finish (up to WORKER_SHUTDOWN_TIMEOUT, default 30s, at most 35s; the worker's stop_grace_period is now 45s instead of Docker's 10s), stops its background jobs and only then closes the database. The worker's health address is configurable as WORKER_HTTP_ADDR (#39).
  • The Posts date filter now uses the timezone chosen in Settings instead of the browser's, so "From" and "To" cover whole days where you expect th...
Read more

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 22:23
d1efe83

First release: the MVP, ready to self-host on one server.

Added

  • Web dashboard to connect social accounts, write a post once, tailor it per network and publish it now or schedule it.
  • REST API with browser sessions (CSRF-protected) and scoped, revocable API keys (sk_live_…, shown once, stored hashed).
  • MCP server with 13 tools, filtered by the key's scope, so AI agents can draft, schedule and review posts (Streamable HTTP and stdio).
  • Publishing to LinkedIn and Telegram; the other networks are shown honestly as "not available". Connecting a Telegram channel needs a one-time link code.
  • Scheduler and worker with idempotent publishing, retries and a clear post state machine; media uploads to S3-compatible storage (bundled MinIO, R2 or S3).
  • Production kit in deploy/: Docker Compose with automatic HTTPS, deploy.sh (migrations, readiness wait, automatic rollback), backups and a runbook.
  • Host-proxy mode for a small shared server: loopback-only ports, memory limits sized for about 1 GB, and an installer that adds one import line to the host's Caddy with validation, checks and automatic rollback.
  • Pull-based updates: a systemd timer deploys the latest GitHub Release when its images exist. It never downgrades and can be switched off with AUTOUPDATE=false.
  • Transactional mail through a queue, with SMTP and log-only adapters and templates for e-mail verification, password reset, password changed, account deleted and export ready.
  • Audit log of every MCP tool call (tool, route, status, target ids, agent), with an "Agent actions" filter in the dashboard; the MCP server and the backend share a gateway secret so the real client IP is recorded.
  • Setup guides for LinkedIn and Telegram, and a project site with an in-browser demo.

Changed

  • Dependencies refreshed: Go 1.26, pgx, minio-go, goose and other Go modules, frontend packages, Redis 8; CI actions moved to Node 24 on pinned runners.
  • The bundled MinIO now uses a maintained, digest-pinned image because minio/minio was removed from Docker Hub.
  • deploy.sh follows COMPOSE_FILE, cleans up only SocialOS images on shared hosts, and exits with 75 when it changed nothing.

Fixed

  • A scheduled post is never published before its scheduled_at; an early task is held briefly or handed back to the queue.
  • The demo's sample data no longer breaks in the first minutes of a month.

Security

  • The client address is read from the right of X-Forwarded-For and only through trusted proxies (TRUSTED_PROXIES), so a forged header can no longer earn a fresh login or register rate-limit bucket.
  • Rate limits group IPv6 clients by /64 and track a bounded number of keys.
  • Post targets are locked and read per user, with tests that no other user can read, change or lock them.
  • A Telegram channel can only be connected by proving ownership with a one-time link code.
  • CI scans every change: govulncheck, npm audit, gitleaks over the history, Trivy on the images and CodeQL; the runtime images no longer ship npm.