Auth & security
What a credential is allowed to do, how to take it away, and where the boundary sits.
How capability is computed
The user's role in the workspace ∩ the credential's scopes — and for writes, ∩ the publish tiers ticked when the credential was issued. Scopes only ever narrow; a credential can never do more than the person behind it.
A tool you lack scope for does not appear in tools/list — it is not an error, it is simply absent. So «the tool is missing» always means scope, never a bug.
Every single call re-checks that the authorizing user (class A) or the key's creator (class B) is still an active, unbanned member of that workspace. Remove them from the team and the credential stops working on its next call — no extra revocation step needed.
Scopes
| scope | Grants |
|---|---|
| workspace:read | Workspace info, member count, quota left |
| accounts:read | Connected social accounts and their auth status |
| posts:read | Drafts, schedules, publish history |
| posts:write | Create / edit drafts, upload media |
| approvals:write | Submit drafts for approval |
| schedule:write | Schedule posts |
| publish:execute | Publish immediately |
| comments:read | Read comments on published posts |
| comments:write | Reply to comments as the workspace's accounts |
| analytics:read | Metrics and trends |
| ideas:read | Read the idea board |
| ideas:write | Write ideas to the board |
The three publish tiers
| action | Needs | What happens |
|---|---|---|
| draft | posts:write | Saved as a draft. No external effect — start here. |
| schedule | schedule:write | Queued in one call, no confirmation step — popkit publishes for real at the time you set. scheduledAt must carry a timezone and be ≥ 60 s away. Counts against the monthly plan quota *and* a daily cap (class A shares the publish cap; class B has its own, wider one). The same-account interval does not apply. |
| publish | publish:execute | Goes out now, irreversibly. Class A first gets status: "draft" + confirm.token and must call confirm_publish (single-use, 10 min); a class-B key issued as pre-authorized publishes in one step. |
publish_post is a fourth path and takes one call with no confirmation, but only for content that is already approved (a human approved it in the app — there is no approve tool) or failed (a retry of something already sent once).
Tiers are ticked at issue time and cannot be widened later — reissue instead. get_workspace_context reports the ones you have under credential.allowedActions. Which accounts a publish reaches is covered in the calling rules (omitted targetAccountIds = every usable account, capped at 20).
Credential lifecycle
| A · OAuth token | B · long-lived key | |
|---|---|---|
| Who issues it | The end user, on the consent screen | A workspace admin or owner, in Settings |
| Lifetime | Access token 1 h, refresh 30 d | Unlimited, or 1–365 days if you set an expiry |
| Where it is stored | Held by the client | Shown once at issue; we store only a hash |
| Revoking it | Settings → Connected systems. An already-issued access token stays valid for up to 1 h | Disable or revoke — the next call fails, there is no grace period |
| If the person leaves | Stops working on the next call | Stops working on the next call; the key is disabled, not deleted, so the team can take it over |
Admins are notified 7 and 1 days before an expiring key lapses, and when a key starts failing a lot — an unattended integration that quietly breaks is the failure mode worth catching early.
Boundary and practice
- One credential sees one workspace — the workspace is fixed at issue time and cannot be switched by any parameter.
- Treat
popkit_it_…like a password: secret store only, never in a repo, never in a prompt. If it leaks, revoke and reissue — there is no way to recover the plaintext. - Writes that have an external effect are audited in the workspace; class-B calls are tagged «via external system \<credential name\>». The call log records input digests and key names, never your raw content.
- A credential without
accounts:readcannot read account handles — the publish precheck masks them, so it cannot be used to enumerate the workspace's accounts. - OAuth metadata (class-A SDKs read these automatically; you only need them if you hand-roll the flow):
https://popkit.loxily.com/.well-known/oauth-authorization-serverandhttps://popkit.loxily.com/.well-known/oauth-protected-resource/api/mcp.
authorization_code and refresh_token: there is no client_credentials path here. Unattended systems use a long-lived key. To identify a client without registering it, use CIMD — the client_id is an HTTPS URL serving the client's own metadata document, which popkit fetches at authorization time.