@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.
Files changed (98) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/env.d.ts +1 -0
  3. package/dist/env.js +1 -0
  4. package/dist/errors/errors.d.ts +14 -0
  5. package/dist/errors/errors.js +27 -0
  6. package/dist/function/function-runner.d.ts +3 -1
  7. package/dist/function/function-runner.js +5 -1
  8. package/dist/handle-error.js +2 -1
  9. package/dist/middleware/auth-bearer.js +10 -1
  10. package/dist/middleware/auth-cookie.js +34 -25
  11. package/dist/middleware/timeout.js +11 -5
  12. package/dist/pikku-state.js +1 -1
  13. package/dist/services/credential-service.d.ts +40 -0
  14. package/dist/services/credential-service.js +1 -0
  15. package/dist/services/credential-wire-service.d.ts +13 -0
  16. package/dist/services/credential-wire-service.js +31 -0
  17. package/dist/services/index.d.ts +5 -0
  18. package/dist/services/index.js +3 -0
  19. package/dist/services/local-content.d.ts +7 -4
  20. package/dist/services/local-content.js +42 -12
  21. package/dist/services/local-credential-service.d.ts +10 -0
  22. package/dist/services/local-credential-service.js +33 -0
  23. package/dist/services/local-secrets.js +2 -2
  24. package/dist/services/typed-credential-service.d.ts +27 -0
  25. package/dist/services/typed-credential-service.js +40 -0
  26. package/dist/testing/service-tests.d.ts +8 -0
  27. package/dist/testing/service-tests.js +107 -1
  28. package/dist/types/core.types.d.ts +7 -0
  29. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
  30. package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
  31. package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
  32. package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
  33. package/dist/wirings/channel/local/local-channel-runner.js +11 -6
  34. package/dist/wirings/credential/credential.types.d.ts +24 -0
  35. package/dist/wirings/credential/credential.types.js +1 -0
  36. package/dist/wirings/credential/index.d.ts +3 -0
  37. package/dist/wirings/credential/index.js +2 -0
  38. package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
  39. package/dist/wirings/credential/validate-credential-definitions.js +38 -0
  40. package/dist/wirings/credential/wire-credential.d.ts +48 -0
  41. package/dist/wirings/credential/wire-credential.js +47 -0
  42. package/dist/wirings/http/http-runner.js +8 -2
  43. package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
  44. package/dist/wirings/oauth2/index.d.ts +2 -1
  45. package/dist/wirings/oauth2/index.js +1 -1
  46. package/dist/wirings/oauth2/oauth2-client.js +4 -8
  47. package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
  48. package/dist/wirings/oauth2/oauth2-routes.js +146 -0
  49. package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
  50. package/dist/wirings/workflow/graph/graph-runner.d.ts +8 -1
  51. package/dist/wirings/workflow/graph/graph-runner.js +87 -7
  52. package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
  53. package/dist/wirings/workflow/pikku-workflow-service.js +99 -24
  54. package/dist/wirings/workflow/workflow.types.d.ts +4 -0
  55. package/package.json +2 -1
  56. package/src/env.ts +1 -0
  57. package/src/errors/errors.ts +35 -0
  58. package/src/function/function-runner.ts +11 -0
  59. package/src/handle-error.ts +2 -1
  60. package/src/middleware/auth-bearer.ts +10 -1
  61. package/src/middleware/auth-cookie.ts +11 -4
  62. package/src/middleware/timeout.ts +14 -7
  63. package/src/pikku-state.ts +1 -1
  64. package/src/services/credential-service.ts +44 -0
  65. package/src/services/credential-wire-service.ts +44 -0
  66. package/src/services/index.ts +12 -0
  67. package/src/services/local-content.ts +61 -13
  68. package/src/services/local-credential-service.ts +41 -0
  69. package/src/services/local-secrets.test.ts +2 -2
  70. package/src/services/local-secrets.ts +2 -2
  71. package/src/services/typed-credential-service.ts +75 -0
  72. package/src/testing/service-tests.ts +140 -1
  73. package/src/types/core.types.ts +7 -0
  74. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
  75. package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
  76. package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
  77. package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
  78. package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
  79. package/src/wirings/channel/local/local-channel-runner.ts +11 -6
  80. package/src/wirings/credential/credential.types.ts +28 -0
  81. package/src/wirings/credential/index.ts +8 -0
  82. package/src/wirings/credential/validate-credential-definitions.ts +64 -0
  83. package/src/wirings/credential/wire-credential.ts +49 -0
  84. package/src/wirings/http/http-runner.ts +11 -2
  85. package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
  86. package/src/wirings/oauth2/index.ts +2 -1
  87. package/src/wirings/oauth2/oauth2-client.ts +4 -8
  88. package/src/wirings/oauth2/oauth2-routes.ts +234 -0
  89. package/src/wirings/oauth2/oauth2.types.ts +0 -27
  90. package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
  91. package/src/wirings/workflow/graph/graph-runner.ts +117 -8
  92. package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
  93. package/src/wirings/workflow/pikku-workflow-service.ts +150 -34
  94. package/src/wirings/workflow/workflow.types.ts +4 -0
  95. package/tsconfig.tsbuildinfo +1 -1
  96. package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
  97. package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
  98. 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 type { ContentService, Logger } from '@pikku/core/services'
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
- public async signURL(url: string): Promise<string> {
24
- return `${url}?signed=true`
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(assetKey: string): Promise<string> {
28
- return this.config.server
29
- ? `${this.config.server}${this.config.assetUrlPrefix}/${assetKey}?signed=true`
30
- : `${this.config.assetUrlPrefix}/${assetKey}?signed=true`
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 = `${this.config.localFileUploadPath}/${assetKey}`
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 = `${this.config.localFileUploadPath}/${assetKey}`
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 = `${this.config.localFileUploadPath}/${assetKey}`
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 = `${this.config.localFileUploadPath}/${assetKey}`
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(`${this.config.localFileUploadPath}/${assetKey}`)
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: 'Secret Not Found: MISSING',
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: 'Secret Not Found: MISSING',
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(`Secret Not Found: ${key}`)
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(`Secret Not Found: ${key}`)
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: 'Secret not found: nonexistent',
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}]`, () => {
@@ -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
  /**