@pikku/core 0.12.23 → 0.12.24

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 (56) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/function/function-runner.js +62 -15
  3. package/dist/function/functions.types.d.ts +8 -5
  4. package/dist/index.d.ts +4 -0
  5. package/dist/index.js +1 -0
  6. package/dist/middleware-runner.d.ts +2 -2
  7. package/dist/permissions.d.ts +1 -1
  8. package/dist/services/ai-agent-runner-service.d.ts +3 -0
  9. package/dist/services/audit-service.d.ts +50 -0
  10. package/dist/services/audit-service.js +106 -0
  11. package/dist/services/content-service.d.ts +1 -0
  12. package/dist/services/email-service.d.ts +36 -0
  13. package/dist/services/email-service.js +1 -0
  14. package/dist/services/index.d.ts +5 -1
  15. package/dist/services/index.js +2 -0
  16. package/dist/services/local-content.js +1 -0
  17. package/dist/services/local-email-service.d.ts +4 -0
  18. package/dist/services/local-email-service.js +26 -0
  19. package/dist/services/local-secrets.d.ts +1 -0
  20. package/dist/services/local-secrets.js +11 -2
  21. package/dist/services/meta-service.d.ts +36 -0
  22. package/dist/services/meta-service.js +59 -0
  23. package/dist/types/core.types.d.ts +15 -1
  24. package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +2 -0
  25. package/dist/wirings/ai-agent/ai-agent-prepare.js +8 -2
  26. package/dist/wirings/ai-agent/ai-agent-runner.js +1 -1
  27. package/dist/wirings/ai-agent/ai-agent-stream.js +1 -1
  28. package/dist/wirings/ai-agent/ai-agent-utils.d.ts +1 -0
  29. package/dist/wirings/ai-agent/ai-agent-utils.js +1 -0
  30. package/dist/wirings/rpc/rpc-runner.js +9 -2
  31. package/package.json +1 -1
  32. package/src/function/function-runner.test.ts +255 -0
  33. package/src/function/function-runner.ts +66 -20
  34. package/src/function/functions.types.ts +26 -11
  35. package/src/index.ts +35 -0
  36. package/src/middleware-runner.ts +10 -10
  37. package/src/permissions.ts +4 -4
  38. package/src/services/ai-agent-runner-service.ts +3 -0
  39. package/src/services/audit-service.test.ts +44 -0
  40. package/src/services/audit-service.ts +198 -0
  41. package/src/services/content-service.ts +1 -0
  42. package/src/services/email-service.ts +46 -0
  43. package/src/services/index.ts +33 -0
  44. package/src/services/local-content.ts +1 -0
  45. package/src/services/local-email-service.test.ts +108 -0
  46. package/src/services/local-email-service.ts +30 -0
  47. package/src/services/local-secrets.ts +11 -2
  48. package/src/services/meta-service.ts +115 -0
  49. package/src/types/core.types.ts +20 -2
  50. package/src/wirings/ai-agent/ai-agent-prepare.ts +11 -2
  51. package/src/wirings/ai-agent/ai-agent-runner.test.ts +105 -0
  52. package/src/wirings/ai-agent/ai-agent-runner.ts +1 -1
  53. package/src/wirings/ai-agent/ai-agent-stream.ts +1 -1
  54. package/src/wirings/ai-agent/ai-agent-utils.ts +1 -0
  55. package/src/wirings/rpc/rpc-runner.ts +9 -2
  56. package/tsconfig.tsbuildinfo +1 -1
@@ -36,6 +36,7 @@ import {
36
36
  createWireServicesCredentialWireProps,
37
37
  } from '../services/credential-wire-service.js'
38
38
  import { defaultPikkuUserIdResolver } from '../services/pikku-user-id.js'
39
+ import { resolveAuditConfig } from '../services/audit-service.js'
39
40
  import { rpcService } from '../wirings/rpc/rpc-runner.js'
40
41
  import { closeWireServices } from '../utils.js'
41
42
 
@@ -288,6 +289,8 @@ export const runPikkuFunc = async <In = any, Out = any>(
288
289
  throw new Error(`Function meta not found: ${funcName}`)
289
290
  }
290
291
 
292
+ const resolvedFunctionId = funcMeta.pikkuFuncId ?? funcName
293
+
291
294
  // For addon packages, get or create their singleton services
292
295
  const resolvedSingletonServices = packageName
293
296
  ? await getOrCreatePackageSingletonServices(packageName, singletonServices)
@@ -332,6 +335,17 @@ export const runPikkuFunc = async <In = any, Out = any>(
332
335
  )
333
336
  }
334
337
 
338
+ const resolvedAuditConfig = resolveAuditConfig(funcConfig.audit)
339
+ const invocationWire = resolvedWire as PikkuWire
340
+ const previousFunctionId = invocationWire.functionId
341
+ const previousAudit = invocationWire.audit
342
+ const previousRpcDescriptor = Object.getOwnPropertyDescriptor(
343
+ invocationWire,
344
+ 'rpc'
345
+ )
346
+ invocationWire.functionId = resolvedFunctionId
347
+ invocationWire.audit = resolvedAuditConfig
348
+
335
349
  // Convert tags to PermissionMetadata and merge with inheritedPermissions
336
350
  let mergedInheritedPermissions: PermissionMetadata[]
337
351
  if (tags && tags.length > 0) {
@@ -346,20 +360,20 @@ export const runPikkuFunc = async <In = any, Out = any>(
346
360
  // Helper function to run permissions and execute the function
347
361
  const executeFunction = async () => {
348
362
  await resolveSession(
349
- resolvedWire,
363
+ invocationWire,
350
364
  resolvedSingletonServices,
351
365
  sessionService
352
366
  )
353
367
 
354
368
  if (sessionService) {
355
- resolvedWire.session = sessionService.freezeInitial()
356
- resolvedWire.setSession = (s: any) => sessionService.set(s)
357
- resolvedWire.clearSession = () => sessionService.clear()
358
- resolvedWire.getSession = () => sessionService.get()
359
- resolvedWire.hasSessionChanged = () => sessionService.sessionChanged
369
+ invocationWire.session = sessionService.freezeInitial()
370
+ invocationWire.setSession = (s: any) => sessionService.set(s)
371
+ invocationWire.clearSession = () => sessionService.clear()
372
+ invocationWire.getSession = () => sessionService.get()
373
+ invocationWire.hasSessionChanged = () => sessionService.sessionChanged
360
374
  }
361
375
 
362
- const session = resolvedWire.session
376
+ const session = invocationWire.session
363
377
 
364
378
  if (funcMeta.sessionless) {
365
379
  if (wiringAuth === true || funcConfig.auth === true) {
@@ -422,7 +436,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
422
436
  funcInheritedPermissions: funcMeta.permissions,
423
437
  funcPermissions: funcConfig.permissions,
424
438
  services: resolvedSingletonServices,
425
- wire: resolvedWire as any,
439
+ wire: invocationWire as any,
426
440
  data: actualData,
427
441
  packageName,
428
442
  })
@@ -432,23 +446,23 @@ export const runPikkuFunc = async <In = any, Out = any>(
432
446
  try {
433
447
  wireServices = (await resolvedCreateWireServices?.(
434
448
  resolvedSingletonServices,
435
- resolvedWire
449
+ invocationWire
436
450
  )) as Record<string, unknown> | undefined
437
451
  const services =
438
452
  wireServices && Object.keys(wireServices).length > 0
439
453
  ? { ...resolvedSingletonServices, ...wireServices }
440
454
  : resolvedSingletonServices
441
455
  const callerPackageName = packageName
442
- Object.defineProperty(resolvedWire, 'rpc', {
456
+ Object.defineProperty(invocationWire, 'rpc', {
443
457
  get() {
444
458
  const rpc = rpcService.getContextRPCService(
445
459
  services,
446
- resolvedWire,
460
+ invocationWire,
447
461
  { sessionService },
448
462
  0,
449
463
  callerPackageName
450
464
  )
451
- Object.defineProperty(resolvedWire, 'rpc', {
465
+ Object.defineProperty(invocationWire, 'rpc', {
452
466
  value: rpc,
453
467
  writable: true,
454
468
  configurable: true,
@@ -458,7 +472,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
458
472
  configurable: true,
459
473
  enumerable: true,
460
474
  })
461
- return await funcConfig.func(services, actualData, resolvedWire)
475
+ return await funcConfig.func(services, actualData, invocationWire)
462
476
  } finally {
463
477
  if (wireServices && Object.keys(wireServices).length > 0) {
464
478
  await closeWireServices(resolvedSingletonServices.logger, wireServices)
@@ -475,13 +489,45 @@ export const runPikkuFunc = async <In = any, Out = any>(
475
489
  })
476
490
 
477
491
  if (allMiddleware.length > 0) {
478
- return (await runMiddleware<CorePikkuMiddleware>(
479
- resolvedSingletonServices,
480
- resolvedWire,
481
- allMiddleware,
482
- executeFunction
483
- )) as Out
492
+ try {
493
+ return (await runMiddleware<CorePikkuMiddleware>(
494
+ resolvedSingletonServices,
495
+ invocationWire,
496
+ allMiddleware,
497
+ executeFunction
498
+ )) as Out
499
+ } finally {
500
+ if (previousRpcDescriptor) {
501
+ Object.defineProperty(invocationWire, 'rpc', previousRpcDescriptor)
502
+ }
503
+ if (previousFunctionId === undefined) {
504
+ delete (invocationWire as any).functionId
505
+ } else {
506
+ invocationWire.functionId = previousFunctionId
507
+ }
508
+ if (previousAudit === undefined) {
509
+ delete (invocationWire as any).audit
510
+ } else {
511
+ invocationWire.audit = previousAudit
512
+ }
513
+ }
484
514
  }
485
515
 
486
- return (await executeFunction()) as Out
516
+ try {
517
+ return (await executeFunction()) as Out
518
+ } finally {
519
+ if (previousRpcDescriptor) {
520
+ Object.defineProperty(invocationWire, 'rpc', previousRpcDescriptor)
521
+ }
522
+ if (previousFunctionId === undefined) {
523
+ delete (invocationWire as any).functionId
524
+ } else {
525
+ invocationWire.functionId = previousFunctionId
526
+ }
527
+ if (previousAudit === undefined) {
528
+ delete (invocationWire as any).audit
529
+ } else {
530
+ invocationWire.audit = previousAudit
531
+ }
532
+ }
487
533
  }
@@ -29,7 +29,12 @@ export type CorePikkuFunction<
29
29
  Out,
30
30
  Services extends CoreSingletonServices = CoreServices,
31
31
  Session extends CoreUserSession = CoreUserSession,
32
- Wire extends PikkuWire<In, Out> = PikkuWire<In, Out, true, Session>,
32
+ Wire extends PikkuWire<In, Out, true, Session> = PikkuWire<
33
+ In,
34
+ Out,
35
+ true,
36
+ Session
37
+ >,
33
38
  > = (
34
39
  services: Services,
35
40
  data: In,
@@ -74,8 +79,15 @@ export type CorePikkuFunctionSessionless<
74
79
  export type CorePikkuPermission<
75
80
  In = any,
76
81
  Services extends CoreSingletonServices = CoreServices,
77
- Wire extends PikkuWire<In, never, false, CoreUserSession, any, never, never> =
78
- PikkuWire<In, never, false, CoreUserSession, never, never, never>,
82
+ Wire extends PikkuWire<In, never, false, any, any, never, never> = PikkuWire<
83
+ In,
84
+ never,
85
+ false,
86
+ any,
87
+ never,
88
+ never,
89
+ never
90
+ >,
79
91
  > = (services: Services, data: In, wire: Wire) => Promise<boolean>
80
92
 
81
93
  /**
@@ -88,11 +100,11 @@ export type CorePikkuPermission<
88
100
  export type CorePikkuPermissionConfig<
89
101
  In = any,
90
102
  Services extends CoreSingletonServices = CoreServices,
91
- Wire extends PikkuWire<In, never, false, CoreUserSession> = PikkuWire<
103
+ Wire extends PikkuWire<In, never, false, any> = PikkuWire<
92
104
  In,
93
105
  never,
94
106
  false,
95
- CoreUserSession
107
+ any
96
108
  >,
97
109
  > = {
98
110
  /** The permission function */
@@ -131,10 +143,8 @@ export type CorePikkuPermissionConfig<
131
143
  export const pikkuPermission = <
132
144
  In = any,
133
145
  Services extends CoreSingletonServices = CoreServices,
134
- Wire extends PickRequired<
135
- PikkuWire<In, never, false, CoreUserSession>,
136
- 'session'
137
- > = PickRequired<PikkuWire<In, never, false, CoreUserSession>, 'session'>,
146
+ Wire extends PickRequired<PikkuWire<In, never, false, any>, 'session'> =
147
+ PickRequired<PikkuWire<In, never, false, any>, 'session'>,
138
148
  >(
139
149
  permission:
140
150
  | CorePikkuPermission<In, Services, Wire>
@@ -154,11 +164,11 @@ export const pikkuPermission = <
154
164
  export type CorePikkuPermissionFactory<
155
165
  In = any,
156
166
  Services extends CoreSingletonServices = CoreServices,
157
- Wire extends PikkuWire<In, never, false, CoreUserSession> = PikkuWire<
167
+ Wire extends PikkuWire<In, never, false, any> = PikkuWire<
158
168
  In,
159
169
  never,
160
170
  false,
161
- CoreUserSession
171
+ any
162
172
  >,
163
173
  > = (input: In) => CorePikkuPermission<any, Services, Wire>
164
174
 
@@ -285,6 +295,11 @@ export type CorePikkuFunctionConfig<
285
295
  readonly?: boolean
286
296
  deploy?: 'serverless' | 'server' | 'auto'
287
297
  approvalRequired?: boolean
298
+ audit?:
299
+ | boolean
300
+ | {
301
+ durability?: 'best-effort' | 'transactional'
302
+ }
288
303
  approvalDescription?: any
289
304
  func: PikkuFunction
290
305
  auth?: boolean
package/src/index.ts CHANGED
@@ -96,6 +96,15 @@ export { NotFoundError } from './errors/errors.js'
96
96
  export type { EventHubService } from './wirings/channel/eventhub-service.js'
97
97
  export type { QueueService } from './wirings/queue/queue.types.js'
98
98
  export type { JWTService } from './services/jwt-service.js'
99
+ export type {
100
+ EmailService,
101
+ EmailTemplateReference,
102
+ SendEmailInput,
103
+ SendEmailResult,
104
+ SendHTMLEmailInput,
105
+ SendTemplateEmailInput,
106
+ SendTextEmailInput,
107
+ } from './services/email-service.js'
99
108
  export type { SecretService } from './services/secret-service.js'
100
109
  export type { VariablesService } from './services/variables-service.js'
101
110
  export type {
@@ -114,9 +123,35 @@ export type { GatewayService } from './services/gateway-service.js'
114
123
  export type { TriggerService } from './services/trigger-service.js'
115
124
  export type { SchemaService } from './services/schema-service.js'
116
125
  export type { SessionService } from './services/user-session-service.js'
126
+ export {
127
+ NoopAuditService,
128
+ createInvocationAudit,
129
+ resolveAuditActorFromWire,
130
+ resolveAuditConfig,
131
+ } from './services/audit-service.js'
132
+ export type {
133
+ AuditActor,
134
+ AuditConfig,
135
+ AuditDurability,
136
+ AuditEvent,
137
+ AuditEventBatch,
138
+ AuditLog,
139
+ AuditLogWriteInput,
140
+ AuditOutcome,
141
+ AuditService,
142
+ AuditSource,
143
+ ResolvedAuditConfig,
144
+ } from './services/audit-service.js'
117
145
  export type { AIAgentRunnerService } from './services/ai-agent-runner-service.js'
118
146
  export type { AIRunStateService } from './services/ai-run-state-service.js'
119
147
  export type { AIStorageService } from './services/ai-storage-service.js'
148
+ export type {
149
+ EmailsMeta,
150
+ EmailTemplateMeta,
151
+ EmailTemplateLocaleMeta,
152
+ EmailTemplateAssets,
153
+ MetaService,
154
+ } from './services/meta-service.js'
120
155
  export type { HTTPMethod } from './wirings/http/http.types.js'
121
156
  export type { GraphNodeConfig } from './wirings/workflow/graph/workflow-graph.types.js'
122
157
  export { createGraph } from './wirings/workflow/graph/graph-node.js'
@@ -1,6 +1,4 @@
1
1
  import type {
2
- CoreSingletonServices,
3
- PikkuWire,
4
2
  CorePikkuMiddleware,
5
3
  CorePikkuMiddlewareGroup,
6
4
  PikkuWiringTypes,
@@ -18,16 +16,16 @@ const PRIORITY_ORDER: Record<MiddlewarePriority, number> = {
18
16
  lowest: 4,
19
17
  }
20
18
 
21
- const getMiddlewarePriority = (fn: CorePikkuMiddleware): number => {
19
+ const getMiddlewarePriority = (fn: CorePikkuMiddleware<any, any>): number => {
22
20
  const priority = (
23
- fn as CorePikkuMiddleware & { __priority?: MiddlewarePriority }
21
+ fn as CorePikkuMiddleware<any, any> & { __priority?: MiddlewarePriority }
24
22
  ).__priority
25
23
  return PRIORITY_ORDER[priority ?? 'medium']
26
24
  }
27
25
 
28
26
  const sortByPriority = (
29
- middlewares: CorePikkuMiddleware[]
30
- ): CorePikkuMiddleware[] => {
27
+ middlewares: CorePikkuMiddleware<any, any>[]
28
+ ): CorePikkuMiddleware<any, any>[] => {
31
29
  return middlewares.sort(
32
30
  (a, b) => getMiddlewarePriority(a) - getMiddlewarePriority(b)
33
31
  )
@@ -50,9 +48,11 @@ const sortByPriority = (
50
48
  * async () => { return await runMain(); }
51
49
  * );
52
50
  */
53
- export const runMiddleware = async <Middleware extends CorePikkuMiddleware>(
54
- services: CoreSingletonServices,
55
- wire: PikkuWire,
51
+ export const runMiddleware = async <
52
+ Middleware extends CorePikkuMiddleware<any, any>,
53
+ >(
54
+ services: Parameters<Middleware>[0],
55
+ wire: Parameters<Middleware>[1],
56
56
  middlewares: readonly Middleware[],
57
57
  main?: () => Promise<unknown>
58
58
  ): Promise<unknown> => {
@@ -77,7 +77,7 @@ export const runMiddleware = async <Middleware extends CorePikkuMiddleware>(
77
77
  }
78
78
 
79
79
  const isSortedByPriority = (
80
- middlewares: readonly CorePikkuMiddleware[]
80
+ middlewares: readonly CorePikkuMiddleware<any, any>[]
81
81
  ): boolean => {
82
82
  for (let i = 1; i < middlewares.length; i++) {
83
83
  if (
@@ -24,7 +24,7 @@ const verifyPermissions = async <Out = any>(
24
24
  permissions: CorePermissionGroup,
25
25
  services: CoreServices,
26
26
  data: any,
27
- wire: PikkuWire<any, never, any, never, never, never>
27
+ wire: PikkuWire<any, never, any, CoreUserSession, never, never, never>
28
28
  ): Promise<boolean> => {
29
29
  if (!permissions) {
30
30
  return true
@@ -311,7 +311,7 @@ export const runPermissions = async (
311
311
  funcInheritedPermissions?: PermissionMetadata[]
312
312
  funcPermissions?: CorePermissionGroup | CorePikkuPermission[]
313
313
  services: CoreServices
314
- wire: PikkuWire<any, never, any, never, never, never>
314
+ wire: PikkuWire<any, never, any, CoreUserSession, never, never, never>
315
315
  data: any
316
316
  packageName?: string | null
317
317
  }
@@ -377,13 +377,13 @@ export const checkAuthPermissions = async (
377
377
  any,
378
378
  never,
379
379
  any,
380
- never,
380
+ CoreUserSession,
381
381
  never,
382
382
  never
383
383
  >
384
384
 
385
385
  // Extract only pikkuAuth permissions (marked with __pikkuAuth)
386
- const authPerms: CorePikkuPermission[] = []
386
+ const authPerms: CorePikkuPermission<any, any, any>[] = []
387
387
  for (const permission of allPermissions) {
388
388
  if (typeof permission === 'function') {
389
389
  if ((permission as any).__pikkuAuth) {
@@ -42,4 +42,7 @@ export interface AIAgentRunnerService {
42
42
  channel: AIStreamChannel
43
43
  ): Promise<AIAgentStepResult>
44
44
  run(params: AIAgentRunnerParams): Promise<AIAgentStepResult>
45
+ /** Return a new runner that uses the given API key for every LLM call.
46
+ * Optional — runners that don't support per-key scoping leave this undefined. */
47
+ withApiKey?(apiKey: string): AIAgentRunnerService
45
48
  }
@@ -0,0 +1,44 @@
1
+ import { describe, test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ createInvocationAudit,
5
+ type AuditEvent,
6
+ type AuditEventBatch,
7
+ } from './audit-service.js'
8
+
9
+ describe('audit-service', () => {
10
+ test('best-effort flush failures are logged and do not throw', async () => {
11
+ const warnings: unknown[][] = []
12
+ const audit = createInvocationAudit(
13
+ {
14
+ async audit(_event: AuditEvent): Promise<void> {
15
+ throw new Error('should not use single-event write')
16
+ },
17
+ async write(_batch: AuditEventBatch): Promise<void> {
18
+ throw new Error('sink failed')
19
+ },
20
+ },
21
+ {
22
+ wireType: 'rpc',
23
+ wireId: 'wire-best-effort',
24
+ functionId: 'bestEffortFunc',
25
+ audit: { durability: 'best-effort' },
26
+ logger: {
27
+ warn: (...args: unknown[]) => warnings.push(args),
28
+ },
29
+ } as any
30
+ )
31
+
32
+ await audit.write({
33
+ type: 'db.query',
34
+ source: 'auto',
35
+ queryId: 'q-1',
36
+ })
37
+
38
+ await assert.doesNotReject(async () => {
39
+ await audit.close()
40
+ })
41
+ assert.equal(warnings.length, 1)
42
+ assert.equal(warnings[0]?.[0], 'best-effort audit flush failed')
43
+ })
44
+ })
@@ -0,0 +1,198 @@
1
+ import type {
2
+ CoreUserSession,
3
+ PikkuWire,
4
+ PikkuWiringTypes,
5
+ } from '../types/core.types.js'
6
+
7
+ export type AuditDurability = 'best-effort' | 'transactional'
8
+ export type AuditOutcome = 'success' | 'failed' | 'denied'
9
+ export type AuditSource = 'auto' | 'explicit'
10
+
11
+ export type AuditConfig =
12
+ | boolean
13
+ | {
14
+ durability?: AuditDurability
15
+ }
16
+
17
+ export type ResolvedAuditConfig = {
18
+ durability: AuditDurability
19
+ }
20
+
21
+ export type AuditActor = {
22
+ userId?: string
23
+ orgId?: string
24
+ pikkuUserId?: string
25
+ }
26
+
27
+ export type AuditEvent = {
28
+ eventId?: string
29
+ type: string
30
+ source: AuditSource
31
+ outcome?: AuditOutcome
32
+ occurredAt: string
33
+ functionId?: string
34
+ wireType?: PikkuWiringTypes
35
+ wireId?: string
36
+ traceId?: string
37
+ transactionId?: string | null
38
+ queryId?: string | null
39
+ actor?: AuditActor
40
+ input?: unknown
41
+ metadata?: Record<string, unknown>
42
+ }
43
+
44
+ export type AuditEventBatch = AuditEvent[]
45
+
46
+ export interface AuditService {
47
+ audit(event: AuditEvent): Promise<void>
48
+ write?(batch: AuditEventBatch): Promise<void>
49
+ }
50
+
51
+ export class NoopAuditService implements AuditService {
52
+ async audit(_event: AuditEvent): Promise<void> {}
53
+
54
+ async write(_batch: AuditEventBatch): Promise<void> {}
55
+ }
56
+
57
+ export type AuditLogWriteInput = Omit<AuditEvent, 'occurredAt'>
58
+
59
+ export interface AuditLog {
60
+ readonly config: ResolvedAuditConfig | undefined
61
+ write(event: AuditLogWriteInput): Promise<void>
62
+ flush(): Promise<void>
63
+ close(): Promise<void>
64
+ }
65
+
66
+ class DisabledInvocationAudit implements AuditLog {
67
+ public readonly config = undefined
68
+ private warned = false
69
+
70
+ constructor(
71
+ private readonly wire: PikkuWire<any, any, any, CoreUserSession>
72
+ ) {}
73
+
74
+ async write(_event: AuditLogWriteInput): Promise<void> {
75
+ if (!this.warned) {
76
+ this.warned = true
77
+ ;(this.wire as any).logger?.warn?.(
78
+ 'audit.write() called for an invocation without wire.audit enabled'
79
+ )
80
+ }
81
+ }
82
+
83
+ async flush(): Promise<void> {}
84
+
85
+ async close(): Promise<void> {}
86
+ }
87
+
88
+ class InvocationAuditLog implements AuditLog {
89
+ private readonly buffer: AuditEvent[] = []
90
+
91
+ constructor(
92
+ public readonly config: ResolvedAuditConfig,
93
+ private readonly service: AuditService,
94
+ private readonly wire: PikkuWire<any, any, any, CoreUserSession>
95
+ ) {}
96
+
97
+ async write(event: AuditLogWriteInput): Promise<void> {
98
+ const resolved = this.resolveEvent(event)
99
+
100
+ if (this.config.durability === 'transactional') {
101
+ await this.service.audit(resolved)
102
+ return
103
+ }
104
+
105
+ this.buffer.push(resolved)
106
+ }
107
+
108
+ async flush(): Promise<void> {
109
+ if (
110
+ this.config.durability === 'transactional' ||
111
+ this.buffer.length === 0
112
+ ) {
113
+ return
114
+ }
115
+
116
+ const batch = this.buffer.splice(0, this.buffer.length)
117
+ const queryEvents = batch.filter((event) => event.queryId != null)
118
+ if (queryEvents.length === 1) {
119
+ queryEvents[0]!.queryId = null
120
+ }
121
+
122
+ try {
123
+ if (this.service.write) {
124
+ await this.service.write(batch)
125
+ return
126
+ }
127
+
128
+ for (const event of batch) {
129
+ await this.service.audit(event)
130
+ }
131
+ } catch (error) {
132
+ ;(this.wire as any).logger?.warn?.(
133
+ 'best-effort audit flush failed',
134
+ error
135
+ )
136
+ }
137
+ }
138
+
139
+ async close(): Promise<void> {
140
+ await this.flush()
141
+ }
142
+
143
+ private resolveEvent(event: AuditLogWriteInput): AuditEvent {
144
+ return {
145
+ functionId: this.wire.functionId,
146
+ wireType: this.wire.wireType,
147
+ wireId: this.wire.wireId,
148
+ traceId: this.wire.traceId,
149
+ actor: event.actor ?? resolveAuditActorFromWire(this.wire),
150
+ ...event,
151
+ occurredAt: new Date().toISOString(),
152
+ }
153
+ }
154
+ }
155
+
156
+ export const resolveAuditConfig = (
157
+ config?: AuditConfig
158
+ ): ResolvedAuditConfig | undefined => {
159
+ if (config === true) {
160
+ return { durability: 'best-effort' }
161
+ }
162
+
163
+ if (!config) {
164
+ return undefined
165
+ }
166
+
167
+ return {
168
+ durability: config.durability ?? 'best-effort',
169
+ }
170
+ }
171
+
172
+ export const createInvocationAudit = (
173
+ service: AuditService,
174
+ wire: PikkuWire<any, any, any, CoreUserSession>
175
+ ): AuditLog => {
176
+ if (!wire.audit) {
177
+ return new DisabledInvocationAudit(wire)
178
+ }
179
+
180
+ return new InvocationAuditLog(wire.audit, service, wire)
181
+ }
182
+
183
+ export const resolveAuditActorFromWire = (
184
+ wire: PikkuWire<any, any, any, CoreUserSession>
185
+ ): AuditActor | undefined => {
186
+ const session = wire.session as CoreUserSession | undefined
187
+ const actor: AuditActor = {
188
+ userId: session?.userId,
189
+ orgId: session?.orgId,
190
+ pikkuUserId: wire.pikkuUserId,
191
+ }
192
+
193
+ if (!actor.userId && !actor.orgId && !actor.pikkuUserId) {
194
+ return undefined
195
+ }
196
+
197
+ return actor
198
+ }
@@ -25,6 +25,7 @@ export interface GetUploadURLArgs<TBucket extends string = string> {
25
25
  fileKey: string
26
26
  contentType: string
27
27
  size?: number
28
+ visibility?: 'private' | 'public'
28
29
  }
29
30
 
30
31
  /**
@@ -0,0 +1,46 @@
1
+ export interface EmailTemplateReference {
2
+ name: string
3
+ locale?: string
4
+ data?: Record<string, unknown>
5
+ }
6
+
7
+ export interface BaseSendEmailInput {
8
+ to: string | string[]
9
+ from?: string
10
+ cc?: string | string[]
11
+ bcc?: string | string[]
12
+ replyTo?: string | string[]
13
+ headers?: Record<string, string>
14
+ subject?: string
15
+ }
16
+
17
+ export interface SendTextEmailInput extends BaseSendEmailInput {
18
+ text: string
19
+ html?: never
20
+ template?: never
21
+ }
22
+
23
+ export interface SendHTMLEmailInput extends BaseSendEmailInput {
24
+ html: string
25
+ text?: string
26
+ template?: never
27
+ }
28
+
29
+ export interface SendTemplateEmailInput extends BaseSendEmailInput {
30
+ template: EmailTemplateReference
31
+ html?: never
32
+ text?: never
33
+ }
34
+
35
+ export type SendEmailInput =
36
+ | SendTextEmailInput
37
+ | SendHTMLEmailInput
38
+ | SendTemplateEmailInput
39
+
40
+ export interface SendEmailResult {
41
+ messageId?: string
42
+ }
43
+
44
+ export interface EmailService {
45
+ send(input: SendEmailInput): Promise<SendEmailResult>
46
+ }