Scoped Access Tokens
Scoped access tokens are self-contained permission grants with an expiration. They enable sharing limited access to specific models and operations without exposing bucket credentials.
Token Structure
A token encodes the issuer, subject, permissions, timestamps, and a cryptographic signature:
interface AccessTokenData {
/** Unique token identifier (e.g. "tok_abc123...") */
id: string;
/** Who issued this token (principal canonical string) */
issuer: string;
/** Who this token is for (principal ID or "*" for bearer) */
subject: string;
/** What this token allows */
permissions: TokenPermission[];
/** ISO 8601 creation timestamp */
issuedAt: string;
/** ISO 8601 expiration timestamp */
expiresAt: string;
/** HMAC-SHA256 signature over the payload */
signature: string;
}
Token Permissions
Each permission entry scopes access to a model, a set of operations, and optionally specific entity IDs or a filter:
interface TokenPermission {
/** Which model this permission applies to */
model: string;
/** Allowed operations */
operations: ("read" | "write" | "delete" | "list")[];
/** Restrict to specific entity IDs */
ids?: string[];
/** Restrict to entities matching a filter */
filter?: Record<string, unknown>;
}
Creating Tokens
Use worm.createAccessToken() to issue a new token. The token is signed with the bucket's signing key (HMAC-SHA256):
import { S3Worm } from "@decoperations/s3worm";
const worm = new S3Worm({
bucket: "my-bucket",
signingKey: "my-secret-signing-key",
// ...credentials
});
const token = await worm.createAccessToken({
subject: "guest-user",
permissions: [
{
model: "Customer",
operations: ["read"],
filter: { status: "active" },
},
{
model: "Invoice",
operations: ["read"],
ids: ["inv-123", "inv-456"],
},
],
expiresIn: "24h",
});
console.log(token.id); // "tok_abc123..."
console.log(token.issuer); // "user:0x1234..." (resolved from keychain)
console.log(token.expiresAt); // "2026-02-25T12:00:00.000Z"
Expiration Formats
The expiresIn field accepts duration strings:
| Format | Duration |
|---|---|
"30m" | 30 minutes |
"24h" | 24 hours |
"7d" | 7 days |
"4w" | 4 weeks |
You can also pass an explicit ISO 8601 timestamp via expiresAt:
const token = await worm.createAccessToken({
subject: "*",
permissions: [{ model: "Post", operations: ["read", "list"] }],
expiresAt: "2026-12-31T23:59:59Z",
});
If neither is provided, the default expiration is 24 hours.
Serializing and Sharing Tokens
Tokens serialize to a URL-safe base64 string for sharing:
import { TokenManager } from "@decoperations/s3worm";
// Serialize
const encoded = TokenManager.toBase64(token);
// -> "eyJpZCI6InRva18..." (URL-safe base64)
// Deserialize
const restored = TokenManager.fromBase64(encoded);
console.log(restored.id); // "tok_abc123..."
console.log(restored.permissions); // [{...}]
Using Tokens
Pass a token to the S3Worm constructor to authenticate as the token holder:
import { S3Worm, TokenManager } from "@decoperations/s3worm";
// Recipient receives the base64-encoded token
const tokenData = TokenManager.fromBase64(encodedToken);
const worm = new S3Worm({
bucket: "my-bucket",
token: tokenData,
// ...credentials
});
// The token holder can now perform only the allowed operations
const customers = worm.model("Customer");
const active = await customers.findAll({ filter: { status: "active" } }); // Allowed
await customers.save({ name: "New" }); // DENIED -- token only grants read
Token-based requests are evaluated in the ACL pipeline at the token layer. The token's permissions must include the requested model and operation. If the token also restricts by ids, the entity ID must be in the allowed set.
Token Verification
Verification checks three things in order:
- Expiration:
expiresAtis compared to the current time. Expired tokens are rejected. - Signature: HMAC-SHA256 signature is verified against the bucket's signing key. Tampered tokens are rejected.
- Revocation: The revocation list (
.worm/.revocations/{tokenId}.json) is checked. Revoked tokens are rejected.
const result = await worm.verifyAccessToken(tokenData);
if (result.valid) {
console.log("Token is valid");
} else {
console.log("Token rejected:", result.reason);
// "token expired" | "invalid signature" | "token revoked"
}
Revoking Tokens
Revoke a token by its ID. This writes a revocation record to the bucket at .worm/.revocations/:
await worm.revokeAccessToken("tok_abc123...");
// Writes .worm/.revocations/tok_abc123....json
Revocation records are stored in the bucket and sync naturally via bucket-sync. There is a small window between revocation and sync where the token may still be valid on other clients.
Token Lifecycle Summary
Create Serialize Share
| | |
v v v
worm.createAccessToken() -> TokenManager.toBase64() -> send to recipient
|
v
TokenManager.fromBase64()
|
v
new S3Worm({ token: ... })
|
v
Operations checked against
token permissions + expiry
| Stage | Method |
|---|---|
| Create | worm.createAccessToken({ subject, permissions, expiresIn }) |
| Serialize | TokenManager.toBase64(token) |
| Deserialize | TokenManager.fromBase64(encoded) |
| Verify | worm.verifyAccessToken(token) |
| Revoke | worm.revokeAccessToken(tokenId) |
Pre-Signed URLs
For sharing access to individual objects without exposing credentials or tokens:
// Generate a pre-signed read URL
const url = await worm.presignedUrl("Customer", "abc-123", {
operation: "read",
expiresIn: "1h",
});
// -> "https://s3.../org/customers/abc-123/profile.json?X-Amz-..."
// Generate a pre-signed upload URL
const uploadUrl = await worm.presignedUrl("Customer", "new-id", {
operation: "write",
expiresIn: "15m",
contentType: "application/json",
});
Pre-signed URLs are time-limited and scoped to a single object. They bypass SDK-side ACL enforcement (the URL holder gets direct S3 access), so generate them only for authorized principals with short expiration times.
CLI Commands
worm auth token create Create a scoped access token
--subject <principal> Who the token is for
--model <name> Model to grant access to (repeatable)
--operations <ops> Comma-separated: read,write,delete,list
--ids <id,...> Restrict to specific entity IDs
--expires <duration> Expiration (e.g. "24h", "7d")
worm auth token list List all stored tokens
worm auth token revoke <token-id> Revoke a previously issued token
worm auth token inspect <base64> Decode and display a token's claims
without verifying the signature
Examples
# Create a read-only token for the Customer model, valid for 7 days
worm auth token create \
--subject guest-user \
--model Customer \
--operations read,list \
--expires 7d
# Inspect a token without verification
worm auth token inspect eyJpZCI6InRva18...
# Revoke a token
worm auth token revoke tok_abc123