Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/lib/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class RuntimeConfig {
public readonly artifactClient: IArtifactClient;
public readonly adapter: Adapter | undefined;
public readonly clientId: string;
public readonly userId: string;

constructor(options: RuntimeAgentOptions) {
this.runtimeId = options.runtimeId;
Expand All @@ -48,6 +49,7 @@ export class RuntimeConfig {
DEFAULT_CONFIG.imageArtifactThresholdBytes;
this.adapter = options.adapter;
this.clientId = options.clientId;
this.userId = options.userId;

// Use injected artifact client or create default one
this.artifactClient = options.artifactClient ??
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface LoggerConfig {
*/
class Logger {
private config: LoggerConfig = {
level: LogLevel.ERROR,
level: LogLevel.INFO,
console: true,
service: "runt-agent",
};
Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/runtime-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class RuntimeAgent {
runtimeId: this.config.runtimeId,
sessionId: this.config.sessionId,
clientId,
userId: this.config.userId,
},
});

Expand Down
2 changes: 2 additions & 0 deletions packages/lib/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export interface RuntimeAgentOptions {
readonly adapter: Adapter;
/** Client ID for sync payload (must be provided) */
readonly clientId: string;
/** User ID for sync payload authorization (must be provided) */
readonly userId: string;
}

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/lib/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Deno.test("RuntimeAgent Integration Tests", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-integration-token",
clientId: "test-integration-client",
userId: "test-user-id",
adapter,

capabilities,
Expand Down Expand Up @@ -154,6 +155,7 @@ Deno.test("RuntimeAgent Integration Tests", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "valid-token",
clientId: "valid-client",
userId: "test-user-id",
adapter,
capabilities: capabilities,
});
Expand All @@ -177,6 +179,7 @@ Deno.test("RuntimeAgent Integration Tests", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "token",
clientId: "test-client",
userId: "test-user-id",
adapter,
capabilities: capabilities,
});
Expand Down Expand Up @@ -252,6 +255,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-token",
clientId: "test-client-3",
userId: "test-user-id",
adapter,
capabilities: {
canExecuteCode: true,
Expand Down Expand Up @@ -279,6 +283,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "token1",
clientId: "client1",
userId: "test-user-1",
adapter: adapter1,
capabilities: {
canExecuteCode: true,
Expand All @@ -296,6 +301,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "token2",
clientId: "client2",
userId: "test-user-2",
adapter: adapter2,
capabilities: {
canExecuteCode: true,
Expand All @@ -318,6 +324,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-ai-token",
clientId: "test-client-ai",
userId: "test-user-id",
adapter,
capabilities: {
canExecuteCode: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/test/mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Deno.test("RuntimeConfig validation works", () => {
authToken: "", // Missing
notebookId: "", // Missing
clientId: "test-client",
userId: "test-user-id",
adapter: makeInMemoryAdapter({}),
capabilities: {
canExecuteCode: true,
Expand All @@ -52,6 +53,7 @@ Deno.test("RuntimeConfig validation works", () => {
authToken: "test-token",
notebookId: "test-notebook",
clientId: "test-client",
userId: "test-user-id",
adapter: makeInMemoryAdapter({}),
capabilities: {
canExecuteCode: true,
Expand Down
6 changes: 6 additions & 0 deletions packages/lib/test/runtime-agent-adapter-injection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function createTestRuntimeConfig(
canExecuteAi: false,
},
clientId: "test-client",
userId: "test-user-id",
adapter: defaultAdapter,
...defaults,
};
Expand All @@ -49,6 +50,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
() => {
const config = createTestRuntimeConfig([], {
clientId: "test-client-backward-compat",
userId: "test-user-id",
notebookId: "test-notebook",
syncUrl: "ws://fake-url:9999", // Will fail but that's expected
});
Expand Down Expand Up @@ -76,6 +78,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
const config = createTestRuntimeConfig([], {
adapter,
clientId: "test-client-adapter",
userId: "test-user-id",
notebookId: "adapter-test",
syncUrl: "ws://fake-url:9999",
});
Expand Down Expand Up @@ -109,6 +112,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
const config = createTestRuntimeConfig([], {
adapter,
clientId: "test-client-generated",
userId: "test-user-id",
notebookId: "adapter-test-2",
authToken: "test-token",
syncUrl: "ws://fake-url:9999",
Expand Down Expand Up @@ -138,6 +142,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
const config = createTestRuntimeConfig([], {
adapter,
runtimeId,
userId: "test-user-id",
notebookId: "clientid-test",
syncUrl: "ws://fake-url:9999",
});
Expand Down Expand Up @@ -168,6 +173,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
const config = createTestRuntimeConfig([], {
adapter,
clientId: explicitClientId,
userId: "test-user-id",
notebookId: "explicit-clientid-test",
syncUrl: "ws://fake-url:9999",
});
Expand Down
1 change: 1 addition & 0 deletions packages/lib/test/runtime-agent-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const mockRuntimeOptions: RuntimeAgentOptions = {
authToken: "test-token",
notebookId: "test-notebook",
clientId: "test-client",
userId: "test-user-id",
imageArtifactThresholdBytes: 6 * 1024, // 6KB threshold
adapter: makeInMemoryAdapter({}),
};
Expand Down
6 changes: 5 additions & 1 deletion packages/lib/test/runtime-agent-text-representations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ Deno.test("RuntimeAgent Text Representations for Artifacts", async (t) => {
runtimeId: "test-runtime",
runtimeType: "test",
notebookId: "test-notebook",
syncUrl: "ws://localhost:8787", // Not used with adapter
syncUrl: "ws://localhost:8787",
authToken: "test-token",
clientId: "test-client",
userId: "test-user-id",
adapter,
capabilities: capabilities,
imageArtifactThresholdBytes: 1024,
});

const agent = new RuntimeAgent(config, capabilities);
Expand Down Expand Up @@ -158,6 +160,7 @@ Deno.test("RuntimeAgent Text Representations for Artifacts", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-token",
clientId: "test-client",
userId: "test-user-id",
adapter,
capabilities,

Expand Down Expand Up @@ -273,6 +276,7 @@ Deno.test("RuntimeAgent Text Representations for Artifacts", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-token",
clientId: "test-client",
userId: "test-user-id",
adapter,
capabilities,

Expand Down
5 changes: 5 additions & 0 deletions packages/lib/test/runtime-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Deno.test("RuntimeAgent", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-token",
clientId: "test-client",
userId: "test-user-id",
adapter,
capabilities,
});
Expand Down Expand Up @@ -177,6 +178,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-token",
clientId: "test-client",
userId: "test-user-id",
adapter,
capabilities: {
canExecuteCode: true,
Expand All @@ -203,6 +205,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "token1",
clientId: "client1",
userId: "test-user-1",
adapter: adapter1,
capabilities: {
canExecuteCode: true,
Expand All @@ -220,6 +223,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "token2",
clientId: "client2",
userId: "test-user-2",
adapter: adapter2,
capabilities: {
canExecuteCode: true,
Expand All @@ -242,6 +246,7 @@ Deno.test("RuntimeConfig", async (t) => {
syncUrl: "ws://localhost:8787", // Not used with adapter
authToken: "test-token",
clientId: "test-client",
userId: "test-user-id",
adapter,
capabilities: {
canExecuteCode: true,
Expand Down
39 changes: 33 additions & 6 deletions packages/pyodide-runtime-agent/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,29 @@ export interface UserInfo {
name?: string;
}

/**
* Authentication result containing user info and generated client ID
*/
export interface AuthenticationResult {
userId: string;
clientId: string;
userInfo: UserInfo;
}

/**
* Discover authenticated user identity via /api/me endpoint
*
* This should be called before creating runtime agents to get the clientId.
* The clientId is generated using the user ID as a prefix plus a unique identifier,
* following LiveStore best practices where clientId identifies device/app instances.
*
* @param options - Configuration for identity discovery
* @returns Promise resolving to user ID
* @returns Promise resolving to authentication result with userId and generated clientId
* @throws Error if authentication fails
*/
export async function discoverUserIdentity(
options: DiscoverUserIdentityOptions,
): Promise<string> {
): Promise<AuthenticationResult> {
const { authToken, syncUrl, skipInTests = true } = options;

// Skip authentication in test environments if enabled
Expand All @@ -55,7 +66,13 @@ export async function discoverUserIdentity(

if (isTestEnvironment) {
logger.debug("Skipping authentication in test environment");
return "test-user-id";
const userId = "test-user-id";
const clientId = `${userId}-${crypto.randomUUID()}`;
return {
userId,
clientId,
userInfo: { id: userId, email: "test@example.com" },
};
}
}

Expand Down Expand Up @@ -104,13 +121,23 @@ export async function discoverUserIdentity(
throw new Error("User ID not found in response");
}

logger.debug("User identity discovered", {
userId: userInfo.id,
// Generate clientId using user ID as prefix plus unique identifier
// This follows LiveStore best practices where clientId identifies device/app instances
const userId = userInfo.id;
const clientId = `${userId}-${crypto.randomUUID()}`;

logger.debug("User identity discovered and clientId generated", {
userId,
clientId,
email: userInfo.email,
name: userInfo.name,
});

return userInfo.id;
return {
userId,
clientId,
userInfo,
};
} catch (error) {
// If we haven't already logged the error above, log it here
if (!(error instanceof Error && error.message.startsWith("HTTP "))) {
Expand Down
22 changes: 16 additions & 6 deletions packages/pyodide-runtime-agent/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@ if (import.meta.main) {
logLevel = LogLevel.ERROR;
break;
default:
logLevel = LogLevel.ERROR;
logLevel = LogLevel.INFO;
}

logger.configure({
level: logLevel,
console: !disableConsole,
});
} else {
// Configure with INFO as default when no RUNT_LOG_LEVEL is set
logger.configure({
level: LogLevel.INFO,
console: !disableConsole,
});
}

logger.info("Authenticating...");
Expand All @@ -63,13 +69,17 @@ if (import.meta.main) {
Deno.exit(1);
}

// Discover user identity first
const clientId = await discoverUserIdentity({
// Discover user identity and generate proper clientId
const { userId, clientId, userInfo } = await discoverUserIdentity({
authToken,
syncUrl,
});

logger.info("Authenticated successfully", { clientId });
logger.info("Authenticated successfully", {
userId,
clientId,
email: userInfo.email,
});

// Create adapter for Node.js environment with Cloudflare sync
const adapter = makeAdapter({
Expand All @@ -80,11 +90,11 @@ if (import.meta.main) {
},
});

// Create agent with discovered clientId and Node.js adapter
// Create agent with discovered clientId, userId, and Node.js adapter
const agent = new PyodideRuntimeAgent(
Deno.args,
{}, // pyodide options
{ clientId, adapter }, // runtime options
{ clientId, adapter, userId }, // runtime options
);

logger.info("Starting Agent");
Expand Down
Loading