@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.
- package/CHANGELOG.md +11 -0
- package/dist/function/function-runner.js +22 -1
- package/dist/services/in-memory-session-store.d.ts +8 -0
- package/dist/services/in-memory-session-store.js +12 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +1 -0
- package/dist/services/session-store.d.ts +6 -0
- package/dist/services/session-store.js +1 -0
- package/dist/services/user-session-service.d.ts +12 -10
- package/dist/services/user-session-service.js +18 -21
- package/dist/testing/service-tests.d.ts +2 -0
- package/dist/testing/service-tests.js +50 -11
- package/dist/types/core.types.d.ts +3 -0
- package/dist/wirings/channel/channel-store.d.ts +5 -6
- package/dist/wirings/channel/local/local-channel-runner.js +7 -4
- package/dist/wirings/channel/serverless/serverless-channel-runner.js +25 -8
- package/dist/wirings/cli/cli-runner.js +1 -1
- package/dist/wirings/http/http-runner.js +1 -1
- package/dist/wirings/mcp/mcp-runner.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +1 -1
- package/dist/wirings/workflow/pikku-workflow-service.js +0 -1
- package/package.json +1 -1
- package/src/function/function-runner.ts +36 -1
- package/src/services/in-memory-session-store.test.ts +58 -0
- package/src/services/in-memory-session-store.ts +21 -0
- package/src/services/index.ts +2 -0
- package/src/services/session-store.ts +9 -0
- package/src/services/user-session-service.test.ts +53 -19
- package/src/services/user-session-service.ts +21 -22
- package/src/testing/service-tests.ts +64 -11
- package/src/types/core.types.ts +3 -0
- package/src/wirings/channel/channel-store.ts +5 -8
- package/src/wirings/channel/local/local-channel-runner.ts +10 -4
- package/src/wirings/channel/local/local-eventhub-service.test.ts +0 -5
- package/src/wirings/channel/serverless/serverless-channel-runner.ts +30 -9
- package/src/wirings/cli/cli-runner.ts +3 -1
- package/src/wirings/http/http-runner.ts +3 -1
- package/src/wirings/mcp/mcp-runner.ts +3 -1
- package/src/wirings/scheduler/scheduler-runner.ts +1 -1
- package/src/wirings/workflow/pikku-workflow-service.ts +0 -1
- 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
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -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
|
|
54
|
-
|
|
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
|
-
|
|
57
|
+
get: async () => undefined,
|
|
58
|
+
set: async (id: string, session: any) => {
|
|
59
|
+
storedId = id
|
|
63
60
|
storedSession = session
|
|
64
61
|
},
|
|
65
|
-
|
|
62
|
+
clear: async () => {},
|
|
66
63
|
}
|
|
67
|
-
const service = new PikkuSessionService(store as any
|
|
68
|
-
service.
|
|
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
|
-
|
|
72
|
-
|
|
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():
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
if (this.sessionStore && this.pikkuUserId) {
|
|
55
|
+
await this.sessionStore.clear(this.pikkuUserId)
|
|
56
|
+
}
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
public get():
|
|
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
|
|
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.
|
|
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.
|
|
65
|
+
assert.equal(result.pikkuUserId, undefined)
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
test('
|
|
67
|
-
|
|
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.
|
|
71
|
-
assert.
|
|
71
|
+
const result = await store.getChannel('ch-1')
|
|
72
|
+
assert.equal(result.pikkuUserId, 'user-1')
|
|
72
73
|
})
|
|
73
74
|
|
|
74
|
-
test('
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/types/core.types.ts
CHANGED
|
@@ -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
|
|
17
|
+
public abstract setPikkuUserId(
|
|
21
18
|
channelId: string,
|
|
22
|
-
|
|
19
|
+
pikkuUserId: string | null
|
|
23
20
|
): Promise<void> | void
|
|
24
|
-
public abstract
|
|
21
|
+
public abstract getChannel(
|
|
25
22
|
channelId: string
|
|
26
23
|
):
|
|
27
|
-
| Promise<TypedChannel & {
|
|
28
|
-
| (TypedChannel & {
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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.
|
|
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,
|
|
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(
|
|
191
|
-
|
|
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,
|
|
241
|
-
await channelStore.
|
|
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(
|
|
251
|
-
|
|
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 {
|