Token Permissions
Token permissions enable cross-app storage access. When App A needs to read or write data in App B's storage, App B uses token_permissions.json to grant access based on tokens App A has.
The Problem
By default, each app's storage is isolated. An app can only access its own storage paths (defined in storage.json).
But sometimes apps need to share data:
- An interview app needs to read project configurations from a research app
- A dashboard app needs to read data from multiple source apps
- An admin tool needs to clean up data across apps
Token permissions solve this without compromising security.
How It Works
Permission checking happens in phases:
- Public paths - Anyone can read from
/public/*(no permission needed) - Same-app access - An app can always access its own storage
- Cross-app access - Check if the requesting app has tokens that grant access
For phase 3, the target app (the one being accessed) must define permissions in token_permissions.json.
Creating Token Permissions
Place token_permissions.json in your app's root directory:
{
"@malv/interview": {
"interview_access": [
{
"type": "storage",
"access": "read",
"prefix": "/teams/<token.teamId>/projects/<token.projectId>/config.json",
"description": "Read project configuration for interviews"
}
]
}
}
This grants read access to @malv/interview when it has an interview_access token, but only to paths matching the prefix.
Structure
{
"@requesting-app-id": {
"token_type": [
{
"type": "storage",
"access": "read" | "write" | "delete",
"prefix": "/path/with/<token.field>/templates/",
"description": "Why this access is needed"
}
]
}
}
| Field | Description |
|---|---|
| App ID key | The app that will request access |
| Token type key | Token type from that app's tokens.json |
type |
Always "storage" |
access |
"read", "write", or "delete" |
prefix |
Path prefix (supports <token.field> templates) |
description |
Human-readable explanation |
Path Templates
Use <token.fieldName> to substitute values from the token's payload:
{
"prefix": "/teams/<token.teamId>/projects/<token.projectId>/"
}
If the token has { "teamId": "team-123", "projectId": "proj-456" }, the prefix becomes:
/teams/team-123/projects/proj-456/
This ensures access is scoped to the specific team and project in the token.
Complete Example
Scenario
@malv/interview needs to read project configs from @malv/research storage.
Step 1: Define the token in @malv/interview
In @malv/interview/tokens.json:
{
"interview_access": {
"description": "Token for accessing interview configurations",
"schema": {
"type": "object",
"properties": {
"projectId": { "type": "string" },
"teamId": { "type": "string" }
},
"required": ["projectId", "teamId"]
}
}
}
Step 2: Grant access in @malv/research
In @malv/research/token_permissions.json:
{
"@malv/interview": {
"interview_access": [
{
"type": "storage",
"access": "read",
"prefix": "/teams/<token.teamId>/projects/<token.projectId>/config.json",
"description": "Read project config for interviews"
},
{
"type": "storage",
"access": "read",
"prefix": "/teams/<token.teamId>/projects/<token.projectId>/versions/",
"description": "Read published config versions"
}
]
}
}
Step 3: Use in the tool
In @malv/interview/src/tools/start_session.ts:
export default async function start_session(
input: Input,
capabilities: Capabilities
): Promise<Output> {
// Access @malv/research storage
// This works because we have interview_access token
// and @malv/research grants permission in token_permissions.json
const researchStorage = capabilities.storage.use('@malv/research');
const config = await researchStorage.get(
`/teams/${input.teamId}/projects/${input.projectId}/config.json`
);
return { config: JSON.parse(config.asString()) };
}
Multiple Token Types
Grant different access levels to different token types:
{
"@malv/interview": {
"interview_access": [
{
"type": "storage",
"access": "read",
"prefix": "/teams/<token.teamId>/projects/<token.projectId>/",
"description": "Read-only access for running interviews"
}
],
"admin_access": [
{
"type": "storage",
"access": "read",
"prefix": "/teams/<token.teamId>/",
"description": "Read all team data"
},
{
"type": "storage",
"access": "delete",
"prefix": "/teams/<token.teamId>/",
"description": "Delete team data for cleanup"
}
]
}
}
Multiple Apps
Grant access to multiple apps:
{
"@malv/interview": {
"interview_access": [
{ "type": "storage", "access": "read", "prefix": "/teams/<token.teamId>/" }
]
},
"@malv/analytics": {
"analytics_access": [
{ "type": "storage", "access": "read", "prefix": "/teams/<token.teamId>/metrics/" }
]
}
}
Permission Flow
┌─────────────────────────────────────────────────────────┐
│ @malv/interview calls: │
│ storage.use("@malv/research").get("/teams/t1/...") │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Storage Service checks: │
│ │
│ 1. Is this public storage? NO │
│ 2. Is @malv/interview accessing its own storage? NO │
│ 3. Check @malv/research/token_permissions.json: │
│ - Rules for @malv/interview? YES │
│ - Rules for interview_access token? YES │
│ - Path matches prefix after substitution? YES │
│ │
│ Result: ACCESS GRANTED │
└─────────────────────────────────────────────────────────┘
Debugging
If access is denied, check:
- Token exists - Does the tool have the required token in
input_tokens? - File exists - Does
token_permissions.jsonexist in the target app? - App ID matches - Is the app ID exactly right (e.g.,
"@malv/interview")? - Token type matches - Does it match a token in the requesting app's
tokens.json? - Path matches - Does the requested path start with the prefix after template substitution?
- Access type matches - Is it
"read"for GET,"write"for PUT,"delete"for DELETE?
Difference from storage.json
| File | Purpose |
|---|---|
storage.json |
What storage paths your app can access |
token_permissions.json |
What other apps can access in your storage |
Think of it this way:
storage.json= "What I can do"token_permissions.json= "What others can do with my data"
Best Practices
Use specific paths - Avoid broad prefixes like /. Be as specific as possible:
// Good - specific path
"prefix": "/teams/<token.teamId>/projects/<token.projectId>/config.json"
// Risky - too broad
"prefix": "/teams/<token.teamId>/"
Document with descriptions - Explain why access is needed:
{
"description": "Read project config to display interview questions"
}
Match token lifecycles - Short-lived tokens should have narrow access. Long-lived admin tokens might have broader access.
Rebuild after changes - After editing token_permissions.json, rebuild and redeploy your app so the changes take effect.