@pikku/core 0.12.8 → 0.12.10
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 +31 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/errors/errors.d.ts +14 -0
- package/dist/errors/errors.js +27 -0
- package/dist/function/function-runner.d.ts +3 -1
- package/dist/function/function-runner.js +5 -1
- package/dist/handle-error.js +2 -1
- package/dist/middleware/auth-bearer.js +10 -1
- package/dist/middleware/auth-cookie.js +34 -25
- package/dist/middleware/timeout.js +11 -5
- package/dist/pikku-state.js +1 -1
- package/dist/services/credential-service.d.ts +40 -0
- package/dist/services/credential-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +13 -0
- package/dist/services/credential-wire-service.js +31 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +3 -0
- package/dist/services/local-content.d.ts +7 -4
- package/dist/services/local-content.js +42 -12
- package/dist/services/local-credential-service.d.ts +10 -0
- package/dist/services/local-credential-service.js +33 -0
- package/dist/services/local-secrets.js +2 -2
- package/dist/services/typed-credential-service.d.ts +27 -0
- package/dist/services/typed-credential-service.js +40 -0
- package/dist/testing/service-tests.d.ts +8 -0
- package/dist/testing/service-tests.js +107 -1
- package/dist/types/core.types.d.ts +7 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
- package/dist/wirings/channel/local/local-channel-runner.js +11 -6
- package/dist/wirings/credential/credential.types.d.ts +24 -0
- package/dist/wirings/credential/credential.types.js +1 -0
- package/dist/wirings/credential/index.d.ts +3 -0
- package/dist/wirings/credential/index.js +2 -0
- package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
- package/dist/wirings/credential/validate-credential-definitions.js +38 -0
- package/dist/wirings/credential/wire-credential.d.ts +48 -0
- package/dist/wirings/credential/wire-credential.js +47 -0
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
- package/dist/wirings/oauth2/index.d.ts +2 -1
- package/dist/wirings/oauth2/index.js +1 -1
- package/dist/wirings/oauth2/oauth2-client.js +4 -8
- package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
- package/dist/wirings/oauth2/oauth2-routes.js +146 -0
- package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
- package/dist/wirings/workflow/graph/graph-runner.d.ts +8 -1
- package/dist/wirings/workflow/graph/graph-runner.js +87 -7
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +99 -24
- package/dist/wirings/workflow/workflow.types.d.ts +4 -0
- package/package.json +2 -1
- package/src/env.ts +1 -0
- package/src/errors/errors.ts +35 -0
- package/src/function/function-runner.ts +11 -0
- package/src/handle-error.ts +2 -1
- package/src/middleware/auth-bearer.ts +10 -1
- package/src/middleware/auth-cookie.ts +11 -4
- package/src/middleware/timeout.ts +14 -7
- package/src/pikku-state.ts +1 -1
- package/src/services/credential-service.ts +44 -0
- package/src/services/credential-wire-service.ts +44 -0
- package/src/services/index.ts +12 -0
- package/src/services/local-content.ts +61 -13
- package/src/services/local-credential-service.ts +41 -0
- package/src/services/local-secrets.test.ts +2 -2
- package/src/services/local-secrets.ts +2 -2
- package/src/services/typed-credential-service.ts +75 -0
- package/src/testing/service-tests.ts +140 -1
- package/src/types/core.types.ts +7 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
- package/src/wirings/channel/local/local-channel-runner.ts +11 -6
- package/src/wirings/credential/credential.types.ts +28 -0
- package/src/wirings/credential/index.ts +8 -0
- package/src/wirings/credential/validate-credential-definitions.ts +64 -0
- package/src/wirings/credential/wire-credential.ts +49 -0
- package/src/wirings/http/http-runner.ts +11 -2
- package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
- package/src/wirings/oauth2/index.ts +2 -1
- package/src/wirings/oauth2/oauth2-client.ts +4 -8
- package/src/wirings/oauth2/oauth2-routes.ts +234 -0
- package/src/wirings/oauth2/oauth2.types.ts +0 -27
- package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
- package/src/wirings/workflow/graph/graph-runner.ts +117 -8
- package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +150 -34
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
- package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
- package/src/wirings/oauth2/wire-oauth2-credential.ts +0 -23
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createReadStream, createWriteStream, promises } from 'fs'
|
|
2
2
|
import { mkdir, readFile } from 'fs/promises'
|
|
3
|
-
import
|
|
3
|
+
import { resolve, normalize } from 'path'
|
|
4
|
+
import type { ContentService, JWTService, Logger } from '@pikku/core/services'
|
|
4
5
|
import { pipeline } from 'stream/promises'
|
|
5
6
|
import type { Readable } from 'stream'
|
|
6
7
|
|
|
@@ -15,19 +16,66 @@ export interface LocalContentConfig {
|
|
|
15
16
|
export class LocalContent implements ContentService {
|
|
16
17
|
constructor(
|
|
17
18
|
private config: LocalContentConfig,
|
|
18
|
-
private logger: Logger
|
|
19
|
+
private logger: Logger,
|
|
20
|
+
private jwt?: JWTService
|
|
19
21
|
) {}
|
|
20
22
|
|
|
23
|
+
private safePath(assetKey: string): string {
|
|
24
|
+
const base = resolve(this.config.localFileUploadPath)
|
|
25
|
+
const target = resolve(base, normalize(assetKey))
|
|
26
|
+
if (!target.startsWith(base + '/') && target !== base) {
|
|
27
|
+
throw new Error('Invalid asset key')
|
|
28
|
+
}
|
|
29
|
+
return target
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
public async init() {}
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
private async signParams(
|
|
35
|
+
dateLessThan: Date,
|
|
36
|
+
dateGreaterThan?: Date
|
|
37
|
+
): Promise<string> {
|
|
38
|
+
const signedAt = Date.now()
|
|
39
|
+
const expiresAt = dateLessThan.getTime()
|
|
40
|
+
const params = new URLSearchParams({
|
|
41
|
+
signedAt: String(signedAt),
|
|
42
|
+
expiresAt: String(expiresAt),
|
|
43
|
+
})
|
|
44
|
+
if (dateGreaterThan) {
|
|
45
|
+
params.set('notBefore', String(dateGreaterThan.getTime()))
|
|
46
|
+
}
|
|
47
|
+
if (this.jwt) {
|
|
48
|
+
const expiresInSeconds = Math.max(
|
|
49
|
+
1,
|
|
50
|
+
Math.floor((expiresAt - signedAt) / 1000)
|
|
51
|
+
)
|
|
52
|
+
const signature = await this.jwt.encode(
|
|
53
|
+
{ value: expiresInSeconds, unit: 'second' },
|
|
54
|
+
{ signedAt, expiresAt }
|
|
55
|
+
)
|
|
56
|
+
params.set('signature', signature)
|
|
57
|
+
}
|
|
58
|
+
return params.toString()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async signURL(
|
|
62
|
+
url: string,
|
|
63
|
+
dateLessThan: Date,
|
|
64
|
+
dateGreaterThan?: Date
|
|
65
|
+
): Promise<string> {
|
|
66
|
+
const params = await this.signParams(dateLessThan, dateGreaterThan)
|
|
67
|
+
return `${url}?${params}`
|
|
25
68
|
}
|
|
26
69
|
|
|
27
|
-
public async signContentKey(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
70
|
+
public async signContentKey(
|
|
71
|
+
assetKey: string,
|
|
72
|
+
dateLessThan: Date,
|
|
73
|
+
dateGreaterThan?: Date
|
|
74
|
+
): Promise<string> {
|
|
75
|
+
const base = this.config.server
|
|
76
|
+
? `${this.config.server}${this.config.assetUrlPrefix}/${assetKey}`
|
|
77
|
+
: `${this.config.assetUrlPrefix}/${assetKey}`
|
|
78
|
+
return this.signURL(base, dateLessThan, dateGreaterThan)
|
|
31
79
|
}
|
|
32
80
|
|
|
33
81
|
public async getUploadURL(assetKey: string) {
|
|
@@ -44,7 +92,7 @@ export class LocalContent implements ContentService {
|
|
|
44
92
|
): Promise<boolean> {
|
|
45
93
|
this.logger.debug(`Writing file: ${assetKey}`)
|
|
46
94
|
|
|
47
|
-
const path =
|
|
95
|
+
const path = this.safePath(assetKey)
|
|
48
96
|
|
|
49
97
|
try {
|
|
50
98
|
await this.createDirectoryForFile(path)
|
|
@@ -65,7 +113,7 @@ export class LocalContent implements ContentService {
|
|
|
65
113
|
): Promise<boolean> {
|
|
66
114
|
this.logger.debug(`Writing file: ${assetKey}`)
|
|
67
115
|
try {
|
|
68
|
-
const path =
|
|
116
|
+
const path = this.safePath(assetKey)
|
|
69
117
|
await this.createDirectoryForFile(path)
|
|
70
118
|
await promises.copyFile(fromAbsolutePath, path)
|
|
71
119
|
} catch (e) {
|
|
@@ -80,7 +128,7 @@ export class LocalContent implements ContentService {
|
|
|
80
128
|
): Promise<ReadableStream | NodeJS.ReadableStream> {
|
|
81
129
|
this.logger.debug(`Getting key: ${assetKey}`)
|
|
82
130
|
|
|
83
|
-
const filePath =
|
|
131
|
+
const filePath = this.safePath(assetKey)
|
|
84
132
|
|
|
85
133
|
try {
|
|
86
134
|
const stream = createReadStream(filePath)
|
|
@@ -97,7 +145,7 @@ export class LocalContent implements ContentService {
|
|
|
97
145
|
}
|
|
98
146
|
|
|
99
147
|
public async readFileAsBuffer(assetKey: string): Promise<Buffer> {
|
|
100
|
-
const filePath =
|
|
148
|
+
const filePath = this.safePath(assetKey)
|
|
101
149
|
this.logger.debug(`Reading file as buffer: ${assetKey}`)
|
|
102
150
|
return readFile(filePath)
|
|
103
151
|
}
|
|
@@ -105,7 +153,7 @@ export class LocalContent implements ContentService {
|
|
|
105
153
|
public async deleteFile(assetKey: string): Promise<boolean> {
|
|
106
154
|
this.logger.debug(`deleting key: ${assetKey}`)
|
|
107
155
|
try {
|
|
108
|
-
await promises.unlink(
|
|
156
|
+
await promises.unlink(this.safePath(assetKey))
|
|
109
157
|
return true
|
|
110
158
|
} catch (e: any) {
|
|
111
159
|
this.logger.error(`Error deleting content ${assetKey}`, e)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CredentialService } from './credential-service.js'
|
|
2
|
+
|
|
3
|
+
export class LocalCredentialService implements CredentialService {
|
|
4
|
+
private store: Map<string, unknown> = new Map()
|
|
5
|
+
|
|
6
|
+
private makeKey(name: string, userId?: string): string {
|
|
7
|
+
return userId ? `${userId}:${name}` : name
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async get<T = unknown>(name: string, userId?: string): Promise<T | null> {
|
|
11
|
+
const key = this.makeKey(name, userId)
|
|
12
|
+
const value = this.store.get(key)
|
|
13
|
+
return (value as T) ?? null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async set(name: string, value: unknown, userId?: string): Promise<void> {
|
|
17
|
+
const key = this.makeKey(name, userId)
|
|
18
|
+
this.store.set(key, value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async delete(name: string, userId?: string): Promise<void> {
|
|
22
|
+
const key = this.makeKey(name, userId)
|
|
23
|
+
this.store.delete(key)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async has(name: string, userId?: string): Promise<boolean> {
|
|
27
|
+
const key = this.makeKey(name, userId)
|
|
28
|
+
return this.store.has(key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getAll(userId: string): Promise<Record<string, unknown>> {
|
|
32
|
+
const prefix = `${userId}:`
|
|
33
|
+
const result: Record<string, unknown> = {}
|
|
34
|
+
for (const [key, value] of this.store) {
|
|
35
|
+
if (key.startsWith(prefix)) {
|
|
36
|
+
result[key.slice(prefix.length)] = value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -22,7 +22,7 @@ describe('LocalSecretService', () => {
|
|
|
22
22
|
const vars = new LocalVariablesService({})
|
|
23
23
|
const service = new LocalSecretService(vars)
|
|
24
24
|
await assert.rejects(() => service.getSecret('MISSING'), {
|
|
25
|
-
message: '
|
|
25
|
+
message: 'Requested secret not found',
|
|
26
26
|
})
|
|
27
27
|
})
|
|
28
28
|
|
|
@@ -44,7 +44,7 @@ describe('LocalSecretService', () => {
|
|
|
44
44
|
const vars = new LocalVariablesService({})
|
|
45
45
|
const service = new LocalSecretService(vars)
|
|
46
46
|
await assert.rejects(() => service.getSecretJSON('MISSING'), {
|
|
47
|
-
message: '
|
|
47
|
+
message: 'Requested secret not found',
|
|
48
48
|
})
|
|
49
49
|
})
|
|
50
50
|
|
|
@@ -35,7 +35,7 @@ export class LocalSecretService implements SecretService {
|
|
|
35
35
|
if (value) {
|
|
36
36
|
return JSON.parse(value)
|
|
37
37
|
}
|
|
38
|
-
throw new Error(
|
|
38
|
+
throw new Error('Requested secret not found')
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -57,7 +57,7 @@ export class LocalSecretService implements SecretService {
|
|
|
57
57
|
if (value) {
|
|
58
58
|
return value
|
|
59
59
|
}
|
|
60
|
-
throw new Error(
|
|
60
|
+
throw new Error('Requested secret not found')
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CredentialService } from './credential-service.js'
|
|
2
|
+
|
|
3
|
+
export interface CredentialStatusInfo {
|
|
4
|
+
name: string
|
|
5
|
+
displayName: string
|
|
6
|
+
isConfigured: boolean
|
|
7
|
+
type: 'singleton' | 'wire'
|
|
8
|
+
oauth2?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type CredentialMetaInfo = {
|
|
12
|
+
name: string
|
|
13
|
+
displayName: string
|
|
14
|
+
type: 'singleton' | 'wire'
|
|
15
|
+
oauth2?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class TypedCredentialService<TMap = Record<string, unknown>>
|
|
19
|
+
implements CredentialService
|
|
20
|
+
{
|
|
21
|
+
constructor(
|
|
22
|
+
private credentials: CredentialService,
|
|
23
|
+
private credentialsMeta: Record<string, CredentialMetaInfo>
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async get<K extends keyof TMap & string>(
|
|
27
|
+
name: K,
|
|
28
|
+
userId?: string
|
|
29
|
+
): Promise<TMap[K] | null>
|
|
30
|
+
async get<T = unknown>(name: string, userId?: string): Promise<T | null>
|
|
31
|
+
async get(name: string, userId?: string): Promise<unknown> {
|
|
32
|
+
return this.credentials.get(name, userId)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async set<K extends string>(
|
|
36
|
+
name: K,
|
|
37
|
+
value: K extends keyof TMap ? TMap[K] : unknown,
|
|
38
|
+
userId?: string
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
return this.credentials.set(name, value, userId)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async delete(name: string, userId?: string): Promise<void> {
|
|
44
|
+
return this.credentials.delete(name, userId)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async has(name: string, userId?: string): Promise<boolean> {
|
|
48
|
+
return this.credentials.has(name, userId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getAll(userId: string): Promise<Record<string, unknown>> {
|
|
52
|
+
return this.credentials.getAll(userId)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getAllStatus(userId?: string): Promise<CredentialStatusInfo[]> {
|
|
56
|
+
const results: CredentialStatusInfo[] = []
|
|
57
|
+
|
|
58
|
+
for (const [name, meta] of Object.entries(this.credentialsMeta)) {
|
|
59
|
+
results.push({
|
|
60
|
+
name,
|
|
61
|
+
displayName: meta.displayName,
|
|
62
|
+
isConfigured: await this.credentials.has(name, userId),
|
|
63
|
+
type: meta.type,
|
|
64
|
+
oauth2: meta.oauth2,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return results
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getMissing(userId?: string): Promise<CredentialStatusInfo[]> {
|
|
72
|
+
const all = await this.getAllStatus(userId)
|
|
73
|
+
return all.filter((c) => !c.isConfigured)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { DeploymentService } from '../services/deployment-service.js'
|
|
|
9
9
|
import type { AIStorageService } from '../services/ai-storage-service.js'
|
|
10
10
|
import type { AIRunStateService } from '../services/ai-run-state-service.js'
|
|
11
11
|
import type { SecretService } from '../services/secret-service.js'
|
|
12
|
+
import type { CredentialService } from '../services/credential-service.js'
|
|
12
13
|
import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
|
|
13
14
|
|
|
14
15
|
export interface ServiceTestConfig {
|
|
@@ -28,6 +29,11 @@ export interface ServiceTestConfig {
|
|
|
28
29
|
keyVersion?: number
|
|
29
30
|
previousKey?: string
|
|
30
31
|
}) => Promise<SecretService & { rotateKEK?(): Promise<number> }>
|
|
32
|
+
credentialService?: (config: {
|
|
33
|
+
key: string
|
|
34
|
+
keyVersion?: number
|
|
35
|
+
previousKey?: string
|
|
36
|
+
}) => Promise<CredentialService & { rotateKEK?(): Promise<number> }>
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -780,7 +786,7 @@ export function defineServiceTests(config: ServiceTestConfig): void {
|
|
|
780
786
|
test('getSecret throws for missing key', async () => {
|
|
781
787
|
const service = await factory({ key: kek })
|
|
782
788
|
await assert.rejects(() => service.getSecret('nonexistent'), {
|
|
783
|
-
message: '
|
|
789
|
+
message: 'Requested secret not found',
|
|
784
790
|
})
|
|
785
791
|
})
|
|
786
792
|
|
|
@@ -840,6 +846,139 @@ export function defineServiceTests(config: ServiceTestConfig): void {
|
|
|
840
846
|
})
|
|
841
847
|
}
|
|
842
848
|
|
|
849
|
+
if (services.credentialService) {
|
|
850
|
+
const factory = services.credentialService
|
|
851
|
+
const kek = 'test-key-encryption-key-32chars!'
|
|
852
|
+
|
|
853
|
+
describe(`CredentialService [${name}]`, () => {
|
|
854
|
+
test('set and get (global)', async () => {
|
|
855
|
+
const service = await factory({ key: kek })
|
|
856
|
+
await service.set('stripe', { apiKey: 'sk-123' })
|
|
857
|
+
const result = await service.get<{ apiKey: string }>('stripe')
|
|
858
|
+
assert.deepEqual(result, { apiKey: 'sk-123' })
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
test('get returns null for missing', async () => {
|
|
862
|
+
const service = await factory({ key: kek })
|
|
863
|
+
const result = await service.get('nonexistent')
|
|
864
|
+
assert.equal(result, null)
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
test('has returns true/false', async () => {
|
|
868
|
+
const service = await factory({ key: kek })
|
|
869
|
+
await service.set('exists', { key: 'val' })
|
|
870
|
+
assert.strictEqual(await service.has('exists'), true)
|
|
871
|
+
assert.strictEqual(await service.has('nope'), false)
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
test('set upserts existing credential', async () => {
|
|
875
|
+
const service = await factory({ key: kek })
|
|
876
|
+
await service.set('upsert', { v: 1 })
|
|
877
|
+
await service.set('upsert', { v: 2 })
|
|
878
|
+
const result = await service.get<{ v: number }>('upsert')
|
|
879
|
+
assert.deepEqual(result, { v: 2 })
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
test('delete removes credential', async () => {
|
|
883
|
+
const service = await factory({ key: kek })
|
|
884
|
+
await service.set('to-delete', { bye: true })
|
|
885
|
+
assert.strictEqual(await service.has('to-delete'), true)
|
|
886
|
+
await service.delete('to-delete')
|
|
887
|
+
assert.strictEqual(await service.has('to-delete'), false)
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
test('per-user isolation', async () => {
|
|
891
|
+
const service = await factory({ key: kek })
|
|
892
|
+
await service.set('token', { access: 'user1' }, 'user-1')
|
|
893
|
+
await service.set('token', { access: 'user2' }, 'user-2')
|
|
894
|
+
|
|
895
|
+
const u1 = await service.get<{ access: string }>('token', 'user-1')
|
|
896
|
+
const u2 = await service.get<{ access: string }>('token', 'user-2')
|
|
897
|
+
assert.deepEqual(u1, { access: 'user1' })
|
|
898
|
+
assert.deepEqual(u2, { access: 'user2' })
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
test('per-user delete does not affect other users', async () => {
|
|
902
|
+
const service = await factory({ key: kek })
|
|
903
|
+
await service.set('cred', { val: 'a' }, 'user-a')
|
|
904
|
+
await service.set('cred', { val: 'b' }, 'user-b')
|
|
905
|
+
|
|
906
|
+
await service.delete('cred', 'user-a')
|
|
907
|
+
assert.strictEqual(await service.has('cred', 'user-a'), false)
|
|
908
|
+
assert.strictEqual(await service.has('cred', 'user-b'), true)
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
test('global and per-user are separate', async () => {
|
|
912
|
+
const service = await factory({ key: kek })
|
|
913
|
+
await service.set('shared', { scope: 'global' })
|
|
914
|
+
await service.set('shared', { scope: 'user' }, 'user-1')
|
|
915
|
+
|
|
916
|
+
const global = await service.get<{ scope: string }>('shared')
|
|
917
|
+
const perUser = await service.get<{ scope: string }>('shared', 'user-1')
|
|
918
|
+
assert.deepEqual(global, { scope: 'global' })
|
|
919
|
+
assert.deepEqual(perUser, { scope: 'user' })
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
test('getAll returns all credentials for a user', async () => {
|
|
923
|
+
const service = await factory({ key: kek })
|
|
924
|
+
await service.set('cred-a', { a: 1 }, 'user-x')
|
|
925
|
+
await service.set('cred-b', { b: 2 }, 'user-x')
|
|
926
|
+
await service.set('cred-c', { c: 3 }, 'user-y')
|
|
927
|
+
|
|
928
|
+
const all = await service.getAll('user-x')
|
|
929
|
+
assert.deepEqual(all['cred-a'], { a: 1 })
|
|
930
|
+
assert.deepEqual(all['cred-b'], { b: 2 })
|
|
931
|
+
assert.strictEqual(all['cred-c'], undefined)
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
test('getAll returns empty for unknown user', async () => {
|
|
935
|
+
const service = await factory({ key: kek })
|
|
936
|
+
const all = await service.getAll('nobody')
|
|
937
|
+
assert.deepEqual(all, {})
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
test('rotateKEK re-wraps all credentials', async () => {
|
|
941
|
+
const newKEK = 'new-key-encryption-key-rotated!'
|
|
942
|
+
const oldService = await factory({ key: kek })
|
|
943
|
+
await oldService.set('rotate-cred', { important: 'data' }, 'user-r')
|
|
944
|
+
|
|
945
|
+
const rotatedService = await factory({
|
|
946
|
+
key: newKEK,
|
|
947
|
+
keyVersion: 2,
|
|
948
|
+
previousKey: kek,
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
const before = await rotatedService.get<{ important: string }>(
|
|
952
|
+
'rotate-cred',
|
|
953
|
+
'user-r'
|
|
954
|
+
)
|
|
955
|
+
assert.deepEqual(before, { important: 'data' })
|
|
956
|
+
|
|
957
|
+
assert.ok(rotatedService.rotateKEK)
|
|
958
|
+
const count = await rotatedService.rotateKEK!()
|
|
959
|
+
assert.ok(count > 0)
|
|
960
|
+
|
|
961
|
+
const newOnlyService = await factory({
|
|
962
|
+
key: newKEK,
|
|
963
|
+
keyVersion: 2,
|
|
964
|
+
})
|
|
965
|
+
const after = await newOnlyService.get<{ important: string }>(
|
|
966
|
+
'rotate-cred',
|
|
967
|
+
'user-r'
|
|
968
|
+
)
|
|
969
|
+
assert.deepEqual(after, { important: 'data' })
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
test('rotateKEK throws without previousKey', async () => {
|
|
973
|
+
const service = await factory({ key: kek })
|
|
974
|
+
assert.ok(service.rotateKEK)
|
|
975
|
+
await assert.rejects(() => service.rotateKEK!(), {
|
|
976
|
+
message: 'No previousKey configured — nothing to rotate from',
|
|
977
|
+
})
|
|
978
|
+
})
|
|
979
|
+
})
|
|
980
|
+
}
|
|
981
|
+
|
|
843
982
|
if (services.agentRunService) {
|
|
844
983
|
const factory = services.agentRunService
|
|
845
984
|
describe(`AgentRunService [${name}]`, () => {
|
package/src/types/core.types.ts
CHANGED
|
@@ -33,6 +33,7 @@ import type { AIRunStateService } from '../services/ai-run-state-service.js'
|
|
|
33
33
|
import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
|
|
34
34
|
import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
|
|
35
35
|
import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
|
|
36
|
+
import type { CredentialService } from '../services/credential-service.js'
|
|
36
37
|
|
|
37
38
|
export type PikkuWiringTypes =
|
|
38
39
|
| 'http'
|
|
@@ -227,6 +228,8 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
227
228
|
agentRunService?: AgentRunService
|
|
228
229
|
/** Workflow run service (listing workflow runs) */
|
|
229
230
|
workflowRunService?: WorkflowRunService
|
|
231
|
+
/** Credential service for dynamic/managed credentials (OAuth tokens, per-user API keys) */
|
|
232
|
+
credentialService?: CredentialService
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
/**
|
|
@@ -270,6 +273,10 @@ export type PikkuWire<
|
|
|
270
273
|
getSession: () => Promise<UserSession> | UserSession | undefined
|
|
271
274
|
/** Whether the session was modified during this run */
|
|
272
275
|
hasSessionChanged: () => boolean
|
|
276
|
+
/** Set a credential value (available in middleware) */
|
|
277
|
+
setCredential: (name: string, value: unknown) => void
|
|
278
|
+
/** Get all resolved credentials (only available in pikkuWireServices) */
|
|
279
|
+
getCredentials: () => Record<string, unknown>
|
|
273
280
|
}>
|
|
274
281
|
|
|
275
282
|
/**
|