@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.
- package/CHANGELOG.md +30 -0
- package/dist/data-classification.d.ts +16 -0
- package/dist/data-classification.js +1 -0
- package/dist/function/function-runner.js +62 -15
- package/dist/function/functions.types.d.ts +8 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/middleware-runner.d.ts +2 -2
- package/dist/permissions.d.ts +1 -1
- package/dist/services/ai-agent-runner-service.d.ts +3 -0
- package/dist/services/audit-service.d.ts +50 -0
- package/dist/services/audit-service.js +106 -0
- package/dist/services/content-service.d.ts +1 -0
- package/dist/services/email-service.d.ts +36 -0
- package/dist/services/email-service.js +1 -0
- package/dist/services/gopass-secrets.d.ts +2 -35
- package/dist/services/gopass-secrets.js +10 -40
- package/dist/services/index.d.ts +5 -1
- package/dist/services/index.js +2 -0
- package/dist/services/local-content.js +1 -0
- package/dist/services/local-email-service.d.ts +4 -0
- package/dist/services/local-email-service.js +26 -0
- package/dist/services/local-secrets.d.ts +3 -36
- package/dist/services/local-secrets.js +12 -52
- package/dist/services/local-variables.d.ts +3 -5
- package/dist/services/local-variables.js +10 -10
- package/dist/services/meta-service.d.ts +36 -0
- package/dist/services/meta-service.js +59 -0
- package/dist/services/scoped-secret-service.d.ts +2 -3
- package/dist/services/scoped-secret-service.js +2 -6
- package/dist/services/secret-service.d.ts +5 -12
- package/dist/services/typed-secret-service.d.ts +3 -4
- package/dist/services/typed-secret-service.js +2 -5
- package/dist/services/typed-variables-service.d.ts +3 -6
- package/dist/services/typed-variables-service.js +0 -6
- package/dist/services/variables-service.d.ts +2 -4
- package/dist/testing/service-tests.js +13 -13
- package/dist/types/core.types.d.ts +15 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +2 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +12 -3
- package/dist/wirings/ai-agent/ai-agent-runner.js +1 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +1 -1
- package/dist/wirings/ai-agent/ai-agent-utils.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-utils.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +4 -0
- package/dist/wirings/oauth2/oauth2-client.js +3 -3
- package/dist/wirings/rpc/rpc-runner.js +9 -2
- package/package.json +1 -1
- package/src/data-classification.ts +15 -0
- package/src/function/function-runner.test.ts +255 -0
- package/src/function/function-runner.ts +66 -20
- package/src/function/functions.types.ts +26 -11
- package/src/index.ts +44 -0
- package/src/middleware-runner.ts +10 -10
- package/src/permissions.ts +4 -4
- package/src/services/ai-agent-runner-service.ts +3 -0
- package/src/services/audit-service.test.ts +44 -0
- package/src/services/audit-service.ts +198 -0
- package/src/services/content-service.ts +1 -0
- package/src/services/email-service.ts +46 -0
- package/src/services/gopass-secrets.ts +10 -42
- package/src/services/index.ts +33 -0
- package/src/services/local-content.ts +1 -0
- package/src/services/local-email-service.test.ts +108 -0
- package/src/services/local-email-service.ts +30 -0
- package/src/services/local-secrets.test.ts +10 -10
- package/src/services/local-secrets.ts +16 -55
- package/src/services/local-variables.test.ts +4 -4
- package/src/services/local-variables.ts +12 -17
- package/src/services/meta-service.ts +115 -0
- package/src/services/scoped-secret-service.test.ts +10 -11
- package/src/services/scoped-secret-service.ts +4 -9
- package/src/services/secret-service.ts +5 -12
- package/src/services/typed-secret-service.test.ts +16 -17
- package/src/services/typed-secret-service.ts +5 -9
- package/src/services/typed-variables-service.test.ts +5 -5
- package/src/services/typed-variables-service.ts +5 -17
- package/src/services/variables-service.ts +2 -4
- package/src/testing/service-tests.ts +13 -13
- package/src/types/core.types.ts +20 -2
- package/src/wirings/ai-agent/ai-agent-prepare.ts +17 -3
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +105 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +1 -1
- package/src/wirings/ai-agent/ai-agent-utils.ts +1 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +4 -0
- package/src/wirings/oauth2/oauth2-client.test.ts +2 -3
- package/src/wirings/oauth2/oauth2-client.ts +3 -3
- package/src/wirings/rpc/rpc-runner.ts +9 -2
- 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) =>
|
|
7
|
-
getSecretJSON: async (key: string) => ({ key }),
|
|
6
|
+
getSecret: async <T>(key: string) => ({ key }) as T,
|
|
8
7
|
hasSecret: async () => true,
|
|
9
|
-
|
|
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.
|
|
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
|
|
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.
|
|
31
|
+
const result = await scoped.getSecret('KEY1')
|
|
33
32
|
assert.deepStrictEqual(result, { key: 'KEY1' })
|
|
34
33
|
})
|
|
35
34
|
|
|
36
|
-
test('should deny
|
|
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.
|
|
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
|
|
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.
|
|
63
|
-
message: '
|
|
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<
|
|
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
|
|
35
|
-
throw new Error('
|
|
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
|
|
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<
|
|
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
|
|
19
|
+
* Stores a secret value.
|
|
27
20
|
* @param key - The key to store the secret under.
|
|
28
|
-
* @param value - The
|
|
21
|
+
* @param value - The value to store.
|
|
29
22
|
* @returns A promise that resolves when the secret is stored.
|
|
30
23
|
*/
|
|
31
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
26
|
-
async
|
|
27
|
-
async
|
|
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
|
|
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.
|
|
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
|
|
39
|
+
test('should delegate get to underlying service', () => {
|
|
40
40
|
const service = createService({ DATA: '{"key":"val"}' })
|
|
41
|
-
assert.deepStrictEqual(service.
|
|
41
|
+
assert.deepStrictEqual(service.get('DATA'), { key: 'val' })
|
|
42
42
|
})
|
|
43
43
|
|
|
44
|
-
test('should delegate
|
|
44
|
+
test('should delegate set to underlying service', () => {
|
|
45
45
|
const service = createService()
|
|
46
|
-
service.
|
|
47
|
-
assert.
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
return this.variables.
|
|
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:
|
|
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
|
|
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:
|
|
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('
|
|
785
|
+
test('setSecret and getSecret', async () => {
|
|
786
786
|
const service = await factory({ key: kek })
|
|
787
|
-
await service.
|
|
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.
|
|
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.
|
|
803
|
+
await service.setSecret('string-secret', 'plain-value')
|
|
804
804
|
const result = await service.getSecret('string-secret')
|
|
805
|
-
assert.strictEqual(result, '
|
|
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('
|
|
821
|
+
test('setSecret upserts existing key', async () => {
|
|
822
822
|
const service = await factory({ key: kek })
|
|
823
|
-
await service.
|
|
824
|
-
await service.
|
|
825
|
-
const result = await service.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
861
|
+
const after = await newOnlyService.getSecret<{
|
|
862
862
|
important: string
|
|
863
863
|
}>('rotate-test')
|
|
864
864
|
assert.deepEqual(after, { important: 'data' })
|
package/src/types/core.types.ts
CHANGED
|
@@ -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:
|
|
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
|
/**
|