@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
@@ -517,13 +517,13 @@ export function defineServiceTests(config) {
517
517
  const factory = services.secretService;
518
518
  const kek = 'test-key-encryption-key-32chars!';
519
519
  describe(`SecretService [${name}]`, () => {
520
- test('setSecretJSON and getSecretJSON', async () => {
520
+ test('setSecret and getSecret', async () => {
521
521
  const service = await factory({ key: kek });
522
- await service.setSecretJSON('api-key', {
522
+ await service.setSecret('api-key', {
523
523
  token: 'sk-123',
524
524
  endpoint: 'https://api.example.com',
525
525
  });
526
- const result = await service.getSecretJSON('api-key');
526
+ const result = await service.getSecret('api-key');
527
527
  assert.deepEqual(result, {
528
528
  token: 'sk-123',
529
529
  endpoint: 'https://api.example.com',
@@ -531,9 +531,9 @@ export function defineServiceTests(config) {
531
531
  });
532
532
  test('getSecret returns raw string', async () => {
533
533
  const service = await factory({ key: kek });
534
- await service.setSecretJSON('string-secret', 'plain-value');
534
+ await service.setSecret('string-secret', 'plain-value');
535
535
  const result = await service.getSecret('string-secret');
536
- assert.strictEqual(result, '"plain-value"');
536
+ assert.strictEqual(result, 'plain-value');
537
537
  });
538
538
  test('hasSecret returns true/false', async () => {
539
539
  const service = await factory({ key: kek });
@@ -546,16 +546,16 @@ export function defineServiceTests(config) {
546
546
  message: 'Requested secret not found',
547
547
  });
548
548
  });
549
- test('setSecretJSON upserts existing key', async () => {
549
+ test('setSecret upserts existing key', async () => {
550
550
  const service = await factory({ key: kek });
551
- await service.setSecretJSON('upsert-key', { v: 1 });
552
- await service.setSecretJSON('upsert-key', { v: 2 });
553
- const result = await service.getSecretJSON('upsert-key');
551
+ await service.setSecret('upsert-key', { v: 1 });
552
+ await service.setSecret('upsert-key', { v: 2 });
553
+ const result = await service.getSecret('upsert-key');
554
554
  assert.deepEqual(result, { v: 2 });
555
555
  });
556
556
  test('deleteSecret removes the key', async () => {
557
557
  const service = await factory({ key: kek });
558
- await service.setSecretJSON('to-delete', 'bye');
558
+ await service.setSecret('to-delete', 'bye');
559
559
  assert.strictEqual(await service.hasSecret('to-delete'), true);
560
560
  await service.deleteSecret('to-delete');
561
561
  assert.strictEqual(await service.hasSecret('to-delete'), false);
@@ -563,13 +563,13 @@ export function defineServiceTests(config) {
563
563
  test('rotateKEK re-wraps all secrets', async () => {
564
564
  const newKEK = 'new-key-encryption-key-rotated!';
565
565
  const oldService = await factory({ key: kek });
566
- await oldService.setSecretJSON('rotate-test', { important: 'data' });
566
+ await oldService.setSecret('rotate-test', { important: 'data' });
567
567
  const rotatedService = await factory({
568
568
  key: newKEK,
569
569
  keyVersion: 2,
570
570
  previousKey: kek,
571
571
  });
572
- const before = await rotatedService.getSecretJSON('rotate-test');
572
+ const before = await rotatedService.getSecret('rotate-test');
573
573
  assert.deepEqual(before, { important: 'data' });
574
574
  assert.ok(rotatedService.rotateKEK);
575
575
  const count = await rotatedService.rotateKEK();
@@ -578,7 +578,7 @@ export function defineServiceTests(config) {
578
578
  key: newKEK,
579
579
  keyVersion: 2,
580
580
  });
581
- const after = await newOnlyService.getSecretJSON('rotate-test');
581
+ const after = await newOnlyService.getSecret('rotate-test');
582
582
  assert.deepEqual(after, { important: 'data' });
583
583
  });
584
584
  test('rotateKEK throws without previousKey', async () => {
@@ -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;
@@ -488,7 +494,10 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
488
494
  const trimmedMessages = trimMessages(allMessages);
489
495
  const aiMiddlewares = agent.aiMiddleware ?? [];
490
496
  const { tools, missingRpcs } = await buildToolDefs(params, agentSessionMap, input.resourceId, resolvedName, packageName, streamContext, aiMiddlewares, agent.agentMode);
491
- const instructions = await buildInstructions(resolvedName, packageName);
497
+ let instructions = await buildInstructions(resolvedName, packageName);
498
+ if (input.context) {
499
+ instructions = `${instructions}\n\nCurrent context (use these identifiers directly in tool calls — do not ask the user for them):\n${input.context}`;
500
+ }
492
501
  const resolved = resolveModelConfig(resolvedName, agent);
493
502
  // Per-request overrides
494
503
  if (input.model) {
@@ -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 };
@@ -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';
@@ -0,0 +1 @@
1
+ export declare const randomUUID: () => string;
@@ -0,0 +1 @@
1
+ export const randomUUID = () => globalThis.crypto.randomUUID();
@@ -76,6 +76,10 @@ export interface AIAgentInput {
76
76
  attachments?: AIAgentInputAttachment[];
77
77
  model?: string;
78
78
  temperature?: number;
79
+ /** Structured context injected into the system instructions for this request.
80
+ * Use to provide upfront state (e.g. current org/project/branch/deployment)
81
+ * so the agent can call tools without asking the user for identifiers. */
82
+ context?: string;
79
83
  }
80
84
  export interface AIAgentOutput {
81
85
  runId: string;
@@ -98,7 +98,7 @@ export class OAuth2Client {
98
98
  throw new Error('OAuth2 token expired and no refresh token available');
99
99
  }
100
100
  // Load from secrets
101
- const token = await this.secrets.getSecretJSON(this.oauth2Config.tokenSecretId);
101
+ const token = await this.secrets.getSecret(this.oauth2Config.tokenSecretId);
102
102
  this.cachedToken = token;
103
103
  // Check if loaded token is expired and needs refresh
104
104
  if (!this.isTokenValid(token)) {
@@ -167,7 +167,7 @@ export class OAuth2Client {
167
167
  tokenType: data.token_type || 'Bearer',
168
168
  scope: data.scope,
169
169
  };
170
- await this.secrets.setSecretJSON(this.oauth2Config.tokenSecretId, token);
170
+ await this.secrets.setSecret(this.oauth2Config.tokenSecretId, token);
171
171
  this.cachedToken = token;
172
172
  return token;
173
173
  }
@@ -188,7 +188,7 @@ export class OAuth2Client {
188
188
  return this.cachedAppCredential;
189
189
  }
190
190
  this.cachedAppCredential =
191
- await this.secrets.getSecretJSON(this.appCredentialSecretId);
191
+ await this.secrets.getSecret(this.appCredentialSecretId);
192
192
  return this.cachedAppCredential;
193
193
  }
194
194
  // ========================================
@@ -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.23",
3
+ "version": "0.12.25",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -0,0 +1,15 @@
1
+ export type Private<T> = T & { readonly __pii__: 'private' }
2
+ export type Secret<T> = T & { readonly __pii__: 'secret' }
3
+
4
+ export type Classification = 'public' | 'private' | 'secret'
5
+ export type AnonymizeStrategy = 'fake:email' | 'fake:name' | 'hash' | 'keep' | null
6
+
7
+ export interface ColumnClassification {
8
+ classification: Classification
9
+ anonymize_strategy: AnonymizeStrategy
10
+ }
11
+
12
+ export type ClassificationManifest = {
13
+ version: 1
14
+ tables: Record<string, Record<string, ColumnClassification>>
15
+ }
@@ -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', () => {