@pikku/core 0.12.23 → 0.12.25

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 (90) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/data-classification.d.ts +16 -0
  3. package/dist/data-classification.js +1 -0
  4. package/dist/function/function-runner.js +62 -15
  5. package/dist/function/functions.types.d.ts +8 -5
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +1 -0
  8. package/dist/middleware-runner.d.ts +2 -2
  9. package/dist/permissions.d.ts +1 -1
  10. package/dist/services/ai-agent-runner-service.d.ts +3 -0
  11. package/dist/services/audit-service.d.ts +50 -0
  12. package/dist/services/audit-service.js +106 -0
  13. package/dist/services/content-service.d.ts +1 -0
  14. package/dist/services/email-service.d.ts +36 -0
  15. package/dist/services/email-service.js +1 -0
  16. package/dist/services/gopass-secrets.d.ts +2 -35
  17. package/dist/services/gopass-secrets.js +10 -40
  18. package/dist/services/index.d.ts +5 -1
  19. package/dist/services/index.js +2 -0
  20. package/dist/services/local-content.js +1 -0
  21. package/dist/services/local-email-service.d.ts +4 -0
  22. package/dist/services/local-email-service.js +26 -0
  23. package/dist/services/local-secrets.d.ts +3 -36
  24. package/dist/services/local-secrets.js +12 -52
  25. package/dist/services/local-variables.d.ts +3 -5
  26. package/dist/services/local-variables.js +10 -10
  27. package/dist/services/meta-service.d.ts +36 -0
  28. package/dist/services/meta-service.js +59 -0
  29. package/dist/services/scoped-secret-service.d.ts +2 -3
  30. package/dist/services/scoped-secret-service.js +2 -6
  31. package/dist/services/secret-service.d.ts +5 -12
  32. package/dist/services/typed-secret-service.d.ts +3 -4
  33. package/dist/services/typed-secret-service.js +2 -5
  34. package/dist/services/typed-variables-service.d.ts +3 -6
  35. package/dist/services/typed-variables-service.js +0 -6
  36. package/dist/services/variables-service.d.ts +2 -4
  37. package/dist/testing/service-tests.js +13 -13
  38. package/dist/types/core.types.d.ts +15 -1
  39. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +2 -0
  40. package/dist/wirings/ai-agent/ai-agent-prepare.js +12 -3
  41. package/dist/wirings/ai-agent/ai-agent-runner.js +1 -1
  42. package/dist/wirings/ai-agent/ai-agent-stream.js +1 -1
  43. package/dist/wirings/ai-agent/ai-agent-utils.d.ts +1 -0
  44. package/dist/wirings/ai-agent/ai-agent-utils.js +1 -0
  45. package/dist/wirings/ai-agent/ai-agent.types.d.ts +4 -0
  46. package/dist/wirings/oauth2/oauth2-client.js +3 -3
  47. package/dist/wirings/rpc/rpc-runner.js +9 -2
  48. package/package.json +1 -1
  49. package/src/data-classification.ts +15 -0
  50. package/src/function/function-runner.test.ts +255 -0
  51. package/src/function/function-runner.ts +66 -20
  52. package/src/function/functions.types.ts +26 -11
  53. package/src/index.ts +44 -0
  54. package/src/middleware-runner.ts +10 -10
  55. package/src/permissions.ts +4 -4
  56. package/src/services/ai-agent-runner-service.ts +3 -0
  57. package/src/services/audit-service.test.ts +44 -0
  58. package/src/services/audit-service.ts +198 -0
  59. package/src/services/content-service.ts +1 -0
  60. package/src/services/email-service.ts +46 -0
  61. package/src/services/gopass-secrets.ts +10 -42
  62. package/src/services/index.ts +33 -0
  63. package/src/services/local-content.ts +1 -0
  64. package/src/services/local-email-service.test.ts +108 -0
  65. package/src/services/local-email-service.ts +30 -0
  66. package/src/services/local-secrets.test.ts +10 -10
  67. package/src/services/local-secrets.ts +16 -55
  68. package/src/services/local-variables.test.ts +4 -4
  69. package/src/services/local-variables.ts +12 -17
  70. package/src/services/meta-service.ts +115 -0
  71. package/src/services/scoped-secret-service.test.ts +10 -11
  72. package/src/services/scoped-secret-service.ts +4 -9
  73. package/src/services/secret-service.ts +5 -12
  74. package/src/services/typed-secret-service.test.ts +16 -17
  75. package/src/services/typed-secret-service.ts +5 -9
  76. package/src/services/typed-variables-service.test.ts +5 -5
  77. package/src/services/typed-variables-service.ts +5 -17
  78. package/src/services/variables-service.ts +2 -4
  79. package/src/testing/service-tests.ts +13 -13
  80. package/src/types/core.types.ts +20 -2
  81. package/src/wirings/ai-agent/ai-agent-prepare.ts +17 -3
  82. package/src/wirings/ai-agent/ai-agent-runner.test.ts +105 -0
  83. package/src/wirings/ai-agent/ai-agent-runner.ts +1 -1
  84. package/src/wirings/ai-agent/ai-agent-stream.ts +1 -1
  85. package/src/wirings/ai-agent/ai-agent-utils.ts +1 -0
  86. package/src/wirings/ai-agent/ai-agent.types.ts +4 -0
  87. package/src/wirings/oauth2/oauth2-client.test.ts +2 -3
  88. package/src/wirings/oauth2/oauth2-client.ts +3 -3
  89. package/src/wirings/rpc/rpc-runner.ts +9 -2
  90. package/tsconfig.tsbuildinfo +1 -1
@@ -109,6 +109,38 @@ export interface PermissionsGroupsMeta {
109
109
  tagGroups: Record<string, GroupMeta>
110
110
  }
111
111
 
112
+ export interface EmailTemplateLocaleMeta {
113
+ contentHash: string
114
+ htmlHash: string
115
+ subjectHash: string
116
+ textHash: string
117
+ }
118
+
119
+ export interface EmailTemplateMeta {
120
+ variables: string[]
121
+ hasHtml: boolean
122
+ hasSubject: boolean
123
+ hasText: boolean
124
+ locales: Record<string, EmailTemplateLocaleMeta>
125
+ }
126
+
127
+ export interface EmailsMeta {
128
+ src: string
129
+ themeHash: string
130
+ templates: Record<string, EmailTemplateMeta>
131
+ }
132
+
133
+ export interface EmailTemplateAssets {
134
+ theme: Record<string, unknown>
135
+ strings: Record<string, unknown>
136
+ layout: string
137
+ partials: Record<string, string>
138
+ html: string
139
+ subject: string
140
+ text: string
141
+ missing: string[]
142
+ }
143
+
112
144
  /**
113
145
  * Abstraction over .pikku metadata file access.
114
146
  * All paths are relative to the .pikku root directory.
@@ -123,6 +155,8 @@ export interface MetaService {
123
155
  readFile(relativePath: string): Promise<string | null>
124
156
  /** List files in a directory by relative path. Returns empty array if not found. */
125
157
  readDir(relativePath: string): Promise<string[]>
158
+ /** Read a project source file relative to the project root (one level above .pikku). */
159
+ readProjectFile(relativePath: string): Promise<string | null>
126
160
 
127
161
  // -- Typed metadata accessors --
128
162
 
@@ -143,6 +177,11 @@ export interface MetaService {
143
177
  getSecretsMeta(): Promise<SecretDefinitionsMeta>
144
178
  getCredentialsMeta(): Promise<CredentialDefinitionsMeta>
145
179
  getVariablesMeta(): Promise<VariableDefinitionsMeta>
180
+ getEmailMeta(): Promise<EmailsMeta>
181
+ getEmailTemplateAssets(
182
+ templateName: string,
183
+ locale: string
184
+ ): Promise<EmailTemplateAssets>
146
185
  getServicesMeta(): Promise<ServicesMetaRecord>
147
186
 
148
187
  getSchema(schemaName: string): Promise<JSONSchema7 | null>
@@ -173,6 +212,7 @@ export class LocalMetaService implements MetaService {
173
212
  private secretsMetaCache: SecretDefinitionsMeta | null = null
174
213
  private credentialsMetaCache: CredentialDefinitionsMeta | null = null
175
214
  private variablesMetaCache: VariableDefinitionsMeta | null = null
215
+ private emailMetaCache: EmailsMeta | null = null
176
216
  private middlewareGroupsMetaCache: MiddlewareGroupsMeta | null = null
177
217
  private permissionsGroupsMetaCache: PermissionsGroupsMeta | null = null
178
218
  private agentsMetaCache: AgentsMeta | null = null
@@ -198,6 +238,14 @@ export class LocalMetaService implements MetaService {
198
238
  }
199
239
  }
200
240
 
241
+ async readProjectFile(relativePath: string): Promise<string | null> {
242
+ try {
243
+ return await readFile(join(this.basePath, '..', relativePath), 'utf-8')
244
+ } catch {
245
+ return null
246
+ }
247
+ }
248
+
201
249
  clearCache(): void {
202
250
  this.httpMetaCache = null
203
251
  this.channelsMetaCache = null
@@ -214,6 +262,7 @@ export class LocalMetaService implements MetaService {
214
262
  this.secretsMetaCache = null
215
263
  this.credentialsMetaCache = null
216
264
  this.variablesMetaCache = null
265
+ this.emailMetaCache = null
217
266
  this.middlewareGroupsMetaCache = null
218
267
  this.permissionsGroupsMetaCache = null
219
268
  this.agentsMetaCache = null
@@ -459,6 +508,72 @@ export class LocalMetaService implements MetaService {
459
508
  return this.variablesMetaCache!
460
509
  }
461
510
 
511
+ async getEmailMeta(): Promise<EmailsMeta> {
512
+ if (this.emailMetaCache) return this.emailMetaCache
513
+
514
+ const content = await this.readFile('email/pikku-emails-meta.gen.json')
515
+ this.emailMetaCache = content
516
+ ? JSON.parse(content)
517
+ : {
518
+ src: '',
519
+ themeHash: '',
520
+ templates: {},
521
+ }
522
+ return this.emailMetaCache!
523
+ }
524
+
525
+ async getEmailTemplateAssets(
526
+ templateName: string,
527
+ locale: string
528
+ ): Promise<EmailTemplateAssets> {
529
+ const emailsMeta = await this.getEmailMeta()
530
+ if (!emailsMeta.src) {
531
+ throw new Error(
532
+ 'No generated email metadata found. Run `pikku emails generate`.'
533
+ )
534
+ }
535
+
536
+ const baseDir = emailsMeta.src
537
+ const [
538
+ themeRaw,
539
+ localeRaw,
540
+ layoutRaw,
541
+ footerRaw,
542
+ templateHtml,
543
+ templateSubject,
544
+ templateText,
545
+ ] = await Promise.all([
546
+ this.readProjectFile(`${baseDir}/theme.json`),
547
+ this.readProjectFile(`${baseDir}/locales/${locale}.json`),
548
+ this.readProjectFile(`${baseDir}/partials/layout.html`),
549
+ this.readProjectFile(`${baseDir}/partials/footer.html`),
550
+ this.readProjectFile(`${baseDir}/templates/${templateName}.html`),
551
+ this.readProjectFile(`${baseDir}/templates/${templateName}.subject.txt`),
552
+ this.readProjectFile(`${baseDir}/templates/${templateName}.text.txt`),
553
+ ])
554
+
555
+ return {
556
+ theme: themeRaw ? (JSON.parse(themeRaw) as Record<string, unknown>) : {},
557
+ strings: localeRaw
558
+ ? (JSON.parse(localeRaw) as Record<string, unknown>)
559
+ : {},
560
+ layout: layoutRaw ?? '',
561
+ partials: {
562
+ footer: footerRaw ?? '',
563
+ },
564
+ html: templateHtml ?? '',
565
+ subject: templateSubject ?? '',
566
+ text: templateText ?? '',
567
+ missing: [
568
+ ...(themeRaw ? [] : ['theme']),
569
+ ...(localeRaw ? [] : ['locale']),
570
+ ...(templateHtml ? [] : ['html']),
571
+ ...(templateSubject ? [] : ['subject']),
572
+ ...(templateText ? [] : ['text']),
573
+ ],
574
+ }
575
+ }
576
+
462
577
  async getServicesMeta(): Promise<ServicesMetaRecord> {
463
578
  if (this.servicesMetaCache) return this.servicesMetaCache
464
579
 
@@ -3,10 +3,9 @@ import assert from 'node:assert'
3
3
  import { ScopedSecretService } from './scoped-secret-service.js'
4
4
 
5
5
  const createMockSecrets = () => ({
6
- getSecret: async (key: string) => `value-of-${key}`,
7
- getSecretJSON: async (key: string) => ({ key }),
6
+ getSecret: async <T>(key: string) => ({ key }) as T,
8
7
  hasSecret: async () => true,
9
- setSecretJSON: async () => {},
8
+ setSecret: async () => {},
10
9
  deleteSecret: async () => {},
11
10
  })
12
11
 
@@ -15,7 +14,7 @@ describe('ScopedSecretService', () => {
15
14
  const mock = createMockSecrets()
16
15
  const scoped = new ScopedSecretService(mock, new Set(['KEY1', 'KEY2']))
17
16
  const result = await scoped.getSecret('KEY1')
18
- assert.strictEqual(result, 'value-of-KEY1')
17
+ assert.deepStrictEqual(result, { key: 'KEY1' })
19
18
  })
20
19
 
21
20
  test('should deny access to non-allowed keys', async () => {
@@ -26,17 +25,17 @@ describe('ScopedSecretService', () => {
26
25
  })
27
26
  })
28
27
 
29
- test('should allow getSecretJSON for allowed keys', async () => {
28
+ test('should allow getSecret for allowed keys', async () => {
30
29
  const mock = createMockSecrets()
31
30
  const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
32
- const result = await scoped.getSecretJSON('KEY1')
31
+ const result = await scoped.getSecret('KEY1')
33
32
  assert.deepStrictEqual(result, { key: 'KEY1' })
34
33
  })
35
34
 
36
- test('should deny getSecretJSON for non-allowed keys', async () => {
35
+ test('should deny getSecret for non-allowed keys', async () => {
37
36
  const mock = createMockSecrets()
38
37
  const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
39
- await assert.rejects(() => scoped.getSecretJSON('FORBIDDEN'), {
38
+ await assert.rejects(() => scoped.getSecret('FORBIDDEN'), {
40
39
  message: 'Access denied to secret key: FORBIDDEN',
41
40
  })
42
41
  })
@@ -56,11 +55,11 @@ describe('ScopedSecretService', () => {
56
55
  })
57
56
  })
58
57
 
59
- test('should always throw on setSecretJSON', async () => {
58
+ test('should always throw on setSecret', async () => {
60
59
  const mock = createMockSecrets()
61
60
  const scoped = new ScopedSecretService(mock, new Set(['KEY1']))
62
- await assert.rejects(() => scoped.setSecretJSON('KEY1', 'val'), {
63
- message: 'setSecretJSON is not allowed in scoped secret service',
61
+ await assert.rejects(() => scoped.setSecret('KEY1', 'val'), {
62
+ message: 'setSecret is not allowed in scoped secret service',
64
63
  })
65
64
  })
66
65
 
@@ -16,14 +16,9 @@ export class ScopedSecretService implements SecretService {
16
16
  }
17
17
  }
18
18
 
19
- async getSecret(key: string): Promise<string> {
19
+ async getSecret<T = string>(key: string): Promise<T> {
20
20
  this.assertAllowed(key)
21
- return this.secrets.getSecret(key)
22
- }
23
-
24
- async getSecretJSON<T = {}>(key: string): Promise<T> {
25
- this.assertAllowed(key)
26
- return this.secrets.getSecretJSON<T>(key)
21
+ return this.secrets.getSecret<T>(key)
27
22
  }
28
23
 
29
24
  async hasSecret(key: string): Promise<boolean> {
@@ -31,8 +26,8 @@ export class ScopedSecretService implements SecretService {
31
26
  return this.secrets.hasSecret(key)
32
27
  }
33
28
 
34
- async setSecretJSON(_key: string, _value: unknown): Promise<void> {
35
- throw new Error('setSecretJSON is not allowed in scoped secret service')
29
+ async setSecret(_key: string, _value: unknown): Promise<void> {
30
+ throw new Error('setSecret is not allowed in scoped secret service')
36
31
  }
37
32
 
38
33
  async deleteSecret(_key: string): Promise<void> {
@@ -3,19 +3,12 @@
3
3
  */
4
4
  export interface SecretService {
5
5
  /**
6
- * Retrieves a secret by key and parses it as JSON.
7
- * @param key - The key of the secret to retrieve.
8
- * @returns A promise that resolves to the parsed secret value.
9
- * @throws If the secret is not found.
10
- */
11
- getSecretJSON<Return = {}>(key: string): Promise<Return>
12
- /**
13
- * Retrieves a secret by key as a string.
6
+ * Retrieves a secret by key, typed as T (defaults to string).
14
7
  * @param key - The key of the secret to retrieve.
15
8
  * @returns A promise that resolves to the secret value.
16
9
  * @throws If the secret is not found.
17
10
  */
18
- getSecret(key: string): Promise<string>
11
+ getSecret<T = string>(key: string): Promise<T>
19
12
  /**
20
13
  * Checks if a secret exists without throwing.
21
14
  * @param key - The key of the secret to check.
@@ -23,12 +16,12 @@ export interface SecretService {
23
16
  */
24
17
  hasSecret(key: string): Promise<boolean>
25
18
  /**
26
- * Stores a JSON value as a secret.
19
+ * Stores a secret value.
27
20
  * @param key - The key to store the secret under.
28
- * @param value - The JSON value to store.
21
+ * @param value - The value to store.
29
22
  * @returns A promise that resolves when the secret is stored.
30
23
  */
31
- setSecretJSON(key: string, value: unknown): Promise<void>
24
+ setSecret(key: string, value: unknown): Promise<void>
32
25
  /**
33
26
  * Deletes a secret by key.
34
27
  * @param key - The key of the secret to delete.
@@ -3,18 +3,17 @@ import assert from 'node:assert'
3
3
  import { TypedSecretService } from './typed-secret-service.js'
4
4
 
5
5
  const createMockSecrets = (store: Map<string, string> = new Map()) => ({
6
- getSecret: async (key: string) => {
6
+ getSecret: async <T = string>(key: string): Promise<T> => {
7
7
  const val = store.get(key)
8
8
  if (!val) throw new Error(`Not found: ${key}`)
9
- return val
10
- },
11
- getSecretJSON: async (key: string) => {
12
- const val = store.get(key)
13
- if (!val) throw new Error(`Not found: ${key}`)
14
- return JSON.parse(val)
9
+ try {
10
+ return JSON.parse(val) as T
11
+ } catch {
12
+ return val as unknown as T
13
+ }
15
14
  },
16
15
  hasSecret: async (key: string) => store.has(key),
17
- setSecretJSON: async (key: string, value: unknown) => {
16
+ setSecret: async (key: string, value: unknown) => {
18
17
  store.set(key, JSON.stringify(value))
19
18
  },
20
19
  deleteSecret: async (key: string) => {
@@ -24,42 +23,42 @@ const createMockSecrets = (store: Map<string, string> = new Map()) => ({
24
23
 
25
24
  describe('TypedSecretService', () => {
26
25
  test('should delegate getSecret to underlying service', async () => {
27
- const store = new Map([['API_KEY', 'sk-123']])
26
+ const store = new Map([['API_KEY', '"sk-123"']])
28
27
  const service = new TypedSecretService(createMockSecrets(store), {})
29
28
  const result = await service.getSecret('API_KEY')
30
29
  assert.strictEqual(result, 'sk-123')
31
30
  })
32
31
 
33
- test('should delegate getSecretJSON to underlying service', async () => {
32
+ test('should delegate getSecret with type to underlying service', async () => {
34
33
  const store = new Map([['CONFIG', '{"port":3000}']])
35
34
  const service = new TypedSecretService(createMockSecrets(store), {})
36
- const result = await service.getSecretJSON('CONFIG')
35
+ const result = await service.getSecret<{ port: number }>('CONFIG')
37
36
  assert.deepStrictEqual(result, { port: 3000 })
38
37
  })
39
38
 
40
39
  test('should delegate hasSecret to underlying service', async () => {
41
- const store = new Map([['EXISTS', 'val']])
40
+ const store = new Map([['EXISTS', '"val"']])
42
41
  const service = new TypedSecretService(createMockSecrets(store), {})
43
42
  assert.strictEqual(await service.hasSecret('EXISTS'), true)
44
43
  assert.strictEqual(await service.hasSecret('MISSING'), false)
45
44
  })
46
45
 
47
- test('should delegate setSecretJSON to underlying service', async () => {
46
+ test('should delegate setSecret to underlying service', async () => {
48
47
  const store = new Map<string, string>()
49
48
  const service = new TypedSecretService(createMockSecrets(store), {})
50
- await service.setSecretJSON('NEW_KEY', { data: 'val' })
49
+ await service.setSecret('NEW_KEY', { data: 'val' })
51
50
  assert.strictEqual(store.get('NEW_KEY'), '{"data":"val"}')
52
51
  })
53
52
 
54
53
  test('should delegate deleteSecret to underlying service', async () => {
55
- const store = new Map([['KEY', 'val']])
54
+ const store = new Map([['KEY', '"val"']])
56
55
  const service = new TypedSecretService(createMockSecrets(store), {})
57
56
  await service.deleteSecret('KEY')
58
57
  assert.strictEqual(store.has('KEY'), false)
59
58
  })
60
59
 
61
60
  test('should get all status for credentials', async () => {
62
- const store = new Map([['STRIPE_KEY', 'sk-123']])
61
+ const store = new Map([['STRIPE_KEY', '"sk-123"']])
63
62
  const meta = {
64
63
  STRIPE_KEY: { name: 'stripe', displayName: 'Stripe' },
65
64
  GITHUB_TOKEN: {
@@ -80,7 +79,7 @@ describe('TypedSecretService', () => {
80
79
  })
81
80
 
82
81
  test('should get missing credentials', async () => {
83
- const store = new Map([['STRIPE_KEY', 'sk-123']])
82
+ const store = new Map([['STRIPE_KEY', '"sk-123"']])
84
83
  const meta = {
85
84
  STRIPE_KEY: { name: 'stripe', displayName: 'Stripe' },
86
85
  GITHUB_TOKEN: { name: 'github', displayName: 'GitHub' },
@@ -22,13 +22,9 @@ export class TypedSecretService<
22
22
  private credentialsMeta: Record<string, CredentialMeta>
23
23
  ) {}
24
24
 
25
- async getSecretJSON<K extends keyof TMap & string>(key: K): Promise<TMap[K]>
26
- async getSecretJSON<T = unknown>(key: string): Promise<T>
27
- async getSecretJSON(key: string): Promise<unknown> {
28
- return this.secrets.getSecretJSON(key)
29
- }
30
-
31
- async getSecret(key: string): Promise<string> {
25
+ async getSecret<K extends keyof TMap & string>(key: K): Promise<TMap[K]>
26
+ async getSecret<T = string>(key: string): Promise<T>
27
+ async getSecret(key: string): Promise<unknown> {
32
28
  return this.secrets.getSecret(key)
33
29
  }
34
30
 
@@ -36,11 +32,11 @@ export class TypedSecretService<
36
32
  return this.secrets.hasSecret(key)
37
33
  }
38
34
 
39
- async setSecretJSON<K extends string>(
35
+ async setSecret<K extends string>(
40
36
  key: K,
41
37
  value: K extends keyof TMap ? TMap[K] : unknown
42
38
  ): Promise<void> {
43
- return this.secrets.setSecretJSON(key, value)
39
+ return this.secrets.setSecret(key, value)
44
40
  }
45
41
 
46
42
  async deleteSecret(key: string): Promise<void> {
@@ -36,15 +36,15 @@ describe('TypedVariablesService', () => {
36
36
  assert.strictEqual(service.has('DB_URL'), false)
37
37
  })
38
38
 
39
- test('should delegate getJSON to underlying service', () => {
39
+ test('should delegate get to underlying service', () => {
40
40
  const service = createService({ DATA: '{"key":"val"}' })
41
- assert.deepStrictEqual(service.getJSON('DATA'), { key: 'val' })
41
+ assert.deepStrictEqual(service.get('DATA'), { key: 'val' })
42
42
  })
43
43
 
44
- test('should delegate setJSON to underlying service', () => {
44
+ test('should delegate set to underlying service', () => {
45
45
  const service = createService()
46
- service.setJSON('DATA', { key: 'val' })
47
- assert.strictEqual(service.get('DATA'), '{"key":"val"}')
46
+ service.set('DATA', { key: 'val' })
47
+ assert.deepStrictEqual(service.get('DATA'), { key: 'val' })
48
48
  })
49
49
 
50
50
  test('should get all status for configured variables', async () => {
@@ -20,20 +20,12 @@ export class TypedVariablesService<
20
20
  private variablesMeta: Record<string, VariableMeta>
21
21
  ) {}
22
22
 
23
- get(
24
- name: keyof TMap & string
25
- ): Promise<string | undefined> | string | undefined
26
- get(name: string): Promise<string | undefined> | string | undefined
27
- get(name: string): Promise<string | undefined> | string | undefined {
28
- return this.variables.get(name)
29
- }
30
-
31
- getJSON<K extends keyof TMap & string>(
23
+ get<K extends keyof TMap & string>(
32
24
  name: K
33
25
  ): Promise<TMap[K] | undefined> | TMap[K] | undefined
34
- getJSON<T = unknown>(name: string): Promise<T | undefined> | T | undefined
35
- getJSON(name: string): Promise<unknown> | unknown {
36
- return this.variables.getJSON(name)
26
+ get<T = string>(name: string): Promise<T | undefined> | T | undefined
27
+ get(name: string): Promise<unknown> | unknown {
28
+ return this.variables.get(name)
37
29
  }
38
30
 
39
31
  getAll():
@@ -42,14 +34,10 @@ export class TypedVariablesService<
42
34
  return this.variables.getAll()
43
35
  }
44
36
 
45
- set(name: string, value: string): Promise<void> | void {
37
+ set(name: string, value: unknown): Promise<void> | void {
46
38
  return this.variables.set(name, value)
47
39
  }
48
40
 
49
- setJSON(name: string, value: unknown): Promise<void> | void {
50
- return this.variables.setJSON(name, value)
51
- }
52
-
53
41
  has(name: string): Promise<boolean> | boolean {
54
42
  return this.variables.has(name)
55
43
  }
@@ -1,11 +1,9 @@
1
1
  export interface VariablesService {
2
- get: (name: string) => Promise<string | undefined> | string | undefined
3
- getJSON: <T = unknown>(name: string) => Promise<T | undefined> | T | undefined
2
+ get<T = string>(name: string): Promise<T | undefined> | T | undefined
4
3
  getAll: () =>
5
4
  | Promise<Record<string, string | undefined>>
6
5
  | Record<string, string | undefined>
7
- set: (name: string, value: string) => Promise<void> | void
8
- setJSON: (name: string, value: unknown) => Promise<void> | void
6
+ set: (name: string, value: unknown) => Promise<void> | void
9
7
  has: (name: string) => Promise<boolean> | boolean
10
8
  delete: (name: string) => Promise<void> | void
11
9
  }
@@ -782,13 +782,13 @@ export function defineServiceTests(config: ServiceTestConfig): void {
782
782
  const kek = 'test-key-encryption-key-32chars!'
783
783
 
784
784
  describe(`SecretService [${name}]`, () => {
785
- test('setSecretJSON and getSecretJSON', async () => {
785
+ test('setSecret and getSecret', async () => {
786
786
  const service = await factory({ key: kek })
787
- await service.setSecretJSON('api-key', {
787
+ await service.setSecret('api-key', {
788
788
  token: 'sk-123',
789
789
  endpoint: 'https://api.example.com',
790
790
  })
791
- const result = await service.getSecretJSON<{
791
+ const result = await service.getSecret<{
792
792
  token: string
793
793
  endpoint: string
794
794
  }>('api-key')
@@ -800,9 +800,9 @@ export function defineServiceTests(config: ServiceTestConfig): void {
800
800
 
801
801
  test('getSecret returns raw string', async () => {
802
802
  const service = await factory({ key: kek })
803
- await service.setSecretJSON('string-secret', 'plain-value')
803
+ await service.setSecret('string-secret', 'plain-value')
804
804
  const result = await service.getSecret('string-secret')
805
- assert.strictEqual(result, '"plain-value"')
805
+ assert.strictEqual(result, 'plain-value')
806
806
  })
807
807
 
808
808
  test('hasSecret returns true/false', async () => {
@@ -818,17 +818,17 @@ export function defineServiceTests(config: ServiceTestConfig): void {
818
818
  })
819
819
  })
820
820
 
821
- test('setSecretJSON upserts existing key', async () => {
821
+ test('setSecret upserts existing key', async () => {
822
822
  const service = await factory({ key: kek })
823
- await service.setSecretJSON('upsert-key', { v: 1 })
824
- await service.setSecretJSON('upsert-key', { v: 2 })
825
- const result = await service.getSecretJSON<{ v: number }>('upsert-key')
823
+ await service.setSecret('upsert-key', { v: 1 })
824
+ await service.setSecret('upsert-key', { v: 2 })
825
+ const result = await service.getSecret<{ v: number }>('upsert-key')
826
826
  assert.deepEqual(result, { v: 2 })
827
827
  })
828
828
 
829
829
  test('deleteSecret removes the key', async () => {
830
830
  const service = await factory({ key: kek })
831
- await service.setSecretJSON('to-delete', 'bye')
831
+ await service.setSecret('to-delete', 'bye')
832
832
  assert.strictEqual(await service.hasSecret('to-delete'), true)
833
833
  await service.deleteSecret('to-delete')
834
834
  assert.strictEqual(await service.hasSecret('to-delete'), false)
@@ -837,7 +837,7 @@ export function defineServiceTests(config: ServiceTestConfig): void {
837
837
  test('rotateKEK re-wraps all secrets', async () => {
838
838
  const newKEK = 'new-key-encryption-key-rotated!'
839
839
  const oldService = await factory({ key: kek })
840
- await oldService.setSecretJSON('rotate-test', { important: 'data' })
840
+ await oldService.setSecret('rotate-test', { important: 'data' })
841
841
 
842
842
  const rotatedService = await factory({
843
843
  key: newKEK,
@@ -845,7 +845,7 @@ export function defineServiceTests(config: ServiceTestConfig): void {
845
845
  previousKey: kek,
846
846
  })
847
847
 
848
- const before = await rotatedService.getSecretJSON<{
848
+ const before = await rotatedService.getSecret<{
849
849
  important: string
850
850
  }>('rotate-test')
851
851
  assert.deepEqual(before, { important: 'data' })
@@ -858,7 +858,7 @@ export function defineServiceTests(config: ServiceTestConfig): void {
858
858
  key: newKEK,
859
859
  keyVersion: 2,
860
860
  })
861
- const after = await newOnlyService.getSecretJSON<{
861
+ const after = await newOnlyService.getSecret<{
862
862
  important: string
863
863
  }>('rotate-test')
864
864
  assert.deepEqual(after, { important: 'data' })
@@ -35,8 +35,13 @@ import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
35
35
  import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
36
36
  import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
37
37
  import type { CredentialService } from '../services/credential-service.js'
38
+ import type { EmailService } from '../services/email-service.js'
38
39
  import type { MetaService } from '../services/meta-service.js'
39
40
  import type { SessionStore } from '../services/session-store.js'
41
+ import type {
42
+ AuditDurability,
43
+ AuditService,
44
+ } from '../services/audit-service.js'
40
45
 
41
46
  export type PikkuWiringTypes =
42
47
  | 'http'
@@ -196,7 +201,10 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
196
201
  /**
197
202
  * Represents a core user session, which can be extended for more specific session information.
198
203
  */
199
- export interface CoreUserSession {}
204
+ export interface CoreUserSession {
205
+ userId?: string
206
+ orgId?: string
207
+ }
200
208
 
201
209
  /**
202
210
  * Interface for core singleton services provided by Pikku.
@@ -239,8 +247,12 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
239
247
  workflowRunService?: WorkflowRunService
240
248
  /** Credential service for dynamic/managed credentials (OAuth tokens, per-user API keys) */
241
249
  credentialService?: CredentialService
250
+ /** Email service for outbound messages and template-backed delivery */
251
+ emailService?: EmailService
242
252
  /** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
243
253
  metaService?: MetaService
254
+ /** Audit service for durable or staged audit event capture */
255
+ audit?: AuditService
244
256
  /** Session store for persisting user sessions keyed by pikkuUserId */
245
257
  sessionStore?: SessionStore
246
258
  }
@@ -263,6 +275,8 @@ export type PikkuWire<
263
275
  wireId: string
264
276
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
265
277
  traceId: string
278
+ /** Function id for the current invocation */
279
+ functionId: string
266
280
  http: PikkuHTTP<In>
267
281
  mcp: PikkuMCP<MCPTools>
268
282
  rpc: TypedRPC
@@ -281,7 +295,7 @@ export type PikkuWire<
281
295
  ? UserSession
282
296
  : UserSession | undefined
283
297
  /** Update and persist the current session */
284
- setSession: (session: CoreUserSession) => Promise<void> | void
298
+ setSession: (session: UserSession) => Promise<void> | void
285
299
  /** Clear and persist the current session */
286
300
  clearSession: () => Promise<void> | void
287
301
  /** Fetch the latest session (may read from backing store) */
@@ -298,6 +312,10 @@ export type PikkuWire<
298
312
  getCredentials: () =>
299
313
  | Record<string, unknown>
300
314
  | Promise<Record<string, unknown>>
315
+ /** Resolved function audit metadata for this invocation, if enabled */
316
+ audit: {
317
+ durability: AuditDurability
318
+ }
301
319
  }>
302
320
 
303
321
  /**