@pikku/core 0.12.17 → 0.12.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/function/function-runner.js +22 -1
  3. package/dist/services/in-memory-session-store.d.ts +8 -0
  4. package/dist/services/in-memory-session-store.js +12 -0
  5. package/dist/services/index.d.ts +2 -0
  6. package/dist/services/index.js +1 -0
  7. package/dist/services/session-store.d.ts +6 -0
  8. package/dist/services/session-store.js +1 -0
  9. package/dist/services/user-session-service.d.ts +12 -10
  10. package/dist/services/user-session-service.js +18 -21
  11. package/dist/testing/service-tests.d.ts +2 -0
  12. package/dist/testing/service-tests.js +50 -11
  13. package/dist/types/core.types.d.ts +3 -0
  14. package/dist/wirings/channel/channel-store.d.ts +5 -6
  15. package/dist/wirings/channel/local/local-channel-runner.js +7 -4
  16. package/dist/wirings/channel/serverless/serverless-channel-runner.js +25 -8
  17. package/dist/wirings/cli/cli-runner.js +1 -1
  18. package/dist/wirings/http/http-runner.js +1 -1
  19. package/dist/wirings/mcp/mcp-runner.js +1 -1
  20. package/dist/wirings/scheduler/scheduler-runner.js +1 -1
  21. package/dist/wirings/workflow/pikku-workflow-service.js +0 -1
  22. package/package.json +1 -1
  23. package/src/function/function-runner.ts +36 -1
  24. package/src/services/in-memory-session-store.test.ts +58 -0
  25. package/src/services/in-memory-session-store.ts +21 -0
  26. package/src/services/index.ts +2 -0
  27. package/src/services/session-store.ts +9 -0
  28. package/src/services/user-session-service.test.ts +53 -19
  29. package/src/services/user-session-service.ts +21 -22
  30. package/src/testing/service-tests.ts +64 -11
  31. package/src/types/core.types.ts +3 -0
  32. package/src/wirings/channel/channel-store.ts +5 -8
  33. package/src/wirings/channel/local/local-channel-runner.ts +10 -4
  34. package/src/wirings/channel/local/local-eventhub-service.test.ts +0 -5
  35. package/src/wirings/channel/serverless/serverless-channel-runner.ts +30 -9
  36. package/src/wirings/cli/cli-runner.ts +3 -1
  37. package/src/wirings/http/http-runner.ts +3 -1
  38. package/src/wirings/mcp/mcp-runner.ts +3 -1
  39. package/src/wirings/scheduler/scheduler-runner.ts +1 -1
  40. package/src/wirings/workflow/pikku-workflow-service.ts +0 -1
  41. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,58 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { InMemorySessionStore } from './in-memory-session-store.js'
5
+
6
+ describe('InMemorySessionStore', () => {
7
+ test('get returns undefined for unknown user', async () => {
8
+ const store = new InMemorySessionStore()
9
+ const result = await store.get('unknown')
10
+ assert.equal(result, undefined)
11
+ })
12
+
13
+ test('set and get round-trip', async () => {
14
+ const store = new InMemorySessionStore()
15
+ const session = { userId: 'user-1', organizationId: 'org-1' } as any
16
+ await store.set('user-1', session)
17
+
18
+ const result = await store.get('user-1')
19
+ assert.deepEqual(result, session)
20
+ })
21
+
22
+ test('set overwrites previous session', async () => {
23
+ const store = new InMemorySessionStore()
24
+ await store.set('user-1', { userId: 'user-1', role: 'admin' } as any)
25
+ await store.set('user-1', { userId: 'user-1', role: 'member' } as any)
26
+
27
+ const result = await store.get('user-1')
28
+ assert.deepEqual(result, { userId: 'user-1', role: 'member' })
29
+ })
30
+
31
+ test('clear removes session', async () => {
32
+ const store = new InMemorySessionStore()
33
+ await store.set('user-1', { userId: 'user-1' } as any)
34
+ assert.ok(await store.get('user-1'))
35
+
36
+ await store.clear('user-1')
37
+ const result = await store.get('user-1')
38
+ assert.equal(result, undefined)
39
+ })
40
+
41
+ test('clear is no-op for unknown user', async () => {
42
+ const store = new InMemorySessionStore()
43
+ await store.clear('nonexistent')
44
+ })
45
+
46
+ test('users are isolated', async () => {
47
+ const store = new InMemorySessionStore()
48
+ await store.set('user-a', { userId: 'a' } as any)
49
+ await store.set('user-b', { userId: 'b' } as any)
50
+
51
+ assert.deepEqual(await store.get('user-a'), { userId: 'a' })
52
+ assert.deepEqual(await store.get('user-b'), { userId: 'b' })
53
+
54
+ await store.clear('user-a')
55
+ assert.equal(await store.get('user-a'), undefined)
56
+ assert.deepEqual(await store.get('user-b'), { userId: 'b' })
57
+ })
58
+ })
@@ -0,0 +1,21 @@
1
+ import type { CoreUserSession } from '../types/core.types.js'
2
+ import type { SessionStore } from './session-store.js'
3
+
4
+ export class InMemorySessionStore<
5
+ UserSession extends CoreUserSession = CoreUserSession,
6
+ > implements SessionStore<UserSession>
7
+ {
8
+ private sessions = new Map<string, UserSession>()
9
+
10
+ async get(pikkuUserId: string): Promise<UserSession | undefined> {
11
+ return this.sessions.get(pikkuUserId)
12
+ }
13
+
14
+ async set(pikkuUserId: string, session: UserSession): Promise<void> {
15
+ this.sessions.set(pikkuUserId, session)
16
+ }
17
+
18
+ async clear(pikkuUserId: string): Promise<void> {
19
+ this.sessions.delete(pikkuUserId)
20
+ }
21
+ }
@@ -68,6 +68,8 @@ export type {
68
68
  } from './typed-credential-service.js'
69
69
  export type { VariableStatus, VariableMeta } from './typed-variables-service.js'
70
70
  export type { MetaService } from './meta-service.js'
71
+ export type { SessionStore } from './session-store.js'
72
+ export { InMemorySessionStore } from './in-memory-session-store.js'
71
73
  export type {
72
74
  MCPMeta,
73
75
  RPCMetaRecord,
@@ -0,0 +1,9 @@
1
+ import type { CoreUserSession } from '../types/core.types.js'
2
+
3
+ export interface SessionStore<
4
+ UserSession extends CoreUserSession = CoreUserSession,
5
+ > {
6
+ get(pikkuUserId: string): Promise<UserSession | undefined>
7
+ set(pikkuUserId: string, session: UserSession): Promise<void>
8
+ clear(pikkuUserId: string): Promise<void>
9
+ }
@@ -19,17 +19,17 @@ describe('PikkuSessionService', () => {
19
19
  assert.strictEqual(service.get(), undefined)
20
20
  })
21
21
 
22
- test('should mark sessionChanged when set is called', () => {
22
+ test('should mark sessionChanged when set is called', async () => {
23
23
  const service = new PikkuSessionService()
24
24
  assert.strictEqual(service.sessionChanged, false)
25
- service.set({ userId: 'user-1' })
25
+ await service.set({ userId: 'user-1' })
26
26
  assert.strictEqual(service.sessionChanged, true)
27
27
  })
28
28
 
29
- test('should clear session', () => {
29
+ test('should clear session', async () => {
30
30
  const service = new PikkuSessionService()
31
31
  service.setInitial({ userId: 'user-1' })
32
- service.clear()
32
+ await service.clear()
33
33
  assert.strictEqual(service.get(), undefined)
34
34
  assert.strictEqual(service.sessionChanged, true)
35
35
  })
@@ -41,35 +41,69 @@ describe('PikkuSessionService', () => {
41
41
  assert.deepStrictEqual(frozen, { userId: 'user-1' })
42
42
  })
43
43
 
44
- test('should only freeze initial once', () => {
44
+ test('should only freeze initial once', async () => {
45
45
  const service = new PikkuSessionService()
46
46
  service.setInitial({ userId: 'user-1' })
47
47
  service.freezeInitial()
48
- service.set({ userId: 'user-2' })
48
+ await service.set({ userId: 'user-2' })
49
49
  const frozen2 = service.freezeInitial()
50
50
  assert.deepStrictEqual(frozen2, { userId: 'user-1' })
51
51
  })
52
52
 
53
- test('should throw when channelStore provided without channelId', () => {
54
- assert.throws(() => new PikkuSessionService({} as any), {
55
- message: 'Channel ID is required when using channel store',
56
- })
57
- })
58
-
59
- test('should use channelStore when provided', async () => {
53
+ test('should persist to sessionStore on set when pikkuUserId is set', async () => {
54
+ let storedId: string | undefined
60
55
  let storedSession: any
61
56
  const store = {
62
- setUserSession: (id: string, session: any) => {
57
+ get: async () => undefined,
58
+ set: async (id: string, session: any) => {
59
+ storedId = id
63
60
  storedSession = session
64
61
  },
65
- getChannelAndSession: (id: string) => ({ session: { userId: 'stored' } }),
62
+ clear: async () => {},
66
63
  }
67
- const service = new PikkuSessionService(store as any, 'ch-1')
68
- service.set({ userId: 'new' })
64
+ const service = new PikkuSessionService(store as any)
65
+ service.setPikkuUserId('user-123')
66
+ await service.set({ userId: 'new' })
67
+ assert.strictEqual(storedId, 'user-123')
69
68
  assert.deepStrictEqual(storedSession, { userId: 'new' })
69
+ })
70
70
 
71
- const session = service.get()
72
- assert.deepStrictEqual(session, { userId: 'stored' })
71
+ test('should not persist to sessionStore when pikkuUserId is not set', async () => {
72
+ let setCalled = false
73
+ const store = {
74
+ get: async () => undefined,
75
+ set: async () => {
76
+ setCalled = true
77
+ },
78
+ clear: async () => {},
79
+ }
80
+ const service = new PikkuSessionService(store as any)
81
+ await service.set({ userId: 'new' })
82
+ assert.strictEqual(setCalled, false)
83
+ })
84
+
85
+ test('should clear from sessionStore when pikkuUserId is set', async () => {
86
+ let clearedId: string | undefined
87
+ const store = {
88
+ get: async () => undefined,
89
+ set: async () => {},
90
+ clear: async (id: string) => {
91
+ clearedId = id
92
+ },
93
+ }
94
+ const service = new PikkuSessionService(store as any)
95
+ service.setPikkuUserId('user-123')
96
+ service.setInitial({ userId: 'user-123' })
97
+ await service.clear()
98
+ assert.strictEqual(clearedId, 'user-123')
99
+ assert.strictEqual(service.get(), undefined)
100
+ })
101
+
102
+ test('getPikkuUserId should return set value', () => {
103
+ const service = new PikkuSessionService()
104
+ assert.strictEqual(service.getPikkuUserId(), undefined)
105
+ service.setPikkuUserId('user-456')
106
+ assert.strictEqual(service.getPikkuUserId(), 'user-456')
73
107
  })
74
108
  })
75
109
 
@@ -1,5 +1,5 @@
1
- import type { ChannelStore } from '../wirings/channel/channel-store.js'
2
1
  import type { CoreUserSession } from '../types/core.types.js'
2
+ import type { SessionStore } from './session-store.js'
3
3
 
4
4
  export interface SessionService<UserSession extends CoreUserSession> {
5
5
  sessionChanged: boolean
@@ -8,7 +8,7 @@ export interface SessionService<UserSession extends CoreUserSession> {
8
8
  freezeInitial(): UserSession | undefined
9
9
  set(session: UserSession): Promise<void> | void
10
10
  clear(): Promise<void> | void
11
- get(): Promise<UserSession> | UserSession | undefined
11
+ get(): UserSession | undefined
12
12
  }
13
13
 
14
14
  export class PikkuSessionService<UserSession extends CoreUserSession>
@@ -17,13 +17,16 @@ export class PikkuSessionService<UserSession extends CoreUserSession>
17
17
  public sessionChanged = false
18
18
  public initial: UserSession | undefined
19
19
  private session: UserSession | undefined
20
- constructor(
21
- private channelStore?: ChannelStore<unknown, unknown, UserSession>,
22
- private channelId?: string
23
- ) {
24
- if (channelStore && !channelId) {
25
- throw new Error('Channel ID is required when using channel store')
26
- }
20
+ private pikkuUserId?: string
21
+
22
+ constructor(private sessionStore?: SessionStore<UserSession>) {}
23
+
24
+ public setPikkuUserId(id: string) {
25
+ this.pikkuUserId = id
26
+ }
27
+
28
+ public getPikkuUserId(): string | undefined {
29
+ return this.pikkuUserId
27
30
  }
28
31
 
29
32
  public setInitial(session: UserSession) {
@@ -37,27 +40,23 @@ export class PikkuSessionService<UserSession extends CoreUserSession>
37
40
  return this.initial
38
41
  }
39
42
 
40
- public set(session: UserSession) {
43
+ public async set(session: UserSession) {
41
44
  this.sessionChanged = true
42
45
  this.session = session
43
- return this.channelStore?.setUserSession(this.channelId!, session)
46
+ if (this.sessionStore && this.pikkuUserId) {
47
+ await this.sessionStore.set(this.pikkuUserId, session)
48
+ }
44
49
  }
45
50
 
46
- public clear() {
51
+ public async clear() {
47
52
  this.sessionChanged = true
48
53
  this.session = undefined
49
- return this.channelStore?.setUserSession(this.channelId!, null)
54
+ if (this.sessionStore && this.pikkuUserId) {
55
+ await this.sessionStore.clear(this.pikkuUserId)
56
+ }
50
57
  }
51
58
 
52
- public get(): Promise<UserSession> | UserSession | undefined {
53
- if (this.channelStore) {
54
- const channel = this.channelStore.getChannelAndSession(this.channelId!)
55
- if (channel instanceof Promise) {
56
- return channel.then(({ session }) => session)
57
- } else {
58
- return channel.session
59
- }
60
- }
59
+ public get(): UserSession | undefined {
61
60
  return this.session
62
61
  }
63
62
  }
@@ -11,6 +11,7 @@ import type { AIRunStateService } from '../services/ai-run-state-service.js'
11
11
  import type { SecretService } from '../services/secret-service.js'
12
12
  import type { CredentialService } from '../services/credential-service.js'
13
13
  import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
14
+ import type { SessionStore } from '../services/session-store.js'
14
15
 
15
16
  export interface ServiceTestConfig {
16
17
  name: string
@@ -34,6 +35,7 @@ export interface ServiceTestConfig {
34
35
  keyVersion?: number
35
36
  previousKey?: string
36
37
  }) => Promise<CredentialService & { rotateKEK?(): Promise<number> }>
38
+ sessionStore?: () => Promise<SessionStore>
37
39
  }
38
40
  }
39
41
 
@@ -49,32 +51,38 @@ export function defineServiceTests(config: ServiceTestConfig): void {
49
51
  store = await factory()
50
52
  })
51
53
 
52
- test('addChannel and getChannelAndSession', async () => {
54
+ test('addChannel and getChannel', async () => {
53
55
  await store.addChannel({
54
56
  channelId: 'ch-1',
55
57
  channelName: 'test-channel',
56
58
  openingData: { foo: 'bar' },
57
59
  })
58
60
 
59
- const result = await store.getChannelAndSession('ch-1')
61
+ const result = await store.getChannel('ch-1')
60
62
  assert.equal(result.channelId, 'ch-1')
61
63
  assert.equal(result.channelName, 'test-channel')
62
64
  assert.deepEqual(result.openingData, { foo: 'bar' })
63
- assert.deepEqual(result.session, {})
65
+ assert.equal(result.pikkuUserId, undefined)
64
66
  })
65
67
 
66
- test('setUserSession', async () => {
67
- const session = { userId: 'user-1' } as any
68
- await store.setUserSession('ch-1', session)
68
+ test('setPikkuUserId', async () => {
69
+ await store.setPikkuUserId('ch-1', 'user-1')
69
70
 
70
- const result = await store.getChannelAndSession('ch-1')
71
- assert.deepEqual(result.session, session)
71
+ const result = await store.getChannel('ch-1')
72
+ assert.equal(result.pikkuUserId, 'user-1')
72
73
  })
73
74
 
74
- test('getChannelAndSession throws for missing channel', async () => {
75
+ test('setPikkuUserId to null', async () => {
76
+ await store.setPikkuUserId('ch-1', null)
77
+
78
+ const result = await store.getChannel('ch-1')
79
+ assert.equal(result.pikkuUserId, undefined)
80
+ })
81
+
82
+ test('getChannel throws for missing channel', async () => {
75
83
  await assert.rejects(
76
84
  async () => {
77
- await store.getChannelAndSession('missing')
85
+ await store.getChannel('missing')
78
86
  },
79
87
  { message: 'Channel not found: missing' }
80
88
  )
@@ -87,7 +95,7 @@ export function defineServiceTests(config: ServiceTestConfig): void {
87
95
  })
88
96
  await store.removeChannels(['ch-2'])
89
97
  await assert.rejects(async () => {
90
- await store.getChannelAndSession('ch-2')
98
+ await store.getChannel('ch-2')
91
99
  })
92
100
  })
93
101
 
@@ -999,4 +1007,49 @@ export function defineServiceTests(config: ServiceTestConfig): void {
999
1007
  })
1000
1008
  })
1001
1009
  }
1010
+
1011
+ if (services.sessionStore) {
1012
+ const factory = services.sessionStore
1013
+ describe(`SessionStore [${name}]`, () => {
1014
+ let store: SessionStore
1015
+
1016
+ before(async () => {
1017
+ store = await factory()
1018
+ })
1019
+
1020
+ test('get returns undefined for unknown user', async () => {
1021
+ const result = await store.get('unknown-user')
1022
+ assert.equal(result, undefined)
1023
+ })
1024
+
1025
+ test('set and get round-trip', async () => {
1026
+ const session = { userId: 'user-1', organizationId: 'org-1' } as any
1027
+ await store.set('user-1', session)
1028
+
1029
+ const result = await store.get('user-1')
1030
+ assert.deepEqual(result, session)
1031
+ })
1032
+
1033
+ test('set overwrites previous session', async () => {
1034
+ await store.set('user-2', { userId: 'user-2', role: 'admin' } as any)
1035
+ await store.set('user-2', { userId: 'user-2', role: 'member' } as any)
1036
+
1037
+ const result = await store.get('user-2')
1038
+ assert.deepEqual(result, { userId: 'user-2', role: 'member' })
1039
+ })
1040
+
1041
+ test('clear removes session', async () => {
1042
+ await store.set('user-3', { userId: 'user-3' } as any)
1043
+ assert.ok(await store.get('user-3'))
1044
+
1045
+ await store.clear('user-3')
1046
+ const result = await store.get('user-3')
1047
+ assert.equal(result, undefined)
1048
+ })
1049
+
1050
+ test('clear is no-op for unknown user', async () => {
1051
+ await store.clear('nonexistent')
1052
+ })
1053
+ })
1054
+ }
1002
1055
  }
@@ -35,6 +35,7 @@ import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.
35
35
  import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
36
36
  import type { CredentialService } from '../services/credential-service.js'
37
37
  import type { MetaService } from '../services/meta-service.js'
38
+ import type { SessionStore } from '../services/session-store.js'
38
39
 
39
40
  export type PikkuWiringTypes =
40
41
  | 'http'
@@ -236,6 +237,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
236
237
  credentialService?: CredentialService
237
238
  /** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
238
239
  metaService?: MetaService
240
+ /** Session store for persisting user sessions keyed by pikkuUserId */
241
+ sessionStore?: SessionStore
239
242
  }
240
243
 
241
244
  /**
@@ -1,5 +1,3 @@
1
- import type { CoreUserSession } from '../../types/core.types.js'
2
-
3
1
  export type Channel<ChannelType = unknown, OpeningData = unknown> = {
4
2
  channelId: string
5
3
  channelName: string
@@ -10,20 +8,19 @@ export type Channel<ChannelType = unknown, OpeningData = unknown> = {
10
8
  export abstract class ChannelStore<
11
9
  ChannelType = unknown,
12
10
  OpeningData = unknown,
13
- UserSession extends CoreUserSession = CoreUserSession,
14
11
  TypedChannel = Channel<ChannelType, OpeningData>,
15
12
  > {
16
13
  public abstract addChannel(
17
14
  channel: Channel<ChannelType, OpeningData>
18
15
  ): Promise<void> | void
19
16
  public abstract removeChannels(channelId: string[]): Promise<void> | void
20
- public abstract setUserSession(
17
+ public abstract setPikkuUserId(
21
18
  channelId: string,
22
- userSession: UserSession | null
19
+ pikkuUserId: string | null
23
20
  ): Promise<void> | void
24
- public abstract getChannelAndSession(
21
+ public abstract getChannel(
25
22
  channelId: string
26
23
  ):
27
- | Promise<TypedChannel & { session: UserSession }>
28
- | (TypedChannel & { session: UserSession })
24
+ | Promise<TypedChannel & { pikkuUserId?: string }>
25
+ | (TypedChannel & { pikkuUserId?: string })
29
26
  }
@@ -45,7 +45,7 @@ export const runLocalChannel = async ({
45
45
  let closedWireServices = false
46
46
 
47
47
  let channelHandler: PikkuLocalChannelHandler | undefined
48
- const userSession = new PikkuSessionService()
48
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
49
49
 
50
50
  let http: PikkuHTTP | undefined
51
51
  if (request) {
@@ -125,7 +125,9 @@ export const runLocalChannel = async ({
125
125
  singletonServices.logger.error(`Error handling onConnect: ${e}`)
126
126
  const errorResponse = getErrorResponse(e)
127
127
  channel.send({
128
- error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
128
+ error:
129
+ errorResponse?.message ??
130
+ (isProduction() ? 'Internal server error' : e.message),
129
131
  ...(!isProduction() && { errorName: e.constructor?.name }),
130
132
  })
131
133
  }
@@ -148,7 +150,9 @@ export const runLocalChannel = async ({
148
150
  singletonServices.logger.error(`Error handling onDisconnect: ${e}`)
149
151
  const errorResponse = getErrorResponse(e)
150
152
  channel.send({
151
- error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
153
+ error:
154
+ errorResponse?.message ??
155
+ (isProduction() ? 'Internal server error' : e.message),
152
156
  ...(!isProduction() && { errorName: e.constructor?.name }),
153
157
  })
154
158
  }
@@ -171,7 +175,9 @@ export const runLocalChannel = async ({
171
175
  singletonServices.logger.error(e)
172
176
  const errorResponse = getErrorResponse(e)
173
177
  channel.send({
174
- error: errorResponse?.message ?? (isProduction() ? 'Internal server error' : e.message),
178
+ error:
179
+ errorResponse?.message ??
180
+ (isProduction() ? 'Internal server error' : e.message),
175
181
  ...(!isProduction() && { errorName: e.constructor?.name }),
176
182
  })
177
183
  setTimeout(() => channel.close(), 200)
@@ -1,7 +1,6 @@
1
1
  import * as assert from 'assert'
2
2
  import { test } from 'node:test'
3
3
  import type { PikkuChannelHandler } from '../channel.types.js'
4
- import type { CoreUserSession } from '../../../types/core.types.js'
5
4
  import { LocalEventHubService } from './local-eventhub-service.js'
6
5
 
7
6
  class MockChannelHandler implements PikkuChannelHandler {
@@ -11,10 +10,6 @@ class MockChannelHandler implements PikkuChannelHandler {
11
10
  this.channelId = channelId
12
11
  }
13
12
 
14
- setUserSession(session: CoreUserSession): Promise<void> | void {
15
- throw new Error('Method not needed.')
16
- }
17
-
18
13
  getChannel() {
19
14
  return { channelId: this.channelId } as any
20
15
  }
@@ -89,7 +89,7 @@ export const runChannelConnect = async ({
89
89
  http = createHTTPWire(new PikkuFetchHTTPRequest(request), response)
90
90
  }
91
91
 
92
- const userSession = new PikkuSessionService(channelStore, channelId)
92
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
93
93
 
94
94
  const { channelConfig, openingData, meta } = await openChannel({
95
95
  channelId,
@@ -138,6 +138,13 @@ export const runChannelConnect = async ({
138
138
  channelMiddlewareMeta: meta.channelMiddleware,
139
139
  })
140
140
  }
141
+
142
+ // Store the pikkuUserId mapping after auth middleware has run
143
+ const pikkuUserId = userSession.getPikkuUserId()
144
+ if (pikkuUserId) {
145
+ await channelStore.setPikkuUserId(channelId, pikkuUserId)
146
+ }
147
+
141
148
  http?.response?.status(101)
142
149
  } catch (e: any) {
143
150
  handleHTTPError(
@@ -171,7 +178,7 @@ export const runChannelDisconnect = async ({
171
178
  // there's nothing to disconnect, so we can return early.
172
179
  let channelData
173
180
  try {
174
- channelData = await channelStore.getChannelAndSession(channelId)
181
+ channelData = await channelStore.getChannel(channelId)
175
182
  } catch {
176
183
  singletonServices.logger.info(
177
184
  `Channel ${channelId} not found during disconnect - already cleaned up`
@@ -179,7 +186,7 @@ export const runChannelDisconnect = async ({
179
186
  return
180
187
  }
181
188
 
182
- const { openingData, channelName, session } = channelData
189
+ const { openingData, channelName, pikkuUserId } = channelData
183
190
  const { channel, channelConfig, meta } = getVariablesForChannel({
184
191
  channelId,
185
192
  channelHandlerFactory,
@@ -187,8 +194,15 @@ export const runChannelDisconnect = async ({
187
194
  channelName,
188
195
  })
189
196
 
190
- const userSession = new PikkuSessionService(channelStore, channelId)
191
- userSession.setInitial(session)
197
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
198
+ if (pikkuUserId) {
199
+ userSession.setPikkuUserId(pikkuUserId)
200
+ const session = await singletonServices.sessionStore?.get(pikkuUserId)
201
+ if (session) {
202
+ userSession.setInitial(session)
203
+ }
204
+ }
205
+
192
206
  const wire: PikkuWire = {
193
207
  channel,
194
208
  ...createMiddlewareSessionWireProps(userSession),
@@ -237,8 +251,8 @@ export const runChannelMessage = async (
237
251
  const singletonServices = getSingletonServices()
238
252
  const createWireServices = getCreateWireServices()
239
253
  let wireServices: WireServices | undefined
240
- const { openingData, channelName, session } =
241
- await channelStore.getChannelAndSession(channelId)
254
+ const { openingData, channelName, pikkuUserId } =
255
+ await channelStore.getChannel(channelId)
242
256
 
243
257
  const { channel, channelHandler, channelConfig } = getVariablesForChannel({
244
258
  channelId,
@@ -247,8 +261,15 @@ export const runChannelMessage = async (
247
261
  channelName,
248
262
  })
249
263
 
250
- const userSession = new PikkuSessionService(channelStore, channelId)
251
- userSession.setInitial(session)
264
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
265
+ if (pikkuUserId) {
266
+ userSession.setPikkuUserId(pikkuUserId)
267
+ const session = await singletonServices.sessionStore?.get(pikkuUserId)
268
+ if (session) {
269
+ userSession.setInitial(session)
270
+ }
271
+ }
272
+
252
273
  const wire: PikkuWire = {
253
274
  channel,
254
275
  ...createMiddlewareSessionWireProps(userSession),
@@ -341,7 +341,9 @@ export async function runCLICommand({
341
341
  state: 'open',
342
342
  }
343
343
 
344
- const userSession = new PikkuSessionService<CoreUserSession>()
344
+ const userSession = new PikkuSessionService<CoreUserSession>(
345
+ singletonServices.sessionStore
346
+ )
345
347
 
346
348
  const wire: PikkuWire = {
347
349
  cli: {
@@ -303,10 +303,12 @@ const executeRoute = async (
303
303
  coerceDataFromSchema: boolean
304
304
  }
305
305
  ) => {
306
- const userSession = new PikkuSessionService<CoreUserSession>()
307
306
  const { params, route, meta } = matchedRoute
308
307
  const { singletonServices, createWireServices, skipUserSession, requestId } =
309
308
  services
309
+ const userSession = new PikkuSessionService<CoreUserSession>(
310
+ singletonServices.sessionStore
311
+ )
310
312
 
311
313
  // Attach URL parameters to the request object
312
314
  http?.request?.setParams(params)
@@ -214,7 +214,9 @@ async function runMCPPikkuFunc(
214
214
 
215
215
  singletonServices.logger.debug(`Running MCP ${type}: ${name}`)
216
216
 
217
- const mcpSessionService = new PikkuSessionService()
217
+ const mcpSessionService = new PikkuSessionService(
218
+ singletonServices.sessionStore
219
+ )
218
220
  const wire: PikkuWire = {
219
221
  mcp: mcpWire,
220
222
  ...createMiddlewareSessionWireProps(mcpSessionService),
@@ -82,7 +82,7 @@ export async function runScheduledTask({
82
82
  const task = pikkuState(null, 'scheduler', 'tasks').get(name)
83
83
  const meta = pikkuState(null, 'scheduler', 'meta')[name]
84
84
 
85
- const userSession = new PikkuSessionService()
85
+ const userSession = new PikkuSessionService(singletonServices.sessionStore)
86
86
  if (session) {
87
87
  userSession.set(session)
88
88
  }
@@ -831,7 +831,6 @@ export abstract class PikkuWorkflowService implements WorkflowService {
831
831
  const wire: PikkuWire = {
832
832
  workflow: workflowWire,
833
833
  pikkuUserId: run.wire?.pikkuUserId,
834
- session: rpcService.wire?.session,
835
834
  rpc: rpcService.wire?.rpc,
836
835
  }
837
836
  try {