Chat
permdock/chat decides chat commands as the principal a verified chat author links to through principalByIdentity. An unlinked author is anonymous. ChatPermDock protects operations and applies approve and reject actions from chat as the clicking user.
permdock/chat lets a chat bot run the same operations (defineOperations) as the app's agents, as the person who typed the command. The chat platform verifies who sent a message or clicked a button. PermDock maps that platform user to a principal through an identity link the app owns, then decides as that principal with live roles and memberships.
Chat authors
A ChatAuthor is { provider, userId, workspace? }: the platform (slack, teams, …), the platform's user id and the workspace or team id. Build it only from an event whose platform signature the transport verified. chatSdkAuthor from permdock/chat-sdk builds it from a Chat SDK event.
subjectFromChat(author, { principals }) resolves the author through principals.principalByIdentity({ kind: 'chat', provider, value: userId, workspace }). A missing, malformed or unlinked author, or a source that throws, gives an anonymous subject, so every check denies with anonymous. It never throws.
The identity link is app data: one row per kind, provider, value and workspace, written when a signed-in user connects their chat account. memoryPrincipalSource({ principals, identities }) holds the links in process.
Protect a command
createPermDock(policy, { principals, memberships, approvals }) returns a ChatPermDock:
import { createPermDock } from "permdock/chat";
const chat = createPermDock(policy, {
principals,
memberships,
approvals: store,
});
const pay = chat.protect(operations["invoice.pay"], async ({ input, row }) => {
await invoices.pay(input.id);
return `Paid ${input.id}.`;
});
const result = await pay(author, { id: "inv_42" });protect(operation, handler) returns a command that resolves ChatCommandResult:
granted. The operation'sdataloaded the row and the author's principal is granted.valueis the handler's return value.denied. The author is unlinked, the row is in another tenant,datareturned nothing (validation) or threw (closure-error), or the policy denies. The handler does not run.approval-required. The grant needs an approval. With anapprovalsstore, the first call opens one request for the author, permission and row, and returns itsApprovalPrompt. A later call while the request is pending returns the same prompt. After an approver approves, the next call runs the handler once; after a rejection or that one run, the command denies until the request expires. Without a store,promptisnull.
Every decision is written to the sink with adapter: 'chat' and the operation's name and meta. getPermDock(author) returns the author's instance for checks outside a command.
Approve from chat
resolveAction(author, { approval, status, note? }) applies approved or rejected to the request with id approval, as the principal the author links to. The button carries the request id, never its token. The verdict goes through resolveApproval, so the approver must be eligible under the grant's approval.by and distinct from the requester. It resolves { ok: true, id, status } or { ok: false, cause }. cause is invalid-action, approver-unauthenticated for an unlinked author, or an approval error such as approver-is-principal, approver-not-eligible or approval-not-pending.
approvalCard and approvalActions from permdock/chat-sdk render the buttons and route their clicks to resolveAction. Send a prompt to approvers with delivery.
Why
A chat message names a platform user, not an account in the app. Display names and emails in a chat profile can be edited by the user or shared across workspaces, so the only trustworthy key is the platform user id in a signed event. The app links that id to a principal once, while the user is signed in to the app. PermDock reads the link on every command instead of caching a role, so a removed member loses access in chat at the same moment as in the app.
Unlinked means anonymous, not a guest account. A bot that ran commands for unknown users would let anyone in a shared channel act. Approvals from chat follow the same rule: the clicking user is resolved like any author, and resolveApproval refuses the requester, so a user cannot approve their own request by clicking their own card.
Last updated on
Delivery
permdock/delivery sends a notification only to recipients the sender's policy grants. Each recipient is checked with decideFor, routed to the email, chat or webhook transport of its channel, and recorded as one delivery event. An approval prompt links to the approval page and never approves.
Authentication and PermDock
PermDock never authenticates: it consumes material something else has already verified, turns it into a subject, and decides. This page defines what counts as verified, which claims may feed grants, and how tokens map to principal, actor and delegation.