@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
|
@@ -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('
|
|
520
|
+
test('setSecret and getSecret', async () => {
|
|
521
521
|
const service = await factory({ key: kek });
|
|
522
|
-
await service.
|
|
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.
|
|
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.
|
|
534
|
+
await service.setSecret('string-secret', 'plain-value');
|
|
535
535
|
const result = await service.getSecret('string-secret');
|
|
536
|
-
assert.strictEqual(result, '
|
|
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('
|
|
549
|
+
test('setSecret upserts existing key', async () => {
|
|
550
550
|
const service = await factory({ key: kek });
|
|
551
|
-
await service.
|
|
552
|
-
await service.
|
|
553
|
-
const result = await service.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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, {
|
|
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, {
|
|
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
|
@@ -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', () => {
|