Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@ by their tags and commit history.

## [Unreleased]

## [0.11.0] - 2026-10-10

Needs laravel-id 1.24 / the Cbox ID release with feature flags, fine-grained
authorization, Pipes and SMS as a second factor. Against an older instance the new claim
reads as absent and the new endpoints answer 404.

### Added

- Feature flags: `feature_flags(source)` and `has_feature(source, key)` read the
`feature_flags` claim from a signed-in user or a verified claim mapping;
`CboxUser.feature_flags` (`None` when the scope was not requested, `[]` when nothing is
on) and `CboxUser.has_feature(key)`. `FEATURE_FLAGS_SCOPE` is the scope to request.
- Management API regenerated from the current specs (Cbox ID wave 10):
`env.feature_flags.*` (including `evaluate({"user_id", "organization_id"})`), `env.fga.*`
(`check`, `check_batch`, tuples write/delete/list, resources and subjects lists, schema
get/update/validate, with `consistency_token` on every read), `env.pipes.*` (pipe setup,
grants, connections), `env.radar.*`, `env.signin.sms.*`, the environment sign-in methods
(`env.signin.social.*` update/enable/disable/inherit/offered, `environment_wide` /
`organization_id` on social set, the auth policy's method fields),
`env.branding.appearance.set()` taking the logo and favicon as base64 data URIs, `env.users.mfa.sms.remove()`, the
HRIS directory routes (`env.directories.*`), and on the account plane
`me.mfa.sms.remove()` and `me.pipes.disconnect()`. `fga_tuple()` writes a tuple in the
notation the batch check takes.
- `FrontendClient.config(organization=...)`: the sign-in document for one organization (by
id or slug), whose `social` list is exactly what that organization's hosted page offers.
Each organization is cached on its own. `FrontendConfig.methods` says whether passkeys and
magic links are switched on.
- Pipes: `client.pipes.lease_token(provider, user_id=..., purpose=...)` leases a fresh
token for a person's connected account with a cached client-credentials `vault.lease`
token; `PipesClient(issuer, access_token)` does the same with a token you hold. Refusals
are typed: `PipeNotConnectedError` and `PipeReauthorizationRequiredError` carry
`connect_url` (and `connect_url_with(client_id=, return_to=)`),
`PipeTemporarilyUnavailableError` carries `retry_after`, `PipeLeaseDeniedError` is the
403, all under `PipeLeaseError`. `client.pipe_connect_url(provider, return_to)` and
`pipe_connect_url()` build the hosted connect page.

### Changed

- The generator reads an action's own `202 Accepted` body (documented as `oneOf` it and the
approval body), so `env.directories.sync()` now returns the directory.

## [0.10.0] - 2026-10-08

### Added
Expand Down
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ from cbox_id import FrontendClient

frontend = FrontendClient("https://id.acme.com", "pk_live_…")

config = frontend.config() # endpoints, social buttons, the customer's theme
config = frontend.config() # endpoints, social buttons, which methods are on, the theme
acme = frontend.config(organization="acme") # the buttons acme's hosted page shows
session = frontend.session(token) # session.user is None when nobody is signed in
```

Expand Down Expand Up @@ -251,6 +252,30 @@ if org and org.role is OrganizationRole.OWNER:
Matching is exact — `invoices:*` does not grant `invoices:delete`. An `org_role` this SDK
version does not recognise reads as `None`, never as a tier it would have to guess.

### Feature flags

Request the `feature_flags` scope (and give the app that scope on its Scopes tab) and the
tokens and UserInfo carry the keys of every flag that is on for the person, in the
organization they signed in to:

```python
from cbox_id import FEATURE_FLAGS_SCOPE, CboxIdConfig, has_feature

config = CboxIdConfig(..., scopes=["openid", "profile", "email", FEATURE_FLAGS_SCOPE])

user.feature_flags # ["acme-beta", "new-dashboard"], or None
if user.has_feature("new-dashboard"):
show_new_dashboard()
has_feature(access_token_payload, "billing.v2") # on a verified claim set too
```

`None` means the claim is absent — the scope was not requested — and `[]` means nothing is
on. Either way `has_feature()` is `False`: a missing scope turns every feature off, never
on. A token carries the flags as they were when it was issued; the next refresh picks up a
change. To ask without a token in hand (a job, a webhook handler), use
`env.feature_flags.evaluate({"user_id": ..., "organization_id": ...})` from the
[Management API](#management-api).

### Support sessions

A staff member can act as one of your users for a limited time (at most an hour, no refresh
Expand Down Expand Up @@ -286,6 +311,37 @@ everywhere" needs. Machine tokens and introspection are confidential-client call
require a `client_secret`; revocation also works for a public (PKCE) client, which names
itself in the request body instead.

## Pipes: a person's own GitHub, Google, Slack… token

When a person has connected their account at a provider, lease a fresh access token for it.
`client.pipes` gets its own client-credentials token with the `vault.lease` scope (and
reuses it until it nearly expires); Cbox ID refreshes the provider token first when it is
about to expire:

```python
from cbox_id import (
PipeNotConnectedError,
PipeReauthorizationRequiredError,
PipeTemporarilyUnavailableError,
)

try:
token = client.pipes.lease_token("github", user_id=user_id, purpose="list-repos")
# call https://api.github.com/user/repos with token.access_token, then drop it
except (PipeNotConnectedError, PipeReauthorizationRequiredError) as e:
return redirect(e.connect_url_with(client_id=CLIENT_ID, return_to=request.url))
except PipeTemporarilyUnavailableError as e:
retry_later(e.retry_after)
# PipeLeaseDeniedError: the app is not granted this pipe — a configuration problem.
```

With a token issued for the person (they signed in to your app), use
`PipesClient(issuer, user.access_token).lease_token("github", purpose=...)` and leave
`user_id` out. To send somebody to connect before any lease,
`client.pipe_connect_url("github", return_to)` is the hosted connect page, preselected to
your app; they come back with `?provider=github&status=connected` (or `cancelled`,
`failed`).

## Declare roles & permissions

Your app declares its authorization **roles** and **permissions** in code and publishes
Expand Down Expand Up @@ -505,6 +561,50 @@ for org in env.organizations.list_all({"limit": 100}):
print(org["id"], org["name"])
```

### Fine-grained authorization

`env.fga` writes relationship tuples and asks checks. Every write answers with a
`consistency_token`; pass it to a check that must see the write:

```python
from cbox_id.management import fga_tuple

written = env.fga.tuples.write(
{
"tuples": [
{
"resource_type": "document",
"resource_id": "leave",
"relation": "viewer",
"subject": {"type": "user", "id": "alice"},
}
]
}
)

answer = env.fga.check(
{
"resource_type": "document",
"resource_id": "leave",
"relation": "viewer",
"subject_type": "user",
"subject_id": "alice",
"consistency_token": written.data["consistency_token"],
}
)
answer.data["allowed"]

# Up to 100 at once, in the tuple notation, answered in order:
env.fga.check_batch(
{"checks": ["document:leave#viewer@user:alice", "document:readme#editor@user:alice"]}
)
```

`env.fga.tuples.delete()`, `env.fga.resources.list()` (which documents can alice view),
`env.fga.subjects.list()` (who can view this one), and `env.fga.schema.get()` /
`.update()` / `.validate()` complete it. `fga_tuple()` writes the notation from the same
mapping `tuples.write` takes.

### Audit Logs

Your app records what its users did, per organization (your customer), and Cbox ID keeps
Expand Down
67 changes: 67 additions & 0 deletions openapi/account.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ tags:
description: "Where a held operation's approval stands."
- name: Organizations
description: 'The organizations you are a member of — and leaving one.'
- name: 'Connected services'
paths:
'/me/action-approvals/{id}':
get:
Expand Down Expand Up @@ -171,6 +172,34 @@ paths:
$ref: '#/components/responses/UnprocessableEntity'
429:
$ref: '#/components/responses/TooManyRequests'
/me/mfa/sms:
delete:
tags:
- 'Sign-in methods'
summary: 'Remove your phone number for text-message sign-in codes.'
description: 'Requires scope `account:sign_in:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: critical.'
operationId: account_mfa_sms_remove
x-action: account.mfa.sms.remove
x-scope: 'account:sign_in:write'
x-danger: critical
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/CboxApproval'
responses:
204:
description: Done.
202:
$ref: '#/components/responses/ApprovalRequired'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
409:
$ref: '#/components/responses/Conflict'
422:
$ref: '#/components/responses/UnprocessableEntity'
429:
$ref: '#/components/responses/TooManyRequests'
'/me/organizations/{organization_id}/api-keys':
post:
tags:
Expand Down Expand Up @@ -363,6 +392,44 @@ paths:
$ref: '#/components/responses/UnprocessableEntity'
429:
$ref: '#/components/responses/TooManyRequests'
'/me/pipes/{provider}':
delete:
tags:
- 'Connected services'
summary: 'Disconnect one of your connected services, revoking the access you gave it.'
description: 'Requires scope `account:pipes:write` on an access token you delegated; it acts on your own account only. No management key is accepted. Danger: destructive.'
operationId: account_pipes_disconnect
x-action: account.pipes.disconnect
x-scope: 'account:pipes:write'
x-danger: destructive
parameters:
-
name: provider
in: path
required: true
schema:
type: string
description: "The provider's key, for example `github`."
maxLength: 64
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/CboxApproval'
responses:
204:
description: Done.
202:
$ref: '#/components/responses/ApprovalRequired'
401:
$ref: '#/components/responses/Unauthorized'
403:
$ref: '#/components/responses/Forbidden'
404:
$ref: '#/components/responses/NotFound'
409:
$ref: '#/components/responses/Conflict'
422:
$ref: '#/components/responses/UnprocessableEntity'
429:
$ref: '#/components/responses/TooManyRequests'
/me/profile:
patch:
tags:
Expand Down
Loading
Loading