Docs/Advanced Features/Access Tokens

    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:

    FormatDuration
    "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:

    1. Expiration: expiresAt is compared to the current time. Expired tokens are rejected.
    2. Signature: HMAC-SHA256 signature is verified against the bucket's signing key. Tampered tokens are rejected.
    3. 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
    
    StageMethod
    Createworm.createAccessToken({ subject, permissions, expiresIn })
    SerializeTokenManager.toBase64(token)
    DeserializeTokenManager.fromBase64(encoded)
    Verifyworm.verifyAccessToken(token)
    Revokeworm.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