Cloud
permdock/cloud is the thin, optional client for PermDock Cloud; it implements ApprovalStore, DecisionSink and SnapshotSource over a documented HTTP API, never sits in the decision path, and is provisioned from the Vercel Marketplace.
permdock/cloud is one implementation of the interfaces the rest of the package defines. cloud({ url, key }) returns a DecisionSink with a queryable decision log sold as compliance evidence and agent governance, an ApprovalStore with an inbox UI behind it, and a SnapshotSource that distributes signed snapshots and invalidates them on CAEP events. Nothing in this entry is consulted by can, decide, filter, where or simulate: every decision still runs in-process on the policy in your bundle. Remove the entry and the application keeps working on the in-memory defaults.
PermDock Cloud itself, the hosted service, lives in the separate PermDock-Cloud repository. This page defines what the client speaks; that repository implements it. Production hosts:
| Host | Service |
|---|---|
| app.permdock.com | Dashboard and inbox |
| api.permdock.com | Machine API (PERMDOCK_CLOUD_URL) |
| mcp.permdock.com | Read-only MCP server |
Identity gateway
The Cloud is also an identity gateway: each environment is an OAuth issuer whose issuer URL is the environment URL <PERMDOCK_CLOUD_URL>/v1/environments/<env> (cloudEndpoints().issuer), with /.well-known/openid-configuration next to its /.well-known/jwks.json. It federates upstream IdPs through RFC 8693 token exchange (below); authorization code plus PKCE and the device flow for agents follow later. It maps Agent Auth capability grants onto permission references. The only OSS integration is subjectFromJwt({ discovery: cloudEndpoints().issuer }). There is no cloud().identity helper, and the Cloud still never sits on the decision path.
The gateway exists because the Cloud already signs with a per-environment JWKS, and an OIDC Discovery document next to it is the other half of an issuer, not a new product. Humans and agents that use the inbox, the evidence export, the read-only MCP server and the hosted ADS need a first-party way to get those tokens. It is deliberately not a login SDK:
- Federation, not a session layer. An upstream IdP (Clerk, Auth0, WorkOS, Google, enterprise SSO, any Discovery issuer) authenticates; the Cloud issues the access token the application and the ADS consume. The application keeps its own sessions.
- One capability vocabulary. Agent Auth capability grants on a device-flow token name permission
keyorscopevalues, never a second set of names. - The library still does not authenticate. Core never verifies a token;
permdock/jwtverifies the Cloud's token exactly as it verifies any other issuer's, and there is nopermdock/cloud-authentry. A Cloud outage leaves the embedded engine deciding from the application's ownsubjectFrom*and tokens already issued.
Directory modes
Each environment picks one directory mode:
| Mode | Directory of record | How facts reach decide | Cloud outage |
|---|---|---|---|
| Cloud-native | The Cloud: users, groups, memberships, held declared roles and plans | Claims on the Cloud-issued access token (roles, groups, entitlements, the tenant claim per JWT authorization claims), verified by subjectFromJwt in your app | New logins fail; issued tokens keep deciding until they expire |
| Bring your own | Your tables, Clerk, Okta, Entra ID or Better Auth | The provider's token through its subjectFrom*, and your DirectoryStore / MembershipSource, fed by the SCIM relay when you use it | Nothing changes; memberships stay at their last synced state |
In both modes permdock/cloud implements neither MembershipSource nor RoleSource. Switching modes changes a connector and a claim mapping, not your policy.
In Cloud-native mode there is no new entry: the application verifies the Cloud's access token with permdock/jwt, like any other issuer.
import { cloudEndpoints } from "permdock/cloud";
import { subjectFromJwt } from "permdock/jwt";
const subject = await subjectFromJwt(request, {
discovery: cloudEndpoints().issuer, // the environment URL; issuer, JWKS and algorithms from OIDC Discovery
audience: "https://app.example.com",
claims: { tenant: "org_id", memberships: "memberships" }, // the environment's tenant claim, `org_id` by default
});| Claim on a Cloud-native access token | Carries | Read as |
|---|---|---|
sub | The Cloud user id | principal.id |
roles | Declared global roles (no on) the principal holds in the active tenant, from assignments plus group memberships at issue time | principal.roles |
memberships | [{ tenant, roles, team? }] for declared assignable roles with on: 'tenant' or 'team', normalised to the named scopes form on read (the canonical { scope, id, within? } form is planned for the Cloud) | principal.memberships |
The tenant claim (org_id unless the environment configures another name) | The active tenant of the exchange | The active tenant (claims.tenant), kept only when a membership matches it |
groups | Directory group ids | Team memberships through groupRoles or a MembershipSource |
entitlements | Plans held | principal.plans |
The token endpoint is POST <env URL>/oauth/token (RFC 6749 form body, client authentication with the environment's OAuth client, client_secret_basic). It supports RFC 8693 token exchange and refresh_token:
| Parameter | Value |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | An ID token from a trusted-issuer connector; the Cloud verifies it with permdock/jwt and maps its sub to a directory principal |
subject_token_type | urn:ietf:params:oauth:token-type:id_token |
audience | Your application; becomes the access token's aud |
tenant | Optional: the active tenant; defaults to the principal's only tenant |
The response is RFC 8693 { access_token, issued_token_type: 'urn:ietf:params:oauth:token-type:access_token', token_type: 'Bearer', expires_in }. The access token is an RFC 9068 JWT (typ: at+jwt) signed with the environment key: iss is the environment URL, aud the requested audience, sub the directory principal id, plus the claims above. Lifetimes are short (five minutes by default); there is no proprietary token-minting route and no endpoint that returns a subject for a request-time lookup.
The Cloud mints only names the pushed catalog declares (permdock cloud push), and the application still drops a role its policy does not declare and ignores a tenant with no matching membership. The package's scenario tests cover the contract with signed fixture tokens: a Cloud-native token decides locally, an undeclared role grants nothing, a tenant with no membership is no tenant, and a token for another audience is anonymous.
Two modes exist because two kinds of team ask for opposite things. Teams without an identity provider want one place to manage people, groups and held roles without building an admin UI; for them an issuer that knows nothing about roles is useless. Teams on Clerk, Okta, Entra ID, Better Auth or their own tables will not move their directory of record and want the Cloud only for federation, the provisioning relay and evidence. Both modes keep the same line: a directory that reaches the engine only as signed claims on a token verified locally sits on the login path, not the decision path, which is the pattern Clerk already uses with org_role, pla and fea. Letting permdock/cloud implement MembershipSource behind a cache was rejected because a remote table on the decision path is the failure mode of hosted control planes, and a cache does not change who is authoritative during an outage. Providers that want to act as a Cloud directory or issuer speak OIDC Discovery with JWKS, SCIM 2.0 and the PermDock claim table; there is no PermDock-specific provider interface and no per-vendor entry. Self-hosters get the same shape with their own issuer, subjectFromJwt, and their own DirectoryStore behind an admin screen.
- Assignment UI. The Cloud edits users, groups, memberships and which declared roles and plans a principal holds. It reads roles, plans and permissions from the catalog
permdock collectproduces, never accepts a free-text role, and only hands out roles the policy declaresassignable, narrowed by the tenant's assignable set and by what the acting admin holds there. - Freshness. A removal takes effect at the next token issue or refresh. Cloud-native access tokens default to a lifetime of minutes; for faster revocation the Cloud's CAEP transmitter sends
session-revokedorcredential-changefor the subject (SSF adapter) and the snapshot edge invalidates signed snapshots. Your app never looks the directory up live. - Evidence. Every assignment or removal writes a
membershipevent withsource: 'cloud'andbyset to the Cloud admin, so "who gave this person that role" is a decision-log query. - Hosted grants. Editing grants themselves, not only assignments, on permissions the code marks
hostable(below).
Hosted grants
A Cloud admin can write grants such as "the pro plan may read audit logs" without a deploy, bounded by the code policy:
- The Cloud publishes portable grants as a
PolicyDocument(v: 1, wire formats) under thepolicyclaim of a compact JWS withtyp: permdock-policy+jwt, signed by the environment key.cloud().policies.refresh()fetches it, verifies it with theverifieryou pass tocloud()(checkingtyp,exp, andissandaudagainst the environment URL<PERMDOCK_CLOUD_URL>/v1/environments/<env>), and keeps it. The Cloud setsexpto 24 hours after issue, so an instance that stops refreshing falls back to its code policy. An unsigned, wrongly typed or unverifiable document is absent, never partially applied, and the last verified document stays; without averifierthe source never holds a document. cloud().policiesis aPolicySource.createPermDockreads itscurrent()document once per instance; the application callsrefresh()on its own schedule (an interval, a cron, or thecatalogwebhook), so no check waits on the Cloud.- Hosted grants apply only to permissions the policy marks
hostable, target only declared roles, plans and relations, never override a code deny and never remove anapprovala code grant requires. The merge rules are on policies and the interface on extension interfaces. - A decision matched by a hosted grant records the document fingerprint and grant id in
matched.hosted, so the decision log can answer which hosted grant allowed it.
import { cloud, cloudEndpoints } from "permdock/cloud";
import { joseTokenVerifier } from "permdock/jwt";
const permdockCloud = cloud({
verifier: joseTokenVerifier({
jwks: cloudEndpoints().jwks,
algorithms: ["Ed25519", "ES256"],
}),
});
await permdockCloud.policies.refresh();
setInterval(() => void permdockCloud.policies.refresh(), 60_000);
const permdock = await createPermDock(policy, user, {
policies: permdockCloud.policies,
});A snapshot source never delivers hosted grants, because a snapshot is a per-subject result the server computed, not a policy; a snapshot the application builds from an instance already reflects that instance's merged grants. Fetching the policy per request would put the Cloud on the decision path.
Purpose
Four things are worth paying for around an embedded decision engine, in the order PermDock Cloud leads with them: a place where decisions accumulate as evidence, so an access review, an agent-activity review or "which agent did what for whom" is a query with a signed export rather than a log grep; a way for services that do not run TypeScript, and for API gateways, to enforce the same policy; a place where pending approvals wait for a person with a UI and notifications; and a hosted SCIM endpoint that relays directory provisioning into a store your application owns. PermDock Cloud provides all four as an Authorization Decision Service plus control plane, and permdock/cloud is the client for the first three (the fourth targets permdock/scim in your app). The Supabase model applies: every capability has an interface in the open-source package with an in-process default, the Cloud is the managed implementation, and the interfaces are documented well enough to implement yourself (approvals adapter, audit).
Design rules
- An embedded PDP, an optional ADS. PermDock is a policy decision point that runs inside your process, and it never needs a network call to decide. Hosted authorization products put every check behind a request and take strings in and booleans out; that adds latency to every check and makes the service a failure mode for the application. The pdp provider is the one opt-in exception, for permissions the application explicitly delegates to a remote PDP. The docs call the embedded engine the PDP and the hosted endpoint the Authorization Decision Service (ADS), never the other way round.
- No forced subscription. Everything the Cloud does, an application can do on its own with the interface and its in-process default, the way Supabase users can run Postgres themselves. That is why approvals and the decision log are interfaces rather than Cloud features, and why a gateway or MCP proxy in the decision path was rejected.
- One package, one server-only entry. The client is a subpath of
permdock, not a separate@permdock/cloudpackage, so consumers and thepermdock-wireskill have one install path and one version line; an app that never imports it pays nothing. The hosted service, its persistence, UI and billing live in the separatePermDock-Cloudrepository and follow this repository's wire formats. - Evidence leads. Buyers with budget pay for proof of who could do what and a way to stop it: procurement for access reviews and audit evidence, security teams for agent governance. The decision event (
principal,actor,delegation,tenant,outcome) and the bound approval token are that proof, so the decision log is the lead capability and the inbox is a feature on top. What makes an approval trustworthy is not the inbox but the resume rule: re-rundecideand recompute the token before trusting a stored approval. - Every event is evidence. Sinks receive every
granted,deniedandapproval-requiredevent; sampling is opt-in and never applies to the evidence log, because access reviews are about what was allowed. Ingest cost is handled by retention tiers. - Governance actions never decide. The Cloud can reject pending approvals and invalidate snapshots; it cannot deny a future check. A "freeze this agent" action is those two operations plus a kill-switch table the application's own
RoleSourceorcontextreads. - A relay, not a membership source. The hosted SCIM endpoint replays provisioning into a
DirectoryStoreyour application owns, so self-hosters get the same feature frompermdock/scimand the Cloud has one documented target instead of writing into arbitrary customer databases. - Approvals are resolved by people. No approval is resolvable through the Cloud's MCP server; approver identity comes only from the Cloud's own authentication, because an agent approving an agent's action is a denial the model can talk its way past.
- No authentication product and no gateway in the decision path. The identity gateway above federates and issues tokens; it does not replace the application's login SDK, and it never decides.
API
import { cloud } from "permdock/cloud";
const permdockCloud = cloud({
url: process.env.PERMDOCK_CLOUD_URL, // set by the Marketplace integration or by hand
key: process.env.PERMDOCK_CLOUD_KEY, // server-only; never in NEXT_PUBLIC_* or a client bundle
environment: "production", // defaults to VERCEL_ENV when present
});
// Plug into any adapter that accepts store / sink / policies
export const { getPermDock, getPermission, PermDockProvider, permdockHandler } =
createPermDock(policy, {
subject,
store: permdockCloud.approvals, // ApprovalStore: persistent, inbox UI, delivery
sink: permdockCloud.sink, // DecisionSink: batched, retried, queryable
policies: permdockCloud.policies, // PolicySource: hosted grants on hostable permissions (optional)
});cloud()returns a frozen object;approvals,sink,snapshotsandpoliciesare the interfaces,issueris the environment URL<PERMDOCK_CLOUD_URL>/v1/environments/<env>andjwksis the environment's JWK Set URL.cloudEndpoints()returns the sameissuerandjwkswithout a key, for building a verifier first. There is nodecideon it.verifyWebhook(request, { jwks, audience, replay })andparseCloudEventverify the Cloud's signed webhook deliveries (Cloud integrations); there is no unsigned mode.policiesholds the last verified policy document (hosted grants). It needsverifier(aTokenVerifieroverjwks); the expectedissandaudare the environment URL, so there is no audience to configure. Without a verifiercurrent()staysnull.sinkbatches events in memory and flushes on size (flushAt) and at the end of a request when the runtime exposeswaitUntil. It posts the rawSinkEventobjects; the Cloud wraps them in CloudEvents envelopes (wire formats). A failed batch is re-queued, bounded bycapacity(default 10 000, the same asmemorySink), and the oldest events are dropped first, so a Cloud outage never grows memory without limit. Every event is written; sampling is opt-in and never applies to the evidence log. Delivery failure never affects a decision and is reported throughon('error'). The Cloud's export endpoint serves the same events back as signed batches (permdock-decisions+jwt), OCSF or CSV. The sink does not acceptpermdock/otelspans: traces go to your collector (the Cloud can export a copy of the log to it as OTLP) and evidence goes to the sink (audit and observability).approvalsspeaks the sameApprovalRequestwire shape as the in-memory store (v: 1, with the optionalapproversandsubject.session); a Cloud inbox rejects an unknown major rather than approve without checking eligibility. The inbox UI, Slack, Teams and email delivery, and approver groups are Cloud features layered on the same records. Slack and Teams delivery is the Vercel Chat SDK recipe from the approvals adapter "Delivery" section, run by the Cloud instead of by your application; a self-hoster who copies that recipe gets identical cards and the same signature-verified responder.snapshotslets clients fetch a scoped snapshot from the Cloud edge instead of from the application server, and honourssession-revokedandcredential-changeCAEP events received by the Cloud's SSF receiver (Shared Signals). Everything the Cloud serves is signed: a compact JWS withtyp: permdock-snapshot+jwtand the unchanged Snapshot object under thesnapshotclaim (wire formats).
Key kinds
Each environment issues three kinds of API key. None of them can sign anything or stand in for a subject:
| Kind | Variable | Reaches |
|---|---|---|
client | PERMDOCK_CLOUD_KEY | What the running application and CI call: decisions, approvals, snapshot, policy and permdock cloud push (POST /catalog) |
admin | PERMDOCK_CLOUD_ADMIN_KEY | Authoring: hosted grants, directory principals, groups and assignments, connectors. Never deployed with the application |
export | Set per export destination | GET /export only |
A leaked client key cannot widen access: it can push a catalog, but the application merges hosted grants only on permissions its own code marks hostable, and authoring a grant needs the admin key or the dashboard.
Keys
The Cloud signs with a per-environment key through the TokenSigner interface (extension interfaces) and publishes the public half as a JWK Set at <PERMDOCK_CLOUD_URL>/v1/environments/<env>/.well-known/jwks.json (cloud().jwks). cloud().snapshots.get() asks for application/jwt and returns only a compact permdock-snapshot+jwt; an unsigned body is an error, never a snapshot. The client PermDockProvider that receives it verifies it with joseTokenVerifier({ jwks: cloud().jwks, typ: 'permdock-snapshot+jwt' }); a consumer in another language points its JOSE library at the same JWKS. Algorithms are Ed25519 by default, ES256 on request for consumers whose libraries lack Ed25519; never RS256 in a FAPI 2.0 environment. Rotation follows the JWKS rule: the new kid is published, the old key stays until the last snapshot signed with it has passed exp, and the dashboard shows both. Signed decision-log exports (typ: permdock-decisions+jwt) use the same key and endpoint (audit and observability). The private key never leaves the Cloud; the PERMDOCK_CLOUD_KEY API key is unrelated to it and cannot sign anything.
- Zero required dependencies: the client uses
fetchand the wire formats from core. Each request is aborted after 10 seconds, combined with anysignalyou pass, and then fails like an unreachable Cloud. It is server-only andtests/bundleasserts no client entry reaches it.
HTTP API
The client speaks these paths under PERMDOCK_CLOUD_URL. In production that URL is https://api.permdock.com; a self-hosted deployment of the Cloud repository works by pointing PERMDOCK_CLOUD_URL at it, with no Marketplace involvement and no discovery step. :env is environment, then PERMDOCK_CLOUD_ENV, then VERCEL_ENV, then production.
| Method | Path | Interface |
|---|---|---|
POST | /v1/environments/:env/decisions | sink.write / sink.flush body { events } |
POST | /v1/environments/:env/approvals | approvals.create |
GET | /v1/environments/:env/approvals/:token | approvals.get (unreachable or unknown → null) |
POST | /v1/environments/:env/approvals/:token/resolve | approvals.resolve (409 not pending, 410 expired, 404 missing, 403 approver refused). A body code that names an ApprovalError code, with its detail, becomes the thrown error |
POST | /v1/environments/:env/approvals/:token/consume | approvals.consume: the approved record with consumedAt set, or 409 when it is not approved, already consumed or expired (any failure → null) |
GET | /v1/environments/:env/approvals | approvals.list: ?status=, principalId, actorId, tenant, session, limit, cursor; returns { items, next? } (unreachable → { items: [] }) |
POST | /v1/environments/:env/approvals/expire | approvals.expire |
POST | /v1/environments/:env/approvals/cancel | approvals.cancel (what cancelApprovals calls on session revocation): body { filter, by, note? } with an ApprovalListFilter; rejects the matching pending requests with resolvedBy: 'system:<by>' like the in-memory store and returns { cancelled }; a non-2xx status throws |
GET | /v1/environments/:env/snapshot | snapshots.get with accept: application/jwt: a compact permdock-snapshot+jwt as the body; an unsigned JSON body throws |
POST | /v1/environments/:env/catalog | permdock cloud push: body { fingerprint, catalog, policy? } (cloud); 200 { version, unchanged: true } when the fingerprint is already published; not called by cloud() |
GET | /v1/environments/:env/policy | policies.refresh: a permdock-policy+jwt compact JWS as the body; 404 means no hosted grants and clears the document, any other failure keeps the last verified one |
Authoring and export routes, used by the dashboard, scripts and the contract tests, never by cloud(). Validation failures are Problem Details (wire formats):
| Method | Path | Key | Purpose |
|---|---|---|---|
GET | /v1/environments/:env/catalog, /catalog/versions | client | The latest catalog and its versions |
GET, POST | /v1/environments/:env/hosted-grants | admin | List ({ items }) or author a hosted grant: body a HostedGrant without id, 201 with the stored grant; a grant mergeHostedGrants would drop is 422 with the drop reason as reason |
DELETE | /v1/environments/:env/hosted-grants/:id | admin | Remove a hosted grant; 204. Every change issues a new policy document |
GET, POST | /v1/environments/:env/directory/principals, /directory/groups | admin | Cloud-native directory: list ({ items }) or create (201 { id }); a principal is created with the trusted-issuer subject it federates from |
GET, POST, DELETE | /v1/environments/:env/directory/assignments[/:id] | admin | Role assignments { principal | group, tenant, team?, role }: only catalog roles marked assignable, narrowed to roles the acting admin holds there; each change writes a membership event with source: 'cloud' |
GET, POST | /v1/environments/:env/connectors | admin | Integrations: { kind, name, config }, for example { kind: 'webhook', name, config: { url, types } } (Cloud integrations) |
GET | /v1/environments/:env/export?format=jws|ocsf|csv|otlp | export | The decision log as a permdock-decisions+jwt, OCSF (toOcsf), CSV (toCsvRow) or OTLP logs |
POST | /v1/environments/:env/oauth/token | OAuth client | RFC 8693 token exchange and refresh_token (identity gateway) |
GET | /v1/environments/:env/.well-known/jwks.json, /.well-known/openid-configuration | none | The environment's JWK Set and issuer metadata; public |
| any | /scim/v2/:connection/* | The connection's bearer | The hosted SCIM endpoint an IdP provisions into; replayed to your scimHandler |
Environment variables
| Variable | Set by | Used for |
|---|---|---|
PERMDOCK_CLOUD_URL | Marketplace provisioning or you | Base URL of the environment's API. Production host is https://api.permdock.com |
PERMDOCK_CLOUD_KEY | Marketplace provisioning or you | Server-side client key for the store, sink, snapshot, policy and catalog endpoints; rotated from the dashboard |
PERMDOCK_CLOUD_ADMIN_KEY | You, from the dashboard | admin key for authoring scripts and the contract tests; never set in the application's runtime environment |
PERMDOCK_CLOUD_ENV | Optional | Overrides the environment name when VERCEL_ENV is absent |
permdock doctor reports whether the variables are present, whether the key reaches the API, and whether a client entry imports permdock/cloud.
What stays local and what the Cloud adds
| Concern | In-process default (always available) | With permdock/cloud |
|---|---|---|
| Deciding | decide on the bundled policy | Unchanged |
| Pending approvals | memoryApprovalStore() | Persistent store, inbox UI scoped per tenant, delivery, approver groups, expiry jobs |
| Memberships and custom roles | subjectFrom*, context, your MembershipSource and RoleSource; permdock/scim writing a DirectoryStore you own | A hosted SCIM relay per tenant (IdP setup wizard, group-to-role mapping UI, sync log) that replays provisioning to your scimHandler with an RFC 7523 bearer. In Cloud-native directory mode, the Cloud directory and assignment UI instead, delivered as claims on the tokens it issues. Either way the Cloud implements neither interface and is never read at request time, so it never joins the decision path (invariant 15); in bring-your-own mode it holds no authoritative copy |
| Evidence and governance | on('decision') handler, a DecisionSink over your table, permdock/otel for traces | Decision log with retention tiers; access-review, agent-activity, approval-chain and tenant queries over the principal, actor, tenant, membership and via fields of every event; signed (permdock-decisions+jwt), OCSF and CSV exports for auditors, SIEMs and compliance platforms; governance actions limited to rejecting pending approvals and invalidating snapshots (audit and observability "Evidence and governance") |
| Snapshots | Served by the application's endpoint, plain JSON or signed with your own TokenSigner | Edge distribution, always signed (permdock-snapshot+jwt, keys at the environment's /.well-known/jwks.json), invalidated by CAEP |
| Non-TypeScript callers | Your own permdock/authzen deployment | Hosted AuthZEN ADS serving the same policy to gateways and services |
| Catalog and usage | permdock collect and permdock usage in CI | Dashboards over the same catalog files, drift alerts |
Hosted Authorization Decision Service
The Cloud runs the same permdock/authzen handler your app could run, against the policy you publish with permdock cloud push. Kong, Envoy, Tyk, Zuplo or a Go service calls POST /access/v1/evaluation and gets an AuthZEN answer whose context carries outcome, denial reasons and the approval token (AuthZEN). Decision semantics are identical to the embedded engine because it is the same code; an evaluations batch of a policy matrix from permdock/testing is the parity test.
Caller authentication is real authentication (threat model, invariant 9):
- On Vercel, the caller presents its OIDC token; the ADS verifies it with
permdock/jwtagainst Vercel's JWKS and mapssub,project_idandenvironmentto a workload principal. - Elsewhere, the caller uses OAuth client credentials issued per environment in the dashboard; the token's
audis the environment URL. - A request with no verifiable token is answered
401; a token for another environment is403. There is no shared static secret mode.
The ADS never reads a subject from the request body unless the caller is registered as a trusted PEP for that environment, matching the in-app endpoint rule.
Vercel Marketplace
PermDock Cloud is listed as a native Marketplace integration so a Vercel project gets an environment, its variables and billing without leaving the dashboard.
- Resource model. One Marketplace resource is one PermDock environment, created per Vercel project and environment (
production,preview,development) so preview approvals never land in the production inbox.PERMDOCK_CLOUD_URLandPERMDOCK_CLOUD_KEYare written to the project's environment variables on provisioning and rotated from either dashboard. - Integration server. The Cloud repository implements the Marketplace provisioning API (install, resource create, update, delete, SSO, billing webhooks and invoices) per the approval checklist; this repository only documents the variables and the
doctorcheck. - Template.
apps/examples/eve-agentis the deploy template the listing requires: an Eve agent withposttools,permdock/eveapprovals stored in the Cloud, and the inbox linked from the deployed app (Eve adapter). - Plans. A free environment with a short retention window, then usage-based plans on five meters: monthly active principals (a human and each agent
actorcounted once), connected tenants (a tenant with a SCIM connection or a tenant-scoped inbox), resolved approvals above a free allowance at a small per-unit price, the decision retention tier, and hosted ADS evaluations. Decisions made by the embedded engine are never metered. The exact numbers belong to the Cloud repository and the landscape research, not to the library docs.
Integrations
The Cloud has an Integrations page; what each connector speaks is fixed here and catalogued on Cloud integrations. Every connector is a standard wire format or an existing PermDock interface (trusted issuers over JWKS or OIDC Discovery, CAEP transmitters, SCIM sources, Vercel provisioning and OIDC, OTLP and OCSF export, CloudEvents webhooks, signed and CSV evidence exports, Chat SDK delivery, a read-only MCP server), and none sits on the decision path. No connector adds an npm entry.
Request lifecycle
- The application decides locally. Nothing on this path touches the network.
on('decision')events go topermdockCloud.sink, which batches and posts them toPOST /v1/environments/:env/decisionswith the key.- An
approval-requiredoutcome causes the adapter to callpermdockCloud.approvals.create, which posts theApprovalRequesttoPOST /v1/environments/:env/approvals. The Cloud notifies the configured approvers. - An approver resolves the request in the inbox UI, through a Slack or Teams card (Chat SDK
requestApproval; the platform signature identifies the responder) or through an email link that lands on the authenticated inbox. The Cloud authenticates the approver, applies the actor and distinct-approver rules, recordsresolvedBy. - The application's retried call reads the record via
permdockCloud.approvals.get, re-runsdecidelocally, compares the token and proceeds. The resumed decision goes to the sink like any other. - Independently of the above, an identity provider connected to the tenant's hosted SCIM endpoint pushes a provisioning operation. The Cloud records it in the sync log, applies the tenant's group-to-role mapping as the
urn:permdock:scim:schemas:extension:roles:1.0attribute, and replays the operation to the application'sscimHandlerwith an RFC 7523 JWT bearer signed by the environment key (iss= the environment URL,aud= your endpoint,tenant= the tenant, a 60-secondexp). YourDirectoryStoreis updated; the nextcreatePermDockfor an affected principal reads the change throughdirectoryMembershipSource(SCIM adapter). If your endpoint is unreachable the Cloud retries with backoff and shows the failure in the sync log; nothing is decided from the Cloud's copy.
What it validates
- The key is present and server-side; the entry throws at construction (not at decision time) when
urlorkeyis missing, so misconfiguration is visible at boot. - Responses from the API are validated against the wire-format versions this package knows; an unknown
vis rejected and reported, and the adapter falls back to the in-memory store for that request so decisions never depend on the API being reachable. - Approver identity on the Cloud side comes from the Cloud's own authentication; the client never asserts who approved.
How denials surface
- The Cloud is not on the decision path, so it produces no denials of its own. A resume against a request the Cloud reports as
rejectedorexpiredsurfaces through the calling adapter exactly as with any store (approvals adapter). - API unreachable during a resume:
deniedwithdetailapproval-not-foundand anon('error')event; the application may retry. - The hosted ADS answers denied evaluations with
decision: falseandcontext.permdock.outcomeplus denial reasons, and transport errors as Problem Details (AuthZEN adapter).
Example app
apps/examples/eve-agent (also the Marketplace template) and a cloud variant of apps/examples/authzen-pdp in which the PEP process points at a hosted environment instead of the local Hono PDP, asserting identical decisions for the policy matrix.
Related standards
- AuthZEN: the hosted ADS wire format.
- Shared Signals and CAEP: snapshot invalidation.
- SCIM 2.0: the hosted relay's protocol and the RFC 7523 bearer it presents to
scimHandler. - Wire formats:
ApprovalRequest,DecisionEvent, the snapshot and the signed-output envelope. - JOSE and the JWT adapter: the
TokenSignerandTokenVerifierthe Cloud signs and clients verify with. - Approvals adapter and Audit and observability: the interfaces this entry implements.
Last updated on
Approvals
permdock/approvals is the pluggable store behind every approval-required decision, an in-memory default, a Fetch handler for approvers, and the interface that self-hosted stores and PermDock Cloud implement.
Cloud integrations
The connectors PermDock Cloud offers on its Integrations page, what each one speaks and what it never does; every connector is a standard wire format or an existing PermDock interface, none is an npm entry, and none sits on the decision path.