@pikku/core 0.12.22 → 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 +22 -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 +6 -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 +3 -1
  27. package/dist/wirings/ai-agent/ai-agent-stream.js +2 -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 +6 -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 +4 -1
  53. package/src/wirings/ai-agent/ai-agent-stream.ts +3 -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
@@ -86,6 +86,34 @@ export interface PermissionsGroupsMeta {
86
86
  httpGroups: Record<string, GroupMeta>;
87
87
  tagGroups: Record<string, GroupMeta>;
88
88
  }
89
+ export interface EmailTemplateLocaleMeta {
90
+ contentHash: string;
91
+ htmlHash: string;
92
+ subjectHash: string;
93
+ textHash: string;
94
+ }
95
+ export interface EmailTemplateMeta {
96
+ variables: string[];
97
+ hasHtml: boolean;
98
+ hasSubject: boolean;
99
+ hasText: boolean;
100
+ locales: Record<string, EmailTemplateLocaleMeta>;
101
+ }
102
+ export interface EmailsMeta {
103
+ src: string;
104
+ themeHash: string;
105
+ templates: Record<string, EmailTemplateMeta>;
106
+ }
107
+ export interface EmailTemplateAssets {
108
+ theme: Record<string, unknown>;
109
+ strings: Record<string, unknown>;
110
+ layout: string;
111
+ partials: Record<string, string>;
112
+ html: string;
113
+ subject: string;
114
+ text: string;
115
+ missing: string[];
116
+ }
89
117
  /**
90
118
  * Abstraction over .pikku metadata file access.
91
119
  * All paths are relative to the .pikku root directory.
@@ -100,6 +128,8 @@ export interface MetaService {
100
128
  readFile(relativePath: string): Promise<string | null>;
101
129
  /** List files in a directory by relative path. Returns empty array if not found. */
102
130
  readDir(relativePath: string): Promise<string[]>;
131
+ /** Read a project source file relative to the project root (one level above .pikku). */
132
+ readProjectFile(relativePath: string): Promise<string | null>;
103
133
  getHttpMeta(): Promise<HTTPWiringsMeta>;
104
134
  getChannelsMeta(): Promise<ChannelsMeta>;
105
135
  getSchedulerMeta(): Promise<ScheduledTasksMeta>;
@@ -117,6 +147,8 @@ export interface MetaService {
117
147
  getSecretsMeta(): Promise<SecretDefinitionsMeta>;
118
148
  getCredentialsMeta(): Promise<CredentialDefinitionsMeta>;
119
149
  getVariablesMeta(): Promise<VariableDefinitionsMeta>;
150
+ getEmailMeta(): Promise<EmailsMeta>;
151
+ getEmailTemplateAssets(templateName: string, locale: string): Promise<EmailTemplateAssets>;
120
152
  getServicesMeta(): Promise<ServicesMetaRecord>;
121
153
  getSchema(schemaName: string): Promise<JSONSchema7 | null>;
122
154
  getSchemas(schemaNames: string[]): Promise<Record<string, JSONSchema7 | null>>;
@@ -143,6 +175,7 @@ export declare class LocalMetaService implements MetaService {
143
175
  private secretsMetaCache;
144
176
  private credentialsMetaCache;
145
177
  private variablesMetaCache;
178
+ private emailMetaCache;
146
179
  private middlewareGroupsMetaCache;
147
180
  private permissionsGroupsMetaCache;
148
181
  private agentsMetaCache;
@@ -150,6 +183,7 @@ export declare class LocalMetaService implements MetaService {
150
183
  constructor(basePath: string);
151
184
  readFile(relativePath: string): Promise<string | null>;
152
185
  readDir(relativePath: string): Promise<string[]>;
186
+ readProjectFile(relativePath: string): Promise<string | null>;
153
187
  clearCache(): void;
154
188
  private readMetaJson;
155
189
  getHttpMeta(): Promise<HTTPWiringsMeta>;
@@ -169,6 +203,8 @@ export declare class LocalMetaService implements MetaService {
169
203
  getSecretsMeta(): Promise<SecretDefinitionsMeta>;
170
204
  getCredentialsMeta(): Promise<CredentialDefinitionsMeta>;
171
205
  getVariablesMeta(): Promise<VariableDefinitionsMeta>;
206
+ getEmailMeta(): Promise<EmailsMeta>;
207
+ getEmailTemplateAssets(templateName: string, locale: string): Promise<EmailTemplateAssets>;
172
208
  getServicesMeta(): Promise<ServicesMetaRecord>;
173
209
  getSchema(schemaName: string): Promise<JSONSchema7 | null>;
174
210
  getSchemas(schemaNames: string[]): Promise<Record<string, JSONSchema7 | null>>;
@@ -21,6 +21,7 @@ export class LocalMetaService {
21
21
  secretsMetaCache = null;
22
22
  credentialsMetaCache = null;
23
23
  variablesMetaCache = null;
24
+ emailMetaCache = null;
24
25
  middlewareGroupsMetaCache = null;
25
26
  permissionsGroupsMetaCache = null;
26
27
  agentsMetaCache = null;
@@ -44,6 +45,14 @@ export class LocalMetaService {
44
45
  return [];
45
46
  }
46
47
  }
48
+ async readProjectFile(relativePath) {
49
+ try {
50
+ return await readFile(join(this.basePath, '..', relativePath), 'utf-8');
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
47
56
  clearCache() {
48
57
  this.httpMetaCache = null;
49
58
  this.channelsMetaCache = null;
@@ -60,6 +69,7 @@ export class LocalMetaService {
60
69
  this.secretsMetaCache = null;
61
70
  this.credentialsMetaCache = null;
62
71
  this.variablesMetaCache = null;
72
+ this.emailMetaCache = null;
63
73
  this.middlewareGroupsMetaCache = null;
64
74
  this.permissionsGroupsMetaCache = null;
65
75
  this.agentsMetaCache = null;
@@ -257,6 +267,55 @@ export class LocalMetaService {
257
267
  this.variablesMetaCache = content ? JSON.parse(content) : {};
258
268
  return this.variablesMetaCache;
259
269
  }
270
+ async getEmailMeta() {
271
+ if (this.emailMetaCache)
272
+ return this.emailMetaCache;
273
+ const content = await this.readFile('email/pikku-emails-meta.gen.json');
274
+ this.emailMetaCache = content
275
+ ? JSON.parse(content)
276
+ : {
277
+ src: '',
278
+ themeHash: '',
279
+ templates: {},
280
+ };
281
+ return this.emailMetaCache;
282
+ }
283
+ async getEmailTemplateAssets(templateName, locale) {
284
+ const emailsMeta = await this.getEmailMeta();
285
+ if (!emailsMeta.src) {
286
+ throw new Error('No generated email metadata found. Run `pikku emails generate`.');
287
+ }
288
+ const baseDir = emailsMeta.src;
289
+ const [themeRaw, localeRaw, layoutRaw, footerRaw, templateHtml, templateSubject, templateText,] = await Promise.all([
290
+ this.readProjectFile(`${baseDir}/theme.json`),
291
+ this.readProjectFile(`${baseDir}/locales/${locale}.json`),
292
+ this.readProjectFile(`${baseDir}/partials/layout.html`),
293
+ this.readProjectFile(`${baseDir}/partials/footer.html`),
294
+ this.readProjectFile(`${baseDir}/templates/${templateName}.html`),
295
+ this.readProjectFile(`${baseDir}/templates/${templateName}.subject.txt`),
296
+ this.readProjectFile(`${baseDir}/templates/${templateName}.text.txt`),
297
+ ]);
298
+ return {
299
+ theme: themeRaw ? JSON.parse(themeRaw) : {},
300
+ strings: localeRaw
301
+ ? JSON.parse(localeRaw)
302
+ : {},
303
+ layout: layoutRaw ?? '',
304
+ partials: {
305
+ footer: footerRaw ?? '',
306
+ },
307
+ html: templateHtml ?? '',
308
+ subject: templateSubject ?? '',
309
+ text: templateText ?? '',
310
+ missing: [
311
+ ...(themeRaw ? [] : ['theme']),
312
+ ...(localeRaw ? [] : ['locale']),
313
+ ...(templateHtml ? [] : ['html']),
314
+ ...(templateSubject ? [] : ['subject']),
315
+ ...(templateText ? [] : ['text']),
316
+ ],
317
+ };
318
+ }
260
319
  async getServicesMeta() {
261
320
  if (this.servicesMetaCache)
262
321
  return this.servicesMetaCache;
@@ -25,8 +25,10 @@ import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js';
25
25
  import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js';
26
26
  import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js';
27
27
  import type { CredentialService } from '../services/credential-service.js';
28
+ import type { EmailService } from '../services/email-service.js';
28
29
  import type { MetaService } from '../services/meta-service.js';
29
30
  import type { SessionStore } from '../services/session-store.js';
31
+ import type { AuditDurability, AuditService } from '../services/audit-service.js';
30
32
  export type PikkuWiringTypes = 'http' | 'scheduler' | 'trigger' | 'channel' | 'rpc' | 'queue' | 'mcp' | 'cli' | 'workflow' | 'agent' | 'gateway';
31
33
  export interface FunctionServicesMeta {
32
34
  optimized: boolean;
@@ -143,6 +145,8 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
143
145
  * Represents a core user session, which can be extended for more specific session information.
144
146
  */
145
147
  export interface CoreUserSession {
148
+ userId?: string;
149
+ orgId?: string;
146
150
  }
147
151
  /**
148
152
  * Interface for core singleton services provided by Pikku.
@@ -184,8 +188,12 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
184
188
  workflowRunService?: WorkflowRunService;
185
189
  /** Credential service for dynamic/managed credentials (OAuth tokens, per-user API keys) */
186
190
  credentialService?: CredentialService;
191
+ /** Email service for outbound messages and template-backed delivery */
192
+ emailService?: EmailService;
187
193
  /** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
188
194
  metaService?: MetaService;
195
+ /** Audit service for durable or staged audit event capture */
196
+ audit?: AuditService;
189
197
  /** Session store for persisting user sessions keyed by pikkuUserId */
190
198
  sessionStore?: SessionStore;
191
199
  }
@@ -197,6 +205,8 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
197
205
  wireId: string;
198
206
  /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
199
207
  traceId: string;
208
+ /** Function id for the current invocation */
209
+ functionId: string;
200
210
  http: PikkuHTTP<In>;
201
211
  mcp: PikkuMCP<MCPTools>;
202
212
  rpc: TypedRPC;
@@ -211,7 +221,7 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
211
221
  gateway: PikkuGateway;
212
222
  session: HasInitialSession extends true ? UserSession : UserSession | undefined;
213
223
  /** Update and persist the current session */
214
- setSession: (session: CoreUserSession) => Promise<void> | void;
224
+ setSession: (session: UserSession) => Promise<void> | void;
215
225
  /** Clear and persist the current session */
216
226
  clearSession: () => Promise<void> | void;
217
227
  /** Fetch the latest session (may read from backing store) */
@@ -226,6 +236,10 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
226
236
  getCredential: <T = unknown>(name: string) => T | null | Promise<T | null>;
227
237
  /** Get all resolved credentials — lazy-loads from CredentialService on first call, sync thereafter */
228
238
  getCredentials: () => Record<string, unknown> | Promise<Record<string, unknown>>;
239
+ /** Resolved function audit metadata for this invocation, if enabled */
240
+ audit: {
241
+ durability: AuditDurability;
242
+ };
229
243
  }>;
230
244
  /**
231
245
  * A function that can wrap an wire and be called before or after
@@ -5,6 +5,8 @@ import { PikkuError } from '../../errors/error-handler.js';
5
5
  import type { SessionService } from '../../services/user-session-service.js';
6
6
  export type RunAIAgentParams = {
7
7
  sessionService?: SessionService<CoreUserSession>;
8
+ /** Credential accessor for the current request — used to read per-user overrides (e.g. AI_API_KEY). */
9
+ getCredential?: <T = unknown>(name: string) => T | null | Promise<T | null>;
8
10
  };
9
11
  export type StreamAIAgentOptions = {
10
12
  requiresToolApproval?: 'all' | 'explicit' | false;
@@ -3,7 +3,7 @@ import { checkAuthPermissions } from '../../permissions.js';
3
3
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
4
4
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
5
5
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js';
6
- import { randomUUID } from 'crypto';
6
+ import { randomUUID } from './ai-agent-utils.js';
7
7
  import { streamAIAgent } from './ai-agent-stream.js';
8
8
  import { runAIAgent } from './ai-agent-runner.js';
9
9
  import { resolveNamespace, ContextAwareRPCService, } from '../../wirings/rpc/rpc-runner.js';
@@ -434,10 +434,16 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
434
434
  export async function prepareAgentRun(agentName, input, params, agentSessionMap, streamContext) {
435
435
  const singletonServices = getSingletonServices();
436
436
  const { agent, packageName, resolvedName } = resolveAgent(agentName);
437
- const agentRunner = singletonServices.aiAgentRunner;
437
+ let agentRunner = singletonServices.aiAgentRunner;
438
438
  if (!agentRunner) {
439
439
  throw new AIProviderNotConfiguredError();
440
440
  }
441
+ if (params.getCredential && agentRunner.withApiKey) {
442
+ const aiCredential = await params.getCredential('AI_API_KEY');
443
+ if (aiCredential?.apiKey?.trim()) {
444
+ agentRunner = agentRunner.withApiKey(aiCredential.apiKey);
445
+ }
446
+ }
441
447
  const { storage } = resolveMemoryServices(agent, singletonServices);
442
448
  const memoryConfig = agent.memory;
443
449
  const threadId = input.threadId;
@@ -4,6 +4,7 @@ import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js';
4
4
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
5
5
  import { resolveModelConfig } from './ai-agent-model-config.js';
6
6
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
7
+ import { randomUUID } from './ai-agent-utils.js';
7
8
  function stripNulls(obj) {
8
9
  if (obj === null)
9
10
  return undefined;
@@ -19,7 +20,6 @@ function stripNulls(obj) {
19
20
  }
20
21
  return result;
21
22
  }
22
- import { randomUUID } from 'crypto';
23
23
  function extractStructuredAssistantOutput(object) {
24
24
  if (!object || typeof object !== 'object') {
25
25
  return { text: null, uiSpec: null };
@@ -33,6 +33,7 @@ function extractStructuredAssistantOutput(object) {
33
33
  export async function runAIAgent(agentName, input, params, agentSessionMap) {
34
34
  const sessionMap = agentSessionMap ?? new Map();
35
35
  const { agent, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, input, params, sessionMap);
36
+ runnerParams.agentId = agentName;
36
37
  const singletonServices = getSingletonServices();
37
38
  const { aiRunState } = singletonServices;
38
39
  if (!aiRunState) {
@@ -417,6 +418,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
417
418
  outputSchema: meta?.outputSchema
418
419
  ? pikkuState(packageName, 'misc', 'schemas').get(meta.outputSchema)
419
420
  : undefined,
421
+ agentId: run.agentName,
420
422
  };
421
423
  try {
422
424
  const accumulatedSteps = [];
@@ -1,7 +1,7 @@
1
1
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
2
2
  import { AIProviderNotConfiguredError } from '../../errors/errors.js';
3
+ import { randomUUID } from './ai-agent-utils.js';
3
4
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel/channel-middleware-runner.js';
4
- import { randomUUID } from 'crypto';
5
5
  import { resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
6
6
  import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
7
7
  import { resolveModelConfig } from './ai-agent-model-config.js';
@@ -354,6 +354,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
354
354
  const streamContext = { channel, options };
355
355
  // delegateState is attached after prepareAgentRun resolves the agent config
356
356
  const { agent, packageName, resolvedName, agentRunner, storage, memoryConfig, threadId, userMessage, runnerParams, maxSteps, missingRpcs, workingMemorySchemaName, } = await prepareAgentRun(agentName, normalizedInput, params, sessionMap, streamContext);
357
+ runnerParams.agentId = agentName;
357
358
  const singletonServices = getSingletonServices();
358
359
  const { aiRunState } = singletonServices;
359
360
  if (!aiRunState) {
@@ -0,0 +1 @@
1
+ export declare const randomUUID: () => string;
@@ -0,0 +1 @@
1
+ export const randomUUID = () => globalThis.crypto.randomUUID();
@@ -250,6 +250,7 @@ export class ContextAwareRPCService {
250
250
  run: async (agentName, input) => {
251
251
  const result = await runAIAgent(agentName, input, {
252
252
  sessionService: this.options.sessionService,
253
+ getCredential: this.wire.getCredential?.bind(this.wire),
253
254
  });
254
255
  return {
255
256
  runId: result.runId,
@@ -265,13 +266,19 @@ export class ContextAwareRPCService {
265
266
  const channel = this.wire.channel;
266
267
  if (!channel)
267
268
  throw new Error('No channel available for streaming');
268
- await streamAIAgent(agentName, input, channel, { sessionService: this.options.sessionService }, undefined, options);
269
+ await streamAIAgent(agentName, input, channel, {
270
+ sessionService: this.options.sessionService,
271
+ getCredential: this.wire.getCredential?.bind(this.wire),
272
+ }, undefined, options);
269
273
  },
270
274
  resume: async (runId, input, options) => {
271
275
  const channel = this.wire.channel;
272
276
  if (!channel)
273
277
  throw new Error('No channel available for streaming');
274
- await resumeAIAgent({ runId, ...input }, channel, { sessionService: this.options.sessionService }, options);
278
+ await resumeAIAgent({ runId, ...input }, channel, {
279
+ sessionService: this.options.sessionService,
280
+ getCredential: this.wire.getCredential?.bind(this.wire),
281
+ }, options);
275
282
  },
276
283
  approve: async (runId, approvals, expectedAgentName) => {
277
284
  const result = await resumeAIAgentSync(runId, approvals, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.22",
3
+ "version": "0.12.24",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -785,6 +785,55 @@ describe('runPikkuFunc - Integration Tests', () => {
785
785
  assert.deepEqual(receivedSession, { userId: 'user-1', loaded: true })
786
786
  })
787
787
 
788
+ test('should expose middleware-set session to createWireServices credential lookups', async () => {
789
+ let credentialLookups = 0
790
+ let receivedCredential: any
791
+ const sessionService = new PikkuSessionService<any>()
792
+
793
+ addTestFunction('credentialWire', {
794
+ middleware: [
795
+ async (_services: any, wire: any, next: any) => {
796
+ wire.setSession({ userId: 'user-1' })
797
+ await next()
798
+ },
799
+ ],
800
+ func: async (_services: any, _data: any, wire: any) => {
801
+ return wire.getSession()
802
+ },
803
+ })
804
+
805
+ await runPikkuFunc('rpc', Math.random().toString(), 'credentialWire', {
806
+ singletonServices: {
807
+ ...mockSingletonServices,
808
+ credentialService: {
809
+ getAll: async (userId: string) => {
810
+ credentialLookups++
811
+ assert.equal(userId, 'user-1')
812
+ return {
813
+ 'user-oauth': { accessToken: 'token-1' },
814
+ }
815
+ },
816
+ },
817
+ } as any,
818
+ createWireServices: async (_services: any, wire: any) => {
819
+ receivedCredential = await wire.getCredential('user-oauth')
820
+ return {}
821
+ },
822
+ data: () => ({}),
823
+ auth: false,
824
+ wire: {
825
+ session: undefined,
826
+ setSession: (session: any) => sessionService.setInitial(session),
827
+ getSession: () => sessionService.get(),
828
+ hasSessionChanged: () => sessionService.sessionChanged,
829
+ } as any,
830
+ sessionService,
831
+ })
832
+
833
+ assert.equal(credentialLookups, 1)
834
+ assert.deepEqual(receivedCredential, { accessToken: 'token-1' })
835
+ })
836
+
788
837
  test('should pass addon-scoped singleton services and wrap bare workflow names', async () => {
789
838
  pikkuState(null, 'addons', 'packages').set('stripe', {
790
839
  package: '@addon/stripe',
@@ -969,6 +1018,212 @@ describe('runPikkuFunc - Integration Tests', () => {
969
1018
  assert.equal(result, true)
970
1019
  assert.ok(receivedWire.rpc)
971
1020
  })
1021
+
1022
+ test('should expose resolved audit metadata on the wire for audited functions', async () => {
1023
+ let receivedAudit: any
1024
+
1025
+ addTestFunction('auditedWire', {
1026
+ audit: { durability: 'transactional' },
1027
+ func: async (_services: any, _data: any, wire: any) => {
1028
+ receivedAudit = wire.audit
1029
+ return 'ok'
1030
+ },
1031
+ })
1032
+
1033
+ const result = await runPikkuFunc('rpc', 'wire-audit-on', 'auditedWire', {
1034
+ singletonServices: mockSingletonServices,
1035
+ data: () => ({}),
1036
+ auth: false,
1037
+ wire: {},
1038
+ })
1039
+
1040
+ assert.equal(result, 'ok')
1041
+ assert.deepEqual(receivedAudit, { durability: 'transactional' })
1042
+ })
1043
+
1044
+ test('should leave wire audit metadata undefined when audit is disabled', async () => {
1045
+ let receivedWire: any
1046
+
1047
+ addTestFunction('plainWire', {
1048
+ func: async (_services: any, _data: any, wire: any) => {
1049
+ receivedWire = wire
1050
+ return 'ok'
1051
+ },
1052
+ })
1053
+
1054
+ const result = await runPikkuFunc('rpc', 'wire-audit-off', 'plainWire', {
1055
+ singletonServices: mockSingletonServices,
1056
+ data: () => ({}),
1057
+ auth: false,
1058
+ wire: {},
1059
+ })
1060
+
1061
+ assert.equal(result, 'ok')
1062
+ assert.equal(receivedWire.audit, undefined)
1063
+ })
1064
+
1065
+ test('should restore parent audit metadata after nested non-audited calls', async () => {
1066
+ let auditBeforeChild: any
1067
+ let auditAfterChild: any
1068
+
1069
+ addTestFunction('childPlain', {
1070
+ func: async () => 'child-ok',
1071
+ })
1072
+
1073
+ addTestFunction('parentAudited', {
1074
+ audit: true,
1075
+ func: async (_services: any, _data: any, wire: any) => {
1076
+ auditBeforeChild = wire.audit
1077
+ await runPikkuFunc('rpc', 'nested-child-wire', 'childPlain', {
1078
+ singletonServices: mockSingletonServices,
1079
+ data: () => ({}),
1080
+ auth: false,
1081
+ wire,
1082
+ })
1083
+ auditAfterChild = wire.audit
1084
+ return 'parent-ok'
1085
+ },
1086
+ })
1087
+
1088
+ const result = await runPikkuFunc(
1089
+ 'rpc',
1090
+ 'nested-parent-wire',
1091
+ 'parentAudited',
1092
+ {
1093
+ singletonServices: mockSingletonServices,
1094
+ data: () => ({}),
1095
+ auth: false,
1096
+ wire: {},
1097
+ }
1098
+ )
1099
+
1100
+ assert.equal(result, 'parent-ok')
1101
+ assert.deepEqual(auditBeforeChild, { durability: 'best-effort' })
1102
+ assert.deepEqual(auditAfterChild, { durability: 'best-effort' })
1103
+ })
1104
+
1105
+ test('should preserve parent audit metadata across rpc sub-calls to non-audited functions', async () => {
1106
+ let parentAuditBeforeChild: any
1107
+ let parentAuditAfterChild: any
1108
+ let parentFunctionIdBeforeChild: any
1109
+ let parentFunctionIdAfterChild: any
1110
+ let childAudit: any
1111
+ let childFunctionId: any
1112
+
1113
+ addTestFunction('rpcChildPlain', {
1114
+ func: async (_services: any, _data: any, wire: any) => {
1115
+ childAudit = wire.audit
1116
+ childFunctionId = wire.functionId
1117
+ return 'child-ok'
1118
+ },
1119
+ })
1120
+
1121
+ addTestFunction('rpcParentAudited', {
1122
+ audit: true,
1123
+ func: async (_services: any, _data: any, wire: any) => {
1124
+ parentAuditBeforeChild = wire.audit
1125
+ parentFunctionIdBeforeChild = wire.functionId
1126
+ const childResult = await wire.rpc.invoke('rpcChildPlain', {})
1127
+ parentAuditAfterChild = wire.audit
1128
+ parentFunctionIdAfterChild = wire.functionId
1129
+ return childResult
1130
+ },
1131
+ })
1132
+
1133
+ const result = await runPikkuFunc(
1134
+ 'rpc',
1135
+ 'rpc-parent-wire',
1136
+ 'rpcParentAudited',
1137
+ {
1138
+ singletonServices: mockSingletonServices,
1139
+ data: () => ({}),
1140
+ auth: false,
1141
+ wire: {},
1142
+ }
1143
+ )
1144
+
1145
+ assert.equal(result, 'child-ok')
1146
+ assert.deepEqual(parentAuditBeforeChild, { durability: 'best-effort' })
1147
+ assert.deepEqual(parentAuditAfterChild, { durability: 'best-effort' })
1148
+ assert.equal(parentFunctionIdBeforeChild, 'rpcParentAudited')
1149
+ assert.equal(parentFunctionIdAfterChild, 'rpcParentAudited')
1150
+ assert.equal(childAudit, undefined)
1151
+ assert.equal(childFunctionId, 'rpcChildPlain')
1152
+ })
1153
+
1154
+ test('should bind audited rpc sub-calls to the child invocation context', async () => {
1155
+ let parentAuditBeforeChild: any
1156
+ let parentAuditAfterChild: any
1157
+ let parentFunctionIdAfterChild: any
1158
+ let childAudit: any
1159
+ let childFunctionId: any
1160
+
1161
+ addTestFunction('rpcChildAudited', {
1162
+ audit: { durability: 'transactional' },
1163
+ func: async (_services: any, _data: any, wire: any) => {
1164
+ childAudit = wire.audit
1165
+ childFunctionId = wire.functionId
1166
+ return 'child-audited-ok'
1167
+ },
1168
+ })
1169
+
1170
+ addTestFunction('rpcParentAuditedChild', {
1171
+ audit: true,
1172
+ func: async (_services: any, _data: any, wire: any) => {
1173
+ parentAuditBeforeChild = wire.audit
1174
+ const childResult = await wire.rpc.invoke('rpcChildAudited', {})
1175
+ parentAuditAfterChild = wire.audit
1176
+ parentFunctionIdAfterChild = wire.functionId
1177
+ return childResult
1178
+ },
1179
+ })
1180
+
1181
+ const result = await runPikkuFunc(
1182
+ 'rpc',
1183
+ 'rpc-parent-child-audit-wire',
1184
+ 'rpcParentAuditedChild',
1185
+ {
1186
+ singletonServices: mockSingletonServices,
1187
+ data: () => ({}),
1188
+ auth: false,
1189
+ wire: {},
1190
+ }
1191
+ )
1192
+
1193
+ assert.equal(result, 'child-audited-ok')
1194
+ assert.deepEqual(parentAuditBeforeChild, { durability: 'best-effort' })
1195
+ assert.deepEqual(parentAuditAfterChild, { durability: 'best-effort' })
1196
+ assert.equal(parentFunctionIdAfterChild, 'rpcParentAuditedChild')
1197
+ assert.deepEqual(childAudit, { durability: 'transactional' })
1198
+ assert.equal(childFunctionId, 'rpcChildAudited')
1199
+ })
1200
+
1201
+ test('should expose the resolved base function id after version fallback', async () => {
1202
+ let receivedFunctionId: any
1203
+
1204
+ addTestFunction('versionedBase', {
1205
+ audit: true,
1206
+ func: async (_services: any, _data: any, wire: any) => {
1207
+ receivedFunctionId = wire.functionId
1208
+ return 'ok'
1209
+ },
1210
+ })
1211
+
1212
+ const result = await runPikkuFunc(
1213
+ 'rpc',
1214
+ 'wire-version-fallback',
1215
+ 'versionedBase@v2',
1216
+ {
1217
+ singletonServices: mockSingletonServices,
1218
+ data: () => ({}),
1219
+ auth: false,
1220
+ wire: {},
1221
+ }
1222
+ )
1223
+
1224
+ assert.equal(result, 'ok')
1225
+ assert.equal(receivedFunctionId, 'versionedBase')
1226
+ })
972
1227
  })
973
1228
 
974
1229
  describe('function-runner helpers', () => {