diff --git a/engine/packages/guard/src/routing/mod.rs b/engine/packages/guard/src/routing/mod.rs index be7bf98582..3424196e0b 100644 --- a/engine/packages/guard/src/routing/mod.rs +++ b/engine/packages/guard/src/routing/mod.rs @@ -142,8 +142,8 @@ pub fn create_routing_function(ctx: StandaloneCtx, shared_state: SharedState) -> /// Parse actor routing information from path /// Matches patterns: -/// - /gateway/actors/{actor_id}/tokens/{token}/route/{...path} -/// - /gateway/actors/{actor_id}/route/{...path} +/// - /gateway/{actor_id}/{...path} +/// - /gateway/{actor_id}@{token}/{...path} pub fn parse_actor_path(path: &str) -> Option { // Find query string position (everything from ? onwards, but before fragment) let query_pos = path.find('?'); @@ -173,47 +173,41 @@ pub fn parse_actor_path(path: &str) -> Option { // Split the path into segments let segments: Vec<&str> = base_path.split('/').filter(|s| !s.is_empty()).collect(); - // Check minimum required segments: gateway, actors, {actor_id}, route - if segments.len() < 4 { + // Check minimum required segments: gateway, {actor_id} + if segments.len() < 2 { return None; } - // Verify the fixed segments - if segments[0] != "gateway" || segments[1] != "actors" { + // Verify the fixed segment + if segments[0] != "gateway" { return None; } - // Check for empty actor_id - if segments[2].is_empty() { + // Check for empty actor_id segment + if segments[1].is_empty() { return None; } - let actor_id = segments[2].to_string(); - - // Check for token or direct route - let (token, remaining_path_start_idx) = - if segments.len() >= 6 && segments[3] == "tokens" && segments[5] == "route" { - // Pattern with token: /gateway/actors/{actor_id}/tokens/{token}/route/{...path} - // Check for empty token - if segments[4].is_empty() { - return None; - } - (Some(segments[4].to_string()), 6) - } else if segments.len() >= 4 && segments[3] == "route" { - // Pattern without token: /gateway/actors/{actor_id}/route/{...path} - (None, 4) - } else { + // Parse actor_id and optional token from second segment + // Pattern: {actor_id}@{token} or just {actor_id} + let actor_id_segment = segments[1]; + let (actor_id, token) = if let Some(at_pos) = actor_id_segment.find('@') { + let aid = &actor_id_segment[..at_pos]; + let tok = &actor_id_segment[at_pos + 1..]; + + // Check for empty actor_id or token + if aid.is_empty() || tok.is_empty() { return None; - }; + } + + (aid.to_string(), Some(tok.to_string())) + } else { + (actor_id_segment.to_string(), None) + }; // Calculate the position in the original path where remaining path starts - let mut prefix_len = 0; - for (i, segment) in segments.iter().enumerate() { - if i >= remaining_path_start_idx { - break; - } - prefix_len += 1 + segment.len(); // +1 for the slash - } + // We need to skip "/gateway/{actor_id_segment}" + let prefix_len = 1 + segments[0].len() + 1 + segments[1].len(); // "/gateway/{actor_id_segment}" // Extract the remaining path preserving trailing slashes let remaining_base = if prefix_len < base_path.len() { diff --git a/engine/packages/guard/tests/parse_actor_path.rs b/engine/packages/guard/tests/parse_actor_path.rs index dee990358b..802f502a8d 100644 --- a/engine/packages/guard/tests/parse_actor_path.rs +++ b/engine/packages/guard/tests/parse_actor_path.rs @@ -2,8 +2,8 @@ use rivet_guard::routing::parse_actor_path; #[test] fn test_parse_actor_path_with_token() { - // Basic path with token and route - let path = "/gateway/actors/actor-123/tokens/my-token/route/api/v1/endpoint"; + // Basic path with token using @ syntax + let path = "/gateway/actor-123@my-token/api/v1/endpoint"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-123"); assert_eq!(result.token, Some("my-token".to_string())); @@ -13,7 +13,7 @@ fn test_parse_actor_path_with_token() { #[test] fn test_parse_actor_path_without_token() { // Path without token - let path = "/gateway/actors/actor-123/route/api/v1/endpoint"; + let path = "/gateway/actor-123/api/v1/endpoint"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-123"); assert_eq!(result.token, None); @@ -23,7 +23,7 @@ fn test_parse_actor_path_without_token() { #[test] fn test_parse_actor_path_with_uuid() { // Path with UUID as actor ID - let path = "/gateway/actors/12345678-1234-1234-1234-123456789abc/route/status"; + let path = "/gateway/12345678-1234-1234-1234-123456789abc/status"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "12345678-1234-1234-1234-123456789abc"); assert_eq!(result.token, None); @@ -33,14 +33,14 @@ fn test_parse_actor_path_with_uuid() { #[test] fn test_parse_actor_path_with_query_params() { // Path with query parameters - let path = "/gateway/actors/actor-456/route/api/endpoint?foo=bar&baz=qux"; + let path = "/gateway/actor-456/api/endpoint?foo=bar&baz=qux"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-456"); assert_eq!(result.token, None); assert_eq!(result.remaining_path, "/api/endpoint?foo=bar&baz=qux"); // Path with token and query parameters - let path = "/gateway/actors/actor-456/tokens/token123/route/api?key=value"; + let path = "/gateway/actor-456@token123/api?key=value"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-456"); assert_eq!(result.token, Some("token123".to_string())); @@ -50,7 +50,7 @@ fn test_parse_actor_path_with_query_params() { #[test] fn test_parse_actor_path_with_fragment() { // Path with fragment - let path = "/gateway/actors/actor-789/route/page#section"; + let path = "/gateway/actor-789/page#section"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-789"); assert_eq!(result.token, None); @@ -60,15 +60,15 @@ fn test_parse_actor_path_with_fragment() { #[test] fn test_parse_actor_path_empty_remaining() { - // Path with no remaining path after route - let path = "/gateway/actors/actor-000/route"; + // Path with no remaining path + let path = "/gateway/actor-000"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-000"); assert_eq!(result.token, None); assert_eq!(result.remaining_path, "/"); // With token and no remaining path - let path = "/gateway/actors/actor-000/tokens/tok/route"; + let path = "/gateway/actor-000@tok"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-000"); assert_eq!(result.token, Some("tok".to_string())); @@ -78,7 +78,7 @@ fn test_parse_actor_path_empty_remaining() { #[test] fn test_parse_actor_path_with_trailing_slash() { // Path with trailing slash - let path = "/gateway/actors/actor-111/route/api/"; + let path = "/gateway/actor-111/api/"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-111"); assert_eq!(result.token, None); @@ -88,8 +88,7 @@ fn test_parse_actor_path_with_trailing_slash() { #[test] fn test_parse_actor_path_complex_remaining() { // Complex remaining path with multiple segments - let path = - "/gateway/actors/actor-complex/tokens/secure-token/route/api/v2/users/123/profile/settings"; + let path = "/gateway/actor-complex@secure-token/api/v2/users/123/profile/settings"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-complex"); assert_eq!(result.token, Some("secure-token".to_string())); @@ -99,7 +98,7 @@ fn test_parse_actor_path_complex_remaining() { #[test] fn test_parse_actor_path_special_characters() { // Actor ID with allowed special characters - let path = "/gateway/actors/actor_id-123.test/route/endpoint"; + let path = "/gateway/actor_id-123.test/endpoint"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor_id-123.test"); assert_eq!(result.token, None); @@ -109,7 +108,7 @@ fn test_parse_actor_path_special_characters() { #[test] fn test_parse_actor_path_encoded_characters() { // URL encoded characters in path - let path = "/gateway/actors/actor-123/route/api%20endpoint/test%2Fpath"; + let path = "/gateway/actor-123/api%20endpoint/test%2Fpath"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-123"); assert_eq!(result.token, None); @@ -121,53 +120,34 @@ fn test_parse_actor_path_encoded_characters() { #[test] fn test_parse_actor_path_invalid_prefix() { // Wrong prefix - assert!(parse_actor_path("/api/actors/123/route/endpoint").is_none()); - assert!(parse_actor_path("/gateway/actor/123/route/endpoint").is_none()); - assert!(parse_actor_path("/actors/123/route/endpoint").is_none()); -} - -#[test] -fn test_parse_actor_path_missing_route() { - // Missing route keyword - assert!(parse_actor_path("/gateway/actors/123").is_none()); - assert!(parse_actor_path("/gateway/actors/123/endpoint").is_none()); - assert!(parse_actor_path("/gateway/actors/123/tokens/tok").is_none()); + assert!(parse_actor_path("/api/123/endpoint").is_none()); + assert!(parse_actor_path("/actor/123/endpoint").is_none()); } #[test] fn test_parse_actor_path_too_short() { // Too few segments assert!(parse_actor_path("/gateway").is_none()); - assert!(parse_actor_path("/gateway/actors").is_none()); - assert!(parse_actor_path("/gateway/actors/123").is_none()); } #[test] -fn test_parse_actor_path_malformed_token_path() { - // Token path but missing route - assert!(parse_actor_path("/gateway/actors/123/tokens/tok/api").is_none()); - // Token without value - assert!(parse_actor_path("/gateway/actors/123/tokens//route/api").is_none()); -} - -#[test] -fn test_parse_actor_path_wrong_segment_positions() { - // Segments in wrong positions - assert!(parse_actor_path("/actors/gateway/123/route/endpoint").is_none()); - assert!(parse_actor_path("/gateway/route/actors/123/endpoint").is_none()); +fn test_parse_actor_path_malformed_token() { + // Token without actor_id (empty before @) + assert!(parse_actor_path("/gateway/@tok/api").is_none()); + // Empty token (nothing after @) + assert!(parse_actor_path("/gateway/actor-123@/api").is_none()); } #[test] fn test_parse_actor_path_empty_values() { // Empty actor_id - assert!(parse_actor_path("/gateway/actors//route/endpoint").is_none()); - assert!(parse_actor_path("/gateway/actors//tokens/tok/route/endpoint").is_none()); + assert!(parse_actor_path("/gateway//endpoint").is_none()); } #[test] fn test_parse_actor_path_double_slash() { // Double slashes in path - let path = "/gateway/actors//actor-123/route/endpoint"; + let path = "/gateway//actor-123/endpoint"; // This will fail because the double slash creates an empty segment assert!(parse_actor_path(path).is_none()); } @@ -175,16 +155,13 @@ fn test_parse_actor_path_double_slash() { #[test] fn test_parse_actor_path_case_sensitive() { // Keywords are case sensitive - assert!(parse_actor_path("/Gateway/actors/123/route/endpoint").is_none()); - assert!(parse_actor_path("/gateway/Actors/123/route/endpoint").is_none()); - assert!(parse_actor_path("/gateway/actors/123/Route/endpoint").is_none()); - assert!(parse_actor_path("/gateway/actors/123/tokens/tok/Route/endpoint").is_none()); + assert!(parse_actor_path("/Gateway/123/endpoint").is_none()); } #[test] fn test_parse_actor_path_query_and_fragment() { // Path with both query and fragment - let path = "/gateway/actors/actor-123/route/api?query=1#section"; + let path = "/gateway/actor-123/api?query=1#section"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-123"); assert_eq!(result.token, None); @@ -194,10 +171,30 @@ fn test_parse_actor_path_query_and_fragment() { #[test] fn test_parse_actor_path_only_query_string() { - // Path ending with route but having query string - let path = "/gateway/actors/actor-123/route?direct=true"; + // Path ending after actor_id but having query string + let path = "/gateway/actor-123?direct=true"; let result = parse_actor_path(path).unwrap(); assert_eq!(result.actor_id, "actor-123"); assert_eq!(result.token, None); assert_eq!(result.remaining_path, "/?direct=true"); } + +#[test] +fn test_parse_actor_path_token_with_special_chars() { + // Token containing special characters + let path = "/gateway/actor-123@token_with-chars.123/endpoint"; + let result = parse_actor_path(path).unwrap(); + assert_eq!(result.actor_id, "actor-123"); + assert_eq!(result.token, Some("token_with-chars.123".to_string())); + assert_eq!(result.remaining_path, "/endpoint"); +} + +#[test] +fn test_parse_actor_path_multiple_at_signs() { + // Multiple @ signs - only first one is used for token splitting + let path = "/gateway/actor-123@token@with@ats/endpoint"; + let result = parse_actor_path(path).unwrap(); + assert_eq!(result.actor_id, "actor-123"); + assert_eq!(result.token, Some("token@with@ats".to_string())); + assert_eq!(result.remaining_path, "/endpoint"); +} diff --git a/examples/cursors-raw-websocket/src/frontend/App.tsx b/examples/cursors-raw-websocket/src/frontend/App.tsx index 2c1c32f46f..d34ad3b956 100644 --- a/examples/cursors-raw-websocket/src/frontend/App.tsx +++ b/examples/cursors-raw-websocket/src/frontend/App.tsx @@ -90,7 +90,7 @@ export function App() { console.log("found actor", actorId); const wsOrigin = rivetUrl.replace(/^http/, "ws"); - const wsUrl = `${wsOrigin}/gateway/actors/${actorId}/route/raw/websocket?sessionId=${encodeURIComponent(sessionId)}`; + const wsUrl = `${wsOrigin}/gateway/${actorId}/raw/websocket?sessionId=${encodeURIComponent(sessionId)}`; console.log("ws url:", wsUrl); diff --git a/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts b/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts index 9da388b83a..4cc1999add 100644 --- a/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts +++ b/rivetkit-typescript/packages/rivetkit/src/manager/gateway.ts @@ -134,8 +134,8 @@ async function handleHttpGatewayPathBased( * Routes requests using either path-based routing or header-based routing: * * Path-based routing (checked first): - * - /gateway/actors/{actor_id}/tokens/{token}/route/{...path} - * - /gateway/actors/{actor_id}/route/{...path} + * - /gateway/{actor_id}/{...path} + * - /gateway/{actor_id}@{token}/{...path} * * Header-based routing (fallback): * - WebSocket requests: Uses sec-websocket-protocol for routing (target.actor, actor.{id}) @@ -340,8 +340,8 @@ async function handleHttpGateway( /** * Parse actor routing information from path * Matches patterns: - * - /gateway/actors/{actor_id}/tokens/{token}/route/{...path} - * - /gateway/actors/{actor_id}/route/{...path} + * - /gateway/{actor_id}/{...path} + * - /gateway/{actor_id}@{token}/{...path} */ export function parseActorPath(path: string): ActorPathInfo | null { // Find query string position (everything from ? onwards, but before fragment) @@ -374,50 +374,48 @@ export function parseActorPath(path: string): ActorPathInfo | null { // Split the path into segments const segments = basePath.split("/").filter((s) => s.length > 0); - // Check minimum required segments: gateway, actors, {actor_id}, route - if (segments.length < 4) { + // Check minimum required segments: gateway, {actor_id} + if (segments.length < 2) { return null; } - // Verify the fixed segments - if (segments[0] !== "gateway" || segments[1] !== "actors") { + // Verify the first segment is "gateway" + if (segments[0] !== "gateway") { return null; } - // Check for empty actor_id - if (segments[2].length === 0) { + // Extract actor_id segment (may contain @token) + const actorSegment = segments[1]; + + // Check for empty actor segment + if (actorSegment.length === 0) { return null; } - const actorId = segments[2]; - - // Check for token or direct route + // Parse actor_id and optional token from the segment + let actorId: string; let token: string | undefined; - let remainingPathStartIdx: number; - - if ( - segments.length >= 6 && - segments[3] === "tokens" && - segments[5] === "route" - ) { - // Pattern with token: /gateway/actors/{actor_id}/tokens/{token}/route/{...path} - // Check for empty token - if (segments[4].length === 0) { + + const atPos = actorSegment.indexOf("@"); + if (atPos !== -1) { + // Pattern: /gateway/{actor_id}@{token}/{...path} + actorId = actorSegment.slice(0, atPos); + token = actorSegment.slice(atPos + 1); + + // Check for empty actor_id or token + if (actorId.length === 0 || token.length === 0) { return null; } - token = segments[4]; - remainingPathStartIdx = 6; - } else if (segments.length >= 4 && segments[3] === "route") { - // Pattern without token: /gateway/actors/{actor_id}/route/{...path} - token = undefined; - remainingPathStartIdx = 4; } else { - return null; + // Pattern: /gateway/{actor_id}/{...path} + actorId = actorSegment; + token = undefined; } - // Calculate the position in the original path where remaining path starts + // Calculate remaining path + // The remaining path starts after /gateway/{actor_id[@token]}/ let prefixLen = 0; - for (let i = 0; i < remainingPathStartIdx; i++) { + for (let i = 0; i < 2; i++) { prefixLen += 1 + segments[i].length; // +1 for the slash } diff --git a/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-http-client.ts b/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-http-client.ts index a74d1300dd..7feb46e6eb 100644 --- a/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-http-client.ts +++ b/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-http-client.ts @@ -1,7 +1,6 @@ import type { ClientConfig } from "@/client/config"; import { HEADER_RIVET_ACTOR, - HEADER_RIVET_TARGET, HEADER_RIVET_TOKEN, } from "@/common/actor-router-consts"; import { combineUrlPath } from "@/utils"; @@ -15,7 +14,10 @@ export async function sendHttpRequestToActor( // Route through guard port const url = new URL(actorRequest.url); const endpoint = getEndpoint(runConfig); - const guardUrl = combineUrlPath(endpoint, url.pathname + url.search); + const guardUrl = combineUrlPath( + endpoint, + `/gateway/${actorId}${url.pathname}${url.search}`, + ); // Handle body properly based on method and presence let bodyToSend: ArrayBuffer | null = null; @@ -76,7 +78,6 @@ function buildGuardHeadersForHttp( headers.set(key, value); } // Add guard-specific headers - headers.set(HEADER_RIVET_TARGET, "actor"); headers.set(HEADER_RIVET_ACTOR, actorId); if (runConfig.token) { headers.set(HEADER_RIVET_TOKEN, runConfig.token); diff --git a/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-websocket-client.ts b/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-websocket-client.ts index fe5fdd9330..18c2fa06c6 100644 --- a/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-websocket-client.ts +++ b/rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/actor-websocket-client.ts @@ -8,7 +8,6 @@ import { WS_PROTOCOL_CONN_TOKEN, WS_PROTOCOL_ENCODING, WS_PROTOCOL_STANDARD as WS_PROTOCOL_RIVETKIT, - WS_PROTOCOL_TARGET, WS_PROTOCOL_TOKEN, } from "@/common/actor-router-consts"; import { importWebSocket } from "@/common/websocket"; @@ -30,7 +29,7 @@ export async function openWebSocketToActor( // WebSocket connections go through guard const endpoint = getEndpoint(runConfig); - const guardUrl = combineUrlPath(endpoint, path); + const guardUrl = combineUrlPath(endpoint, `/gateway/${actorId}${path}`); logger().debug({ msg: "opening websocket to actor via guard", @@ -70,7 +69,6 @@ export function buildWebSocketProtocols( ): string[] { const protocols: string[] = []; protocols.push(WS_PROTOCOL_RIVETKIT); - protocols.push(`${WS_PROTOCOL_TARGET}actor`); protocols.push(`${WS_PROTOCOL_ACTOR}${actorId}`); protocols.push(`${WS_PROTOCOL_ENCODING}${encoding}`); if (runConfig.token) { diff --git a/rivetkit-typescript/packages/rivetkit/tests/parse-actor-path.test.ts b/rivetkit-typescript/packages/rivetkit/tests/parse-actor-path.test.ts index 0651092a0d..5ec8919e43 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/parse-actor-path.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/parse-actor-path.test.ts @@ -3,9 +3,8 @@ import { parseActorPath } from "@/manager/gateway"; describe("parseActorPath", () => { describe("Valid paths with token", () => { - test("should parse basic path with token and route", () => { - const path = - "/gateway/actors/actor-123/tokens/my-token/route/api/v1/endpoint"; + test("should parse basic path with token", () => { + const path = "/gateway/actor-123@my-token/api/v1/endpoint"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -16,7 +15,7 @@ describe("parseActorPath", () => { test("should parse path with UUID as actor ID", () => { const path = - "/gateway/actors/12345678-1234-1234-1234-123456789abc/tokens/my-token/route/status"; + "/gateway/12345678-1234-1234-1234-123456789abc@my-token/status"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -28,8 +27,7 @@ describe("parseActorPath", () => { }); test("should parse path with token and query parameters", () => { - const path = - "/gateway/actors/actor-456/tokens/token123/route/api?key=value"; + const path = "/gateway/actor-456@token123/api?key=value"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -39,7 +37,7 @@ describe("parseActorPath", () => { }); test("should parse path with token and no remaining path", () => { - const path = "/gateway/actors/actor-000/tokens/tok/route"; + const path = "/gateway/actor-000@tok"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -50,7 +48,7 @@ describe("parseActorPath", () => { test("should parse complex path with token and multiple segments", () => { const path = - "/gateway/actors/actor-complex/tokens/secure-token/route/api/v2/users/123/profile/settings"; + "/gateway/actor-complex@secure-token/api/v2/users/123/profile/settings"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -64,7 +62,7 @@ describe("parseActorPath", () => { describe("Valid paths without token", () => { test("should parse basic path without token", () => { - const path = "/gateway/actors/actor-123/route/api/v1/endpoint"; + const path = "/gateway/actor-123/api/v1/endpoint"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -74,8 +72,7 @@ describe("parseActorPath", () => { }); test("should parse path with UUID without token", () => { - const path = - "/gateway/actors/12345678-1234-1234-1234-123456789abc/route/status"; + const path = "/gateway/12345678-1234-1234-1234-123456789abc/status"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -87,8 +84,7 @@ describe("parseActorPath", () => { }); test("should parse path without token and with query params", () => { - const path = - "/gateway/actors/actor-456/route/api/endpoint?foo=bar&baz=qux"; + const path = "/gateway/actor-456/api/endpoint?foo=bar&baz=qux"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -98,7 +94,7 @@ describe("parseActorPath", () => { }); test("should parse path without token and no remaining path", () => { - const path = "/gateway/actors/actor-000/route"; + const path = "/gateway/actor-000"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -110,8 +106,7 @@ describe("parseActorPath", () => { describe("Query parameters and fragments", () => { test("should preserve query parameters", () => { - const path = - "/gateway/actors/actor-456/route/api/endpoint?foo=bar&baz=qux"; + const path = "/gateway/actor-456/api/endpoint?foo=bar&baz=qux"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -119,7 +114,7 @@ describe("parseActorPath", () => { }); test("should strip fragment from path", () => { - const path = "/gateway/actors/actor-789/route/page#section"; + const path = "/gateway/actor-789/page#section"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -129,7 +124,7 @@ describe("parseActorPath", () => { }); test("should preserve query but strip fragment", () => { - const path = "/gateway/actors/actor-123/route/api?query=1#section"; + const path = "/gateway/actor-123/api?query=1#section"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -138,8 +133,8 @@ describe("parseActorPath", () => { expect(result?.remainingPath).toBe("/api?query=1"); }); - test("should handle path ending with route but having query string", () => { - const path = "/gateway/actors/actor-123/route?direct=true"; + test("should handle path with only actor ID and query string", () => { + const path = "/gateway/actor-123?direct=true"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -151,7 +146,7 @@ describe("parseActorPath", () => { describe("Trailing slashes", () => { test("should preserve trailing slash in remaining path", () => { - const path = "/gateway/actors/actor-111/route/api/"; + const path = "/gateway/actor-111/api/"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -163,7 +158,7 @@ describe("parseActorPath", () => { describe("Special characters", () => { test("should handle actor ID with allowed special characters", () => { - const path = "/gateway/actors/actor_id-123.test/route/endpoint"; + const path = "/gateway/actor_id-123.test/endpoint"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -173,8 +168,7 @@ describe("parseActorPath", () => { }); test("should handle URL encoded characters in remaining path", () => { - const path = - "/gateway/actors/actor-123/route/api%20endpoint/test%2Fpath"; + const path = "/gateway/actor-123/api%20endpoint/test%2Fpath"; const result = parseActorPath(path); expect(result).not.toBeNull(); @@ -186,31 +180,11 @@ describe("parseActorPath", () => { describe("Invalid paths - wrong prefix", () => { test("should reject path with wrong prefix", () => { - expect(parseActorPath("/api/actors/123/route/endpoint")).toBeNull(); - }); - - test("should reject path with wrong actor keyword", () => { - expect( - parseActorPath("/gateway/actor/123/route/endpoint"), - ).toBeNull(); + expect(parseActorPath("/api/123/endpoint")).toBeNull(); }); test("should reject path missing gateway prefix", () => { - expect(parseActorPath("/actors/123/route/endpoint")).toBeNull(); - }); - }); - - describe("Invalid paths - missing route", () => { - test("should reject path without route keyword", () => { - expect(parseActorPath("/gateway/actors/123")).toBeNull(); - }); - - test("should reject path with endpoint but no route keyword", () => { - expect(parseActorPath("/gateway/actors/123/endpoint")).toBeNull(); - }); - - test("should reject path with tokens but no route keyword", () => { - expect(parseActorPath("/gateway/actors/123/tokens/tok")).toBeNull(); + expect(parseActorPath("/123/endpoint")).toBeNull(); }); }); @@ -218,88 +192,57 @@ describe("parseActorPath", () => { test("should reject path with only gateway", () => { expect(parseActorPath("/gateway")).toBeNull(); }); - - test("should reject path with only gateway and actors", () => { - expect(parseActorPath("/gateway/actors")).toBeNull(); - }); - - test("should reject path with only gateway, actors, and actor ID", () => { - expect(parseActorPath("/gateway/actors/123")).toBeNull(); - }); }); - describe("Invalid paths - malformed token path", () => { - test("should reject token path missing route keyword", () => { - expect( - parseActorPath("/gateway/actors/123/tokens/tok/api"), - ).toBeNull(); + describe("Invalid paths - malformed token", () => { + test("should reject path with empty actor ID before @", () => { + expect(parseActorPath("/gateway/@token/endpoint")).toBeNull(); }); - test("should reject path with empty token", () => { - expect( - parseActorPath("/gateway/actors/123/tokens//route/api"), - ).toBeNull(); - }); - }); - - describe("Invalid paths - wrong segment positions", () => { - test("should reject segments in wrong order", () => { - expect( - parseActorPath("/actors/gateway/123/route/endpoint"), - ).toBeNull(); - }); - - test("should reject route keyword in wrong position", () => { - expect( - parseActorPath("/gateway/route/actors/123/endpoint"), - ).toBeNull(); + test("should reject path with empty token after @", () => { + expect(parseActorPath("/gateway/actor-123@/endpoint")).toBeNull(); }); }); describe("Invalid paths - empty values", () => { - test("should reject path with empty actor ID", () => { - expect( - parseActorPath("/gateway/actors//route/endpoint"), - ).toBeNull(); - }); - - test("should reject path with empty actor ID in token path", () => { - expect( - parseActorPath("/gateway/actors//tokens/tok/route/endpoint"), - ).toBeNull(); + test("should reject path with empty actor segment", () => { + expect(parseActorPath("/gateway//endpoint")).toBeNull(); }); }); describe("Invalid paths - double slash", () => { test("should reject path with double slashes", () => { - const path = "/gateway/actors//actor-123/route/endpoint"; + const path = "/gateway//actor-123/endpoint"; expect(parseActorPath(path)).toBeNull(); }); }); describe("Invalid paths - case sensitive", () => { test("should reject path with capitalized Gateway", () => { - expect( - parseActorPath("/Gateway/actors/123/route/endpoint"), - ).toBeNull(); + expect(parseActorPath("/Gateway/123/endpoint")).toBeNull(); }); + }); - test("should reject path with capitalized Actors", () => { - expect( - parseActorPath("/gateway/Actors/123/route/endpoint"), - ).toBeNull(); - }); + describe("Token edge cases", () => { + test("should handle token with special characters", () => { + const path = + "/gateway/actor-123@token-with-dashes_and_underscores/api"; + const result = parseActorPath(path); - test("should reject path with capitalized Route", () => { - expect( - parseActorPath("/gateway/actors/123/Route/endpoint"), - ).toBeNull(); + expect(result).not.toBeNull(); + expect(result?.actorId).toBe("actor-123"); + expect(result?.token).toBe("token-with-dashes_and_underscores"); + expect(result?.remainingPath).toBe("/api"); }); - test("should reject token path with capitalized Route", () => { - expect( - parseActorPath("/gateway/actors/123/tokens/tok/Route/endpoint"), - ).toBeNull(); + test("should handle multiple @ symbols (only first is used)", () => { + const path = "/gateway/actor-123@token@extra/api"; + const result = parseActorPath(path); + + expect(result).not.toBeNull(); + expect(result?.actorId).toBe("actor-123"); + expect(result?.token).toBe("token@extra"); + expect(result?.remainingPath).toBe("/api"); }); }); });