Skip to content

Commit 406b9d3

Browse files
committed
Fix clientId usage to follow LiveStore best practices
- Generate clientId using userId prefix + unique identifier instead of using userId directly - Add userId to sync payload for proper authorization - Update authentication pattern to separate client identification from user identification - clientId now identifies device/app instances, userId identifies users for auth - Update all tests and schema documentation to reflect new pattern Fixes the issue where clientId was incorrectly set to userId, which goes against LiveStore's design where clientId should identify unique client instances.
1 parent 4f5a19a commit 406b9d3

File tree

15 files changed

+81
-15
lines changed

15 files changed

+81
-15
lines changed

packages/lib/src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class RuntimeConfig {
3636
public readonly artifactClient: IArtifactClient;
3737
public readonly adapter: Adapter | undefined;
3838
public readonly clientId: string;
39+
public readonly userId: string;
3940

4041
constructor(options: RuntimeAgentOptions) {
4142
this.runtimeId = options.runtimeId;
@@ -48,6 +49,7 @@ export class RuntimeConfig {
4849
DEFAULT_CONFIG.imageArtifactThresholdBytes;
4950
this.adapter = options.adapter;
5051
this.clientId = options.clientId;
52+
this.userId = options.userId;
5153

5254
// Use injected artifact client or create default one
5355
this.artifactClient = options.artifactClient ??

packages/lib/src/runtime-agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export class RuntimeAgent {
9898
runtimeId: this.config.runtimeId,
9999
sessionId: this.config.sessionId,
100100
clientId,
101+
userId: this.config.userId,
101102
},
102103
});
103104

packages/lib/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export interface RuntimeAgentOptions {
7373
readonly adapter: Adapter;
7474
/** Client ID for sync payload (must be provided) */
7575
readonly clientId: string;
76+
/** User ID for sync payload authorization (must be provided) */
77+
readonly userId: string;
7678
}
7779

7880
/**

packages/lib/test/integration.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Deno.test("RuntimeAgent Integration Tests", async (t) => {
6161
syncUrl: "ws://localhost:8787", // Not used with adapter
6262
authToken: "test-integration-token",
6363
clientId: "test-integration-client",
64+
userId: "test-user-id",
6465
adapter,
6566

6667
capabilities,
@@ -154,6 +155,7 @@ Deno.test("RuntimeAgent Integration Tests", async (t) => {
154155
syncUrl: "ws://localhost:8787", // Not used with adapter
155156
authToken: "valid-token",
156157
clientId: "valid-client",
158+
userId: "test-user-id",
157159
adapter,
158160
capabilities: capabilities,
159161
});
@@ -177,6 +179,7 @@ Deno.test("RuntimeAgent Integration Tests", async (t) => {
177179
syncUrl: "ws://localhost:8787", // Not used with adapter
178180
authToken: "token",
179181
clientId: "test-client",
182+
userId: "test-user-id",
180183
adapter,
181184
capabilities: capabilities,
182185
});
@@ -252,6 +255,7 @@ Deno.test("RuntimeConfig", async (t) => {
252255
syncUrl: "ws://localhost:8787", // Not used with adapter
253256
authToken: "test-token",
254257
clientId: "test-client-3",
258+
userId: "test-user-id",
255259
adapter,
256260
capabilities: {
257261
canExecuteCode: true,
@@ -279,6 +283,7 @@ Deno.test("RuntimeConfig", async (t) => {
279283
syncUrl: "ws://localhost:8787", // Not used with adapter
280284
authToken: "token1",
281285
clientId: "client1",
286+
userId: "test-user-1",
282287
adapter: adapter1,
283288
capabilities: {
284289
canExecuteCode: true,
@@ -296,6 +301,7 @@ Deno.test("RuntimeConfig", async (t) => {
296301
syncUrl: "ws://localhost:8787", // Not used with adapter
297302
authToken: "token2",
298303
clientId: "client2",
304+
userId: "test-user-2",
299305
adapter: adapter2,
300306
capabilities: {
301307
canExecuteCode: true,
@@ -318,6 +324,7 @@ Deno.test("RuntimeConfig", async (t) => {
318324
syncUrl: "ws://localhost:8787", // Not used with adapter
319325
authToken: "test-ai-token",
320326
clientId: "test-client-ai",
327+
userId: "test-user-id",
321328
adapter,
322329
capabilities: {
323330
canExecuteCode: true,

packages/lib/test/mod.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Deno.test("RuntimeConfig validation works", () => {
2828
authToken: "", // Missing
2929
notebookId: "", // Missing
3030
clientId: "test-client",
31+
userId: "test-user-id",
3132
adapter: makeInMemoryAdapter({}),
3233
capabilities: {
3334
canExecuteCode: true,
@@ -52,6 +53,7 @@ Deno.test("RuntimeConfig validation works", () => {
5253
authToken: "test-token",
5354
notebookId: "test-notebook",
5455
clientId: "test-client",
56+
userId: "test-user-id",
5557
adapter: makeInMemoryAdapter({}),
5658
capabilities: {
5759
canExecuteCode: true,

packages/lib/test/runtime-agent-adapter-injection.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function createTestRuntimeConfig(
3737
canExecuteAi: false,
3838
},
3939
clientId: "test-client",
40+
userId: "test-user-id",
4041
adapter: defaultAdapter,
4142
...defaults,
4243
};
@@ -49,6 +50,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
4950
() => {
5051
const config = createTestRuntimeConfig([], {
5152
clientId: "test-client-backward-compat",
53+
userId: "test-user-id",
5254
notebookId: "test-notebook",
5355
syncUrl: "ws://fake-url:9999", // Will fail but that's expected
5456
});
@@ -76,6 +78,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
7678
const config = createTestRuntimeConfig([], {
7779
adapter,
7880
clientId: "test-client-adapter",
81+
userId: "test-user-id",
7982
notebookId: "adapter-test",
8083
syncUrl: "ws://fake-url:9999",
8184
});
@@ -109,6 +112,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
109112
const config = createTestRuntimeConfig([], {
110113
adapter,
111114
clientId: "test-client-generated",
115+
userId: "test-user-id",
112116
notebookId: "adapter-test-2",
113117
authToken: "test-token",
114118
syncUrl: "ws://fake-url:9999",
@@ -138,6 +142,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
138142
const config = createTestRuntimeConfig([], {
139143
adapter,
140144
runtimeId,
145+
userId: "test-user-id",
141146
notebookId: "clientid-test",
142147
syncUrl: "ws://fake-url:9999",
143148
});
@@ -168,6 +173,7 @@ Deno.test("RuntimeAgent adapter injection", async (t) => {
168173
const config = createTestRuntimeConfig([], {
169174
adapter,
170175
clientId: explicitClientId,
176+
userId: "test-user-id",
171177
notebookId: "explicit-clientid-test",
172178
syncUrl: "ws://fake-url:9999",
173179
});

packages/lib/test/runtime-agent-artifact.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ const mockRuntimeOptions: RuntimeAgentOptions = {
7373
authToken: "test-token",
7474
notebookId: "test-notebook",
7575
clientId: "test-client",
76+
userId: "test-user-id",
7677
imageArtifactThresholdBytes: 6 * 1024, // 6KB threshold
7778
adapter: makeInMemoryAdapter({}),
7879
};

packages/lib/test/runtime-agent-text-representations.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@ Deno.test("RuntimeAgent Text Representations for Artifacts", async (t) => {
4242
runtimeId: "test-runtime",
4343
runtimeType: "test",
4444
notebookId: "test-notebook",
45-
syncUrl: "ws://localhost:8787", // Not used with adapter
45+
syncUrl: "ws://localhost:8787",
4646
authToken: "test-token",
4747
clientId: "test-client",
48+
userId: "test-user-id",
4849
adapter,
4950
capabilities: capabilities,
51+
imageArtifactThresholdBytes: 1024,
5052
});
5153

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

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

packages/lib/test/runtime-agent.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Deno.test("RuntimeAgent", async (t) => {
6161
syncUrl: "ws://localhost:8787", // Not used with adapter
6262
authToken: "test-token",
6363
clientId: "test-client",
64+
userId: "test-user-id",
6465
adapter,
6566
capabilities,
6667
});
@@ -177,6 +178,7 @@ Deno.test("RuntimeConfig", async (t) => {
177178
syncUrl: "ws://localhost:8787", // Not used with adapter
178179
authToken: "test-token",
179180
clientId: "test-client",
181+
userId: "test-user-id",
180182
adapter,
181183
capabilities: {
182184
canExecuteCode: true,
@@ -203,6 +205,7 @@ Deno.test("RuntimeConfig", async (t) => {
203205
syncUrl: "ws://localhost:8787", // Not used with adapter
204206
authToken: "token1",
205207
clientId: "client1",
208+
userId: "test-user-1",
206209
adapter: adapter1,
207210
capabilities: {
208211
canExecuteCode: true,
@@ -220,6 +223,7 @@ Deno.test("RuntimeConfig", async (t) => {
220223
syncUrl: "ws://localhost:8787", // Not used with adapter
221224
authToken: "token2",
222225
clientId: "client2",
226+
userId: "test-user-2",
223227
adapter: adapter2,
224228
capabilities: {
225229
canExecuteCode: true,
@@ -242,6 +246,7 @@ Deno.test("RuntimeConfig", async (t) => {
242246
syncUrl: "ws://localhost:8787", // Not used with adapter
243247
authToken: "test-token",
244248
clientId: "test-client",
249+
userId: "test-user-id",
245250
adapter,
246251
capabilities: {
247252
canExecuteCode: true,

packages/pyodide-runtime-agent/src/auth.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,29 @@ export interface UserInfo {
2727
name?: string;
2828
}
2929

30+
/**
31+
* Authentication result containing user info and generated client ID
32+
*/
33+
export interface AuthenticationResult {
34+
userId: string;
35+
clientId: string;
36+
userInfo: UserInfo;
37+
}
38+
3039
/**
3140
* Discover authenticated user identity via /api/me endpoint
3241
*
3342
* This should be called before creating runtime agents to get the clientId.
43+
* The clientId is generated using the user ID as a prefix plus a unique identifier,
44+
* following LiveStore best practices where clientId identifies device/app instances.
3445
*
3546
* @param options - Configuration for identity discovery
36-
* @returns Promise resolving to user ID
47+
* @returns Promise resolving to authentication result with userId and generated clientId
3748
* @throws Error if authentication fails
3849
*/
3950
export async function discoverUserIdentity(
4051
options: DiscoverUserIdentityOptions,
41-
): Promise<string> {
52+
): Promise<AuthenticationResult> {
4253
const { authToken, syncUrl, skipInTests = true } = options;
4354

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

5667
if (isTestEnvironment) {
5768
logger.debug("Skipping authentication in test environment");
58-
return "test-user-id";
69+
const userId = "test-user-id";
70+
const clientId = `${userId}-${crypto.randomUUID()}`;
71+
return {
72+
userId,
73+
clientId,
74+
userInfo: { id: userId, email: "test@example.com" },
75+
};
5976
}
6077
}
6178

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

107-
logger.debug("User identity discovered", {
108-
userId: userInfo.id,
124+
// Generate clientId using user ID as prefix plus unique identifier
125+
// This follows LiveStore best practices where clientId identifies device/app instances
126+
const userId = userInfo.id;
127+
const clientId = `${userId}-${crypto.randomUUID()}`;
128+
129+
logger.debug("User identity discovered and clientId generated", {
130+
userId,
131+
clientId,
109132
email: userInfo.email,
110133
name: userInfo.name,
111134
});
112135

113-
return userInfo.id;
136+
return {
137+
userId,
138+
clientId,
139+
userInfo,
140+
};
114141
} catch (error) {
115142
// If we haven't already logged the error above, log it here
116143
if (!(error instanceof Error && error.message.startsWith("HTTP "))) {

0 commit comments

Comments
 (0)