Framework-agnostic session management for AT Protocol applications using Iron Session encryption
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove unused mobile Bearer token support (v2.0.0)

Breaking change: Removes mobile-specific methods that were unused by the
iOS app (which uses cookie-based auth instead):
- sealToken()
- unsealToken()
- validateBearerToken()
- refreshBearerToken()

Also removes MobileTokenData type and INVALID_TOKEN error type.

+29 -219
+18
CHANGELOG.md
··· 2 2 3 3 All notable changes to this project will be documented in this file. 4 4 5 + ## [2.0.0] - 2025-11-29 6 + 7 + ### Breaking Changes 8 + 9 + - **Removed mobile Bearer token support**: The following methods have been 10 + removed as they were unused (the Anchor iOS app uses cookie-based auth): 11 + - `sealToken()` - Created sealed tokens for mobile Bearer auth 12 + - `unsealToken()` - Decoded sealed tokens 13 + - `validateBearerToken()` - Validated Authorization headers 14 + - `refreshBearerToken()` - Refreshed Bearer tokens 15 + - **Removed `MobileTokenData` type**: No longer exported 16 + - **Removed `INVALID_TOKEN` from `SessionErrorType`**: Only cookie-related 17 + errors remain 18 + 19 + The library now focuses solely on cookie-based session management for web 20 + applications. Mobile apps should use app-specific endpoints for their OAuth 21 + flow. 22 + 5 23 ## [1.0.1] - 2025-11-28 6 24 7 25 ### Fixed
+1 -1
deno.json
··· 1 1 { 2 2 "$schema": "https://jsr.io/schema/config-file.v1.json", 3 3 "name": "@tijs/atproto-sessions", 4 - "version": "1.0.1", 4 + "version": "2.0.0", 5 5 "license": "MIT", 6 6 "exports": "./mod.ts", 7 7 "publish": {
+3 -8
mod.ts
··· 3 3 * 4 4 * Framework-agnostic session management for AT Protocol applications. 5 5 * 6 - * Provides encrypted session cookies and mobile Bearer tokens using Iron Session. 6 + * Provides encrypted session cookies using Iron Session. 7 7 * Works with standard Web Request/Response APIs - no framework dependencies. 8 8 * 9 9 * @example ··· 31 31 32 32 // Types 33 33 export type { 34 + CookieSessionData, 34 35 Logger, 35 - MobileTokenData, 36 36 SessionConfig, 37 37 SessionData, 38 38 SessionErrorInfo, ··· 41 41 } from "./src/types.ts"; 42 42 43 43 // Errors 44 - export { 45 - ConfigurationError, 46 - CookieError, 47 - SessionError, 48 - TokenError, 49 - } from "./src/errors.ts"; 44 + export { ConfigurationError, CookieError, SessionError } from "./src/errors.ts";
-10
src/errors.ts
··· 30 30 this.name = "CookieError"; 31 31 } 32 32 } 33 - 34 - /** 35 - * Error thrown when mobile token operations fail 36 - */ 37 - export class TokenError extends SessionError { 38 - constructor(message: string) { 39 - super(message, "TOKEN_ERROR"); 40 - this.name = "TokenError"; 41 - } 42 - }
+5 -91
src/sessions.test.ts
··· 1 1 import { assertEquals, assertExists } from "@std/assert"; 2 2 import { SessionManager } from "./sessions.ts"; 3 3 import { ConfigurationError } from "./errors.ts"; 4 - import type { SessionData } from "./types.ts"; 4 + import type { CookieSessionData } from "./types.ts"; 5 5 6 6 const TEST_SECRET = "test-secret-that-is-at-least-32-characters-long"; 7 7 const TEST_DID = "did:plc:test123"; ··· 53 53 const manager = new SessionManager({ cookieSecret: TEST_SECRET }); 54 54 55 55 await t.step("creates valid Set-Cookie header", async () => { 56 - const sessionData: SessionData = { 56 + const sessionData: CookieSessionData = { 57 57 did: TEST_DID, 58 58 createdAt: Date.now(), 59 59 lastAccessed: Date.now(), ··· 75 75 cookieName: "custom_session", 76 76 }); 77 77 78 - const sessionData: SessionData = { 78 + const sessionData: CookieSessionData = { 79 79 did: TEST_DID, 80 80 createdAt: Date.now(), 81 81 lastAccessed: Date.now(), ··· 153 153 154 154 await t.step("successfully extracts valid session", async () => { 155 155 // First create a valid session cookie 156 - const sessionData: SessionData = { 156 + const sessionData: CookieSessionData = { 157 157 did: TEST_DID, 158 158 createdAt: Date.now() - 1000, 159 159 lastAccessed: Date.now() - 1000, ··· 182 182 }); 183 183 184 184 await t.step("handles multiple cookies correctly", async () => { 185 - const sessionData: SessionData = { 185 + const sessionData: CookieSessionData = { 186 186 did: TEST_DID, 187 187 createdAt: Date.now(), 188 188 lastAccessed: Date.now(), ··· 202 202 assertEquals(result.data.did, TEST_DID); 203 203 }); 204 204 }); 205 - 206 - Deno.test("SessionManager - mobile token operations", async (t) => { 207 - const manager = new SessionManager({ cookieSecret: TEST_SECRET }); 208 - 209 - await t.step("sealToken creates valid token", async () => { 210 - const token = await manager.sealToken({ did: TEST_DID }); 211 - assertEquals(typeof token, "string"); 212 - assertEquals(token.length > 0, true); 213 - }); 214 - 215 - await t.step("unsealToken recovers data", async () => { 216 - const token = await manager.sealToken({ did: TEST_DID }); 217 - const data = await manager.unsealToken(token); 218 - 219 - assertExists(data); 220 - assertEquals(data.did, TEST_DID); 221 - }); 222 - 223 - await t.step("unsealToken returns null for invalid token", async () => { 224 - const data = await manager.unsealToken("invalid_token"); 225 - assertEquals(data, null); 226 - }); 227 - 228 - await t.step("unsealToken returns null for wrong secret", async () => { 229 - const otherManager = new SessionManager({ 230 - cookieSecret: "different-secret-that-is-at-least-32-chars", 231 - }); 232 - 233 - const token = await manager.sealToken({ did: TEST_DID }); 234 - const data = await otherManager.unsealToken(token); 235 - 236 - assertEquals(data, null); 237 - }); 238 - }); 239 - 240 - Deno.test("SessionManager - validateBearerToken", async (t) => { 241 - const manager = new SessionManager({ cookieSecret: TEST_SECRET }); 242 - 243 - await t.step("returns error for invalid header format", async () => { 244 - const result = await manager.validateBearerToken("Basic xxx"); 245 - 246 - assertEquals(result.data, null); 247 - assertEquals(result.error?.type, "INVALID_TOKEN"); 248 - assertEquals(result.error?.message, "Invalid authorization header format"); 249 - }); 250 - 251 - await t.step("returns error for invalid token", async () => { 252 - const result = await manager.validateBearerToken("Bearer invalid_token"); 253 - 254 - assertEquals(result.data, null); 255 - assertEquals(result.error?.type, "INVALID_TOKEN"); 256 - }); 257 - 258 - await t.step("successfully validates valid token", async () => { 259 - const token = await manager.sealToken({ did: TEST_DID }); 260 - const result = await manager.validateBearerToken(`Bearer ${token}`); 261 - 262 - assertExists(result.data); 263 - assertEquals(result.data.did, TEST_DID); 264 - assertEquals(result.error, undefined); 265 - }); 266 - }); 267 - 268 - Deno.test("SessionManager - refreshBearerToken", async (t) => { 269 - const manager = new SessionManager({ cookieSecret: TEST_SECRET }); 270 - 271 - await t.step("returns null for invalid token", async () => { 272 - const result = await manager.refreshBearerToken("Bearer invalid"); 273 - assertEquals(result, null); 274 - }); 275 - 276 - await t.step("returns new token for valid token", async () => { 277 - const originalToken = await manager.sealToken({ did: TEST_DID }); 278 - const newToken = await manager.refreshBearerToken( 279 - `Bearer ${originalToken}`, 280 - ); 281 - 282 - assertExists(newToken); 283 - assertEquals(typeof newToken, "string"); 284 - 285 - // New token should be valid and contain same DID 286 - const data = await manager.unsealToken(newToken); 287 - assertExists(data); 288 - assertEquals(data.did, TEST_DID); 289 - }); 290 - });
+2 -100
src/sessions.ts
··· 3 3 import type { 4 4 CookieSessionData, 5 5 Logger, 6 - MobileTokenData, 7 6 SessionConfig, 8 7 SessionResult, 9 8 } from "./types.ts"; ··· 29 28 /** 30 29 * Framework-agnostic session manager using Iron Session. 31 30 * 32 - * Handles encrypted session cookies and mobile Bearer tokens 33 - * for AT Protocol applications. Works with standard Web Request/Response 34 - * APIs - no framework dependencies. 31 + * Handles encrypted session cookies for AT Protocol applications. 32 + * Works with standard Web Request/Response APIs - no framework dependencies. 35 33 * 36 34 * @example 37 35 * ```typescript ··· 222 220 */ 223 221 getClearCookieHeader(): string { 224 222 return `${this.cookieName}=; Path=/; HttpOnly; SameSite=Lax; Secure; Max-Age=0`; 225 - } 226 - 227 - /** 228 - * Seal data into a mobile Bearer token. 229 - * 230 - * Creates a token that is compatible with cookie-based session validation, 231 - * so mobile apps can use this token either as a Bearer token or as a cookie value. 232 - * 233 - * @param data - Data to seal (typically just { did }) 234 - * @returns Sealed token string 235 - */ 236 - async sealToken(data: MobileTokenData): Promise<string> { 237 - // Include createdAt and lastAccessed for cookie compatibility 238 - // This allows mobile tokens to work as cookie values 239 - const now = Date.now(); 240 - const sessionData: CookieSessionData = { 241 - did: data.did, 242 - createdAt: now, 243 - lastAccessed: now, 244 - }; 245 - return await sealData(sessionData, { 246 - password: this.cookieSecret, 247 - ttl: this.sessionTtl, 248 - }); 249 - } 250 - 251 - /** 252 - * Unseal a mobile Bearer token. 253 - * 254 - * @param token - Sealed token string 255 - * @returns Unsealed data, or null if invalid 256 - */ 257 - async unsealToken(token: string): Promise<MobileTokenData | null> { 258 - try { 259 - const data = await unsealData(token, { 260 - password: this.cookieSecret, 261 - }) as MobileTokenData; 262 - 263 - if (!data?.did) { 264 - return null; 265 - } 266 - 267 - return data; 268 - } catch { 269 - return null; 270 - } 271 - } 272 - 273 - /** 274 - * Validate a Bearer token from Authorization header. 275 - * 276 - * @param authHeader - Authorization header value (e.g., "Bearer xxx") 277 - * @returns Session result with token data or error 278 - */ 279 - async validateBearerToken( 280 - authHeader: string, 281 - ): Promise<SessionResult<MobileTokenData>> { 282 - if (!authHeader.startsWith("Bearer ")) { 283 - return { 284 - data: null, 285 - error: { 286 - type: "INVALID_TOKEN", 287 - message: "Invalid authorization header format", 288 - }, 289 - }; 290 - } 291 - 292 - const token = authHeader.slice(7); 293 - const data = await this.unsealToken(token); 294 - 295 - if (!data) { 296 - return { 297 - data: null, 298 - error: { 299 - type: "INVALID_TOKEN", 300 - message: "Invalid or expired token", 301 - }, 302 - }; 303 - } 304 - 305 - return { data }; 306 - } 307 - 308 - /** 309 - * Refresh a mobile Bearer token with new seal. 310 - * 311 - * @param authHeader - Authorization header with current Bearer token 312 - * @returns New sealed token, or null if invalid 313 - */ 314 - async refreshBearerToken(authHeader: string): Promise<string | null> { 315 - const result = await this.validateBearerToken(authHeader); 316 - if (!result.data) { 317 - return null; 318 - } 319 - 320 - return await this.sealToken(result.data); 321 223 } 322 224 }
-9
src/types.ts
··· 69 69 | "NO_COOKIE" 70 70 | "INVALID_COOKIE" 71 71 | "SESSION_EXPIRED" 72 - | "INVALID_TOKEN" 73 72 | "UNKNOWN"; 74 73 75 74 /** ··· 100 99 /** Error information if session retrieval failed */ 101 100 error?: SessionErrorInfo; 102 101 } 103 - 104 - /** 105 - * Mobile token data - minimal payload sealed in Bearer tokens 106 - */ 107 - export interface MobileTokenData { 108 - /** User's DID */ 109 - did: string; 110 - }