Outcome
Change the scope or expiry of one unrevoked non-workspace resource grant, including an expired grant.
These examples prove local handler behavior with real isolated SQL and explicitly simulated identity/permission dependencies.
Before you start
-
Verify identity and the current credential-scoped action contract before preparing input. Resource-grant operations authorize the target organization/app at runtime; metadata discovery does not prove execution permission.
-
Discover the recipient and existing application resource. Use one exact organization/app/type/id tuple consistently; a grant never creates the target resource.
-
Review the validated plan and intended effect before confirmation. Capture previous grant settings before replacing/updating/revoking and all organization grants/membership before promotion.
Discover the target
topolo whoami --json
topolo apps --json
topolo actions get app_topolo_auth.resource_grants.update --detail --json
Discover the recipient and target resource using owning-application actions. Read app_topolo_auth.resource_grants.list_resource with the exact resource tuple and follow all pages.
Input fields
Supply scope or expiresAt as well as grantId; grantId alone is invalid.
| Field | Required | Meaning |
|---|---|---|
grantId | Yes | Stored resource-grant id discovered through the exact resource listing; update and revoke resolve its own organization/app boundary. For an expired unrevoked row, use a previously captured exact id: live listings omit it, and no public historical grant lookup is exposed by this action family. |
scope | No | Use read, read_write or admin. Create defaults to read and its legacy string input is constrained by the SQL schema; unsupported values can cause SERVER_ERROR. Update explicitly validates these three values. The owning application must enforce the chosen scope. |
expiresAt | No | Epoch seconds or null for no expiry. Use a positive integer when setting expiry; past expiry removes live access. Create also accepts legacy strings, which are not reliably date-validated: convert timestamps to integer seconds before execution. Update accepts positive integer seconds or null only. |
Effects
- Changes scope or expiry without changing the stored grant id; a future expiry can restore an expired unrevoked grant.
Interpret the response
The raw target success envelope is success/message/data/timestamp. Create returns HTTP 201; all other successes return 200. CLI and MCP may wrap or project this envelope.
Grant id and createdAt remain stable when create updates the same user/organization/app/resourceType/resourceId key or reactivates a revoked grant. Omitted scope/type/metadata/expiry reset to create defaults rather than preserve previous values. Take a snapshot before replacing an existing grant.
Live lists omit revoked and expired rows. page.hasMore controls continuation; nextCursor is null at the end. list_mine contains only the authenticated subject, never a supplied userId. list_resource additionally returns recipient id/email/name or null when identity lookup is unavailable.
Create data.provisioned means this call created an unverified identity. It does not imply membership, invitation, sign-in, recipient acceptance or resource access. A create with past expiry can succeed with data.grant null because its stored row is already absent from live access.
Grant createdAt, expiresAt and revokedAt are epoch seconds; response timestamp is ISO text. Legacy create expiry strings can remain strings in stored output. Convert dates before planning; never infer live access from a successful storage response.
Update preserves identity and can extend an expired unrevoked grant. A revoked row is refused. Revoke returns only data.id, including on repeated revocation, and does not delete the recipient identity or affect separate membership/other grants.
Promote creates organization membership before revoking all non-workspace resource grants for that user and organization across apps. appId limits the permission check, not the revocation scope. These writes span databases and are not atomic; a 500 can leave membership active while grants remain. Read membership/effective access and remaining grants before any reviewed retry or compensation.
Promote data.role echoes the requested/default role. Existing membership, stored user role and effective authorization require separate verification. Notification delivery is nonfatal and is not proven by success. An empty list_mine after promotion does not mean the user has lost organization access.
AUTH_ERROR (401) is used both for absent/invalid tokens and for authenticated callers lacking organization or app grant authority. Inspect the error and identity before assuming OAuth expired; preserve authorization denials rather than changing credential context automatically.
Worked examples
These are complete raw target-handler responses, not CLI/gateway envelopes or live authorization proof. Replace synthetic identifiers using authorized discovery. Save only the reviewed input as payload.json; inspect validation and plan before executing. --confirm assumes the intended operation is approved. Metadata read-back requires separate recipient-principal and owning-application access tests.
Update scope and future expiry preserves the grant id
Input · payload.json:
{
"grantId": "rg_JOD6QZYnE1JA",
"scope": "admin",
"expiresAt": 1791050500
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.update --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.update --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.update --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.update",
"input": {
"grantId": "rg_JOD6QZYnE1JA",
"scope": "admin",
"expiresAt": 1791050500
},
"confirm": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "Resource grant updated",
"data": {
"grant": {
"id": "rg_JOD6QZYnE1JA",
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"grantType": "consumer",
"scope": "admin",
"expiresAt": 1791050500,
"grantedBy": "usr_operator",
"metadata": {
"purpose": "interview"
},
"revokedAt": null,
"createdAt": 1790964100
}
},
"timestamp": "2026-10-02T18:01:40.147Z"
}
Explicit null removes expiry
Input · payload.json:
{
"grantId": "rg_JOD6QZYnE1JA",
"expiresAt": null
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.update --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.update --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.update --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.update",
"input": {
"grantId": "rg_JOD6QZYnE1JA",
"expiresAt": null
},
"confirm": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "Resource grant updated",
"data": {
"grant": {
"id": "rg_JOD6QZYnE1JA",
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_one",
"grantType": "consumer",
"scope": "admin",
"expiresAt": null,
"grantedBy": "usr_operator",
"metadata": {
"purpose": "interview"
},
"revokedAt": null,
"createdAt": 1790964100
}
},
"timestamp": "2026-10-02T18:01:40.148Z"
}
Updating expired unrevoked grant to future expiry restores live listing
Input · payload.json:
{
"grantId": "rg_ZgLq3BZDdHf-",
"expiresAt": 1791050500
}
CLI:
topolo actions validate app_topolo_auth.resource_grants.update --data @payload.json --json
topolo actions plan app_topolo_auth.resource_grants.update --data @payload.json --confirm --json
topolo actions call app_topolo_auth.resource_grants.update --data @payload.json --confirm --json
MCP call after validation and plan review:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.resource_grants.update",
"input": {
"grantId": "rg_ZgLq3BZDdHf-",
"expiresAt": 1791050500
},
"confirm": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "Resource grant updated",
"data": {
"grant": {
"id": "rg_ZgLq3BZDdHf-",
"userId": "usr_recipient",
"organizationId": "org_example",
"appId": "app_topolo_people",
"resourceType": "candidate",
"resourceId": "candidate_expired",
"grantType": "consumer",
"scope": "read",
"expiresAt": 1791050500,
"grantedBy": "usr_manager",
"metadata": null,
"revokedAt": null,
"createdAt": 1790964100
}
},
"timestamp": "2026-10-02T18:01:40.149Z"
}
Verify the result
-
Read list_resource for the exact organization/application/type/id and compare recipient, scope and expiry against the intended change; follow all pages.
-
Use the intended recipient principal with list_mine, then verify the owning application accepts only the intended resource/scope and denies an unrelated resource. Metadata read-back alone does not prove resource enforcement.
Errors and recovery
Revoked grant cannot be updated
Captured local refusal · HTTP 404:
{
"input": {
"grantId": "rg_JOD6QZYnE1JA",
"scope": "read"
},
"response": {
"success": false,
"error": {
"message": "Resource grant not found",
"code": "NOT_FOUND",
"details": null,
"timestamp": "2026-10-02T18:01:40.148Z"
}
}
}
Authenticated recipient cannot change own grant
Captured local refusal · HTTP 401:
{
"input": {
"grantId": "rg_JOD6QZYnE1JA",
"scope": "read"
},
"response": {
"success": false,
"error": {
"message": "Not authorized to manage this resource grant",
"code": "AUTH_ERROR",
"details": null,
"timestamp": "2026-10-02T18:01:40.150Z"
}
}
}
Missing grant is refused
Captured local refusal · HTTP 404:
{
"input": {
"grantId": "rg_missing",
"scope": "read"
},
"response": {
"success": false,
"error": {
"message": "Resource grant not found",
"code": "NOT_FOUND",
"details": null,
"timestamp": "2026-10-02T18:01:40.151Z"
}
}
}
Published recovery guidance
-
state_conflict(HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid. -
VALIDATION_ERROR(HTTP 400): Correct reported fields. Pair resource filters, preserve pagination cursors and use workspace access actions for workspace grants. -
AUTH_ERROR(HTTP 401): Inspect identity and error message: invalid token and insufficient grant authority both use 401. Refresh an expired token when necessary; obtain intended access through its authorized owner for permission denials. -
NOT_FOUND(HTTP 404): Rediscover the exact live resource grant. Revoked rows cannot be updated; creation reactivation is a separate reviewed access grant. -
SERVER_ERROR(HTTP 500): Inspect actual stored state before retrying. Create can fail after identity provisioning; promote can fail after membership creation. Correct unsupported SQL-constrained scope/type input; do not treat a failure as proof that nothing changed.
Recovery boundaries
Review restoration of the captured scope and expiry only after reading current state. This restores metadata, not proof of downstream application enforcement.
Discovery and evidence
This record belongs to app_topolo_auth. Local tests prove real resource-grant SQL and handler responses with simulated identity, permissions, identity lookup/provisioning and stored-role synchronization. They do not prove recipient sign-in, invitation delivery, owning-application resource enforcement, live permissions, deployment or production completion.