@pikku/core 0.12.8 → 0.12.10
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 +31 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +1 -0
- package/dist/errors/errors.d.ts +14 -0
- package/dist/errors/errors.js +27 -0
- package/dist/function/function-runner.d.ts +3 -1
- package/dist/function/function-runner.js +5 -1
- package/dist/handle-error.js +2 -1
- package/dist/middleware/auth-bearer.js +10 -1
- package/dist/middleware/auth-cookie.js +34 -25
- package/dist/middleware/timeout.js +11 -5
- package/dist/pikku-state.js +1 -1
- package/dist/services/credential-service.d.ts +40 -0
- package/dist/services/credential-service.js +1 -0
- package/dist/services/credential-wire-service.d.ts +13 -0
- package/dist/services/credential-wire-service.js +31 -0
- package/dist/services/index.d.ts +5 -0
- package/dist/services/index.js +3 -0
- package/dist/services/local-content.d.ts +7 -4
- package/dist/services/local-content.js +42 -12
- package/dist/services/local-credential-service.d.ts +10 -0
- package/dist/services/local-credential-service.js +33 -0
- package/dist/services/local-secrets.js +2 -2
- package/dist/services/typed-credential-service.d.ts +27 -0
- package/dist/services/typed-credential-service.js +40 -0
- package/dist/testing/service-tests.d.ts +8 -0
- package/dist/testing/service-tests.js +107 -1
- package/dist/types/core.types.d.ts +7 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
- package/dist/wirings/ai-agent/ai-agent-prepare.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
- package/dist/wirings/channel/local/local-channel-runner.js +11 -6
- package/dist/wirings/credential/credential.types.d.ts +24 -0
- package/dist/wirings/credential/credential.types.js +1 -0
- package/dist/wirings/credential/index.d.ts +3 -0
- package/dist/wirings/credential/index.js +2 -0
- package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
- package/dist/wirings/credential/validate-credential-definitions.js +38 -0
- package/dist/wirings/credential/wire-credential.d.ts +48 -0
- package/dist/wirings/credential/wire-credential.js +47 -0
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/pikku-fetch-http-request.js +11 -1
- package/dist/wirings/oauth2/index.d.ts +2 -1
- package/dist/wirings/oauth2/index.js +1 -1
- package/dist/wirings/oauth2/oauth2-client.js +4 -8
- package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
- package/dist/wirings/oauth2/oauth2-routes.js +146 -0
- package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
- package/dist/wirings/workflow/graph/graph-runner.d.ts +8 -1
- package/dist/wirings/workflow/graph/graph-runner.js +87 -7
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +5 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +99 -24
- package/dist/wirings/workflow/workflow.types.d.ts +4 -0
- package/package.json +2 -1
- package/src/env.ts +1 -0
- package/src/errors/errors.ts +35 -0
- package/src/function/function-runner.ts +11 -0
- package/src/handle-error.ts +2 -1
- package/src/middleware/auth-bearer.ts +10 -1
- package/src/middleware/auth-cookie.ts +11 -4
- package/src/middleware/timeout.ts +14 -7
- package/src/pikku-state.ts +1 -1
- package/src/services/credential-service.ts +44 -0
- package/src/services/credential-wire-service.ts +44 -0
- package/src/services/index.ts +12 -0
- package/src/services/local-content.ts +61 -13
- package/src/services/local-credential-service.ts +41 -0
- package/src/services/local-secrets.test.ts +2 -2
- package/src/services/local-secrets.ts +2 -2
- package/src/services/typed-credential-service.ts +75 -0
- package/src/testing/service-tests.ts +140 -1
- package/src/types/core.types.ts +7 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
- package/src/wirings/ai-agent/ai-agent-prepare.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +34 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +2 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +2 -1
- package/src/wirings/channel/local/local-channel-runner.ts +11 -6
- package/src/wirings/credential/credential.types.ts +28 -0
- package/src/wirings/credential/index.ts +8 -0
- package/src/wirings/credential/validate-credential-definitions.ts +64 -0
- package/src/wirings/credential/wire-credential.ts +49 -0
- package/src/wirings/http/http-runner.ts +11 -2
- package/src/wirings/http/pikku-fetch-http-request.ts +11 -1
- package/src/wirings/oauth2/index.ts +2 -1
- package/src/wirings/oauth2/oauth2-client.ts +4 -8
- package/src/wirings/oauth2/oauth2-routes.ts +234 -0
- package/src/wirings/oauth2/oauth2.types.ts +0 -27
- package/src/wirings/workflow/graph/graph-runner.test.ts +42 -0
- package/src/wirings/workflow/graph/graph-runner.ts +117 -8
- package/src/wirings/workflow/pikku-workflow-service.test.ts +109 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +150 -34
- package/src/wirings/workflow/workflow.types.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
- package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
- package/src/wirings/oauth2/wire-oauth2-credential.ts +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
## 0.12.4
|
|
2
2
|
|
|
3
|
+
## 0.12.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0f59432: Add per-user credential system with CredentialService, OAuth2 route handlers, and KyselyCredentialService with envelope encryption
|
|
8
|
+
- 52b64d1: Provide workflow wire to graph nodes for sleep/suspend support. Graph nodes now receive a workflow wire alongside the graph wire, enabling tools like `graph:sleep` to work in graph execution context. Improves dynamic workflow system instructions with output path documentation, nested path examples, and design principles for AI agents.
|
|
9
|
+
|
|
10
|
+
## 0.12.9
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- e412b4d: Replace raw Error throws in AI agent runner/stream/prepare with typed PikkuError subclasses. `AIProviderNotConfiguredError` (503) replaces "AIAgentRunnerService not available" with a user-friendly message. `AIProviderAuthError` (401) available for API key validation errors.
|
|
15
|
+
- 53dc8c8: Fix toWebRequest to respect x-forwarded-proto and x-forwarded-host headers behind reverse proxies. Previously always used http:// which broke OAuth callback URLs behind TLS-terminating proxies like Fly.io.
|
|
16
|
+
- 0a1cc51: Add secure defaults for cookie authentication: httpOnly, secure, sameSite 'lax', and path '/'. User-provided options override these defaults.
|
|
17
|
+
- 0a1cc51: Prevent internal error details from leaking to clients. Stack traces via exposeErrors are now blocked in production. SSE and WebSocket error handlers use registered error responses instead of raw error messages. Secret key names and route paths are no longer included in error messages.
|
|
18
|
+
- 0a1cc51: Cap form-urlencoded parameters at 256 to prevent abuse via unbounded parameter parsing.
|
|
19
|
+
- 0a1cc51: Add path traversal protection to LocalContent file operations. Asset keys are now validated to stay within the configured upload directory.
|
|
20
|
+
- 0a1cc51: Use private Symbol for global pikku state key to prevent external code from accessing framework internals via Symbol.for().
|
|
21
|
+
- 0a1cc51: Filter out **proto**, constructor, and prototype keys during request data merging to prevent prototype pollution.
|
|
22
|
+
- 0a1cc51: Improve LocalContent URL signing with proper signedAt/expiresAt parameters. When an optional JWTService is provided, URLs include a cryptographic signature for verification.
|
|
23
|
+
- 0a1cc51: Fix timeout middleware to use Promise.race instead of throwing inside setTimeout, which caused uncatchable exceptions that crashed the process.
|
|
24
|
+
- 0a1cc51: Use constant-time comparison for static bearer token authentication to prevent timing side-channel attacks.
|
|
25
|
+
- 8b9b2e9: Fix child workflow completion in queued execution mode. When a sub-workflow completes, the parent step is now marked as succeeded and the parent orchestrator resumes automatically via `onChildWorkflowCompleted`. Adds `parentStepId` to `WorkflowRunWire` to track the parent step without querying. Retains advisory locks in PgKyselyWorkflowService for concurrency safety. Fixes pgboss `registerQueues` to accept an optional logger parameter.
|
|
26
|
+
- 8b9b2e9: Add debug-level logging to workflow service for step scheduling, execution, and orchestration to aid troubleshooting.
|
|
27
|
+
- b973d44: Add `inline` property to workflow function definitions. When `inline: true` is set on a workflow, it always executes inline without dispatching to the queue service, even when a queue service is available. This is useful for workflows that should run synchronously within the parent process (e.g. scaffolding/setup steps that produce local files).
|
|
28
|
+
|
|
29
|
+
The flag flows from the function definition through the inspector, into the serialized workflow graph, and is checked at runtime by the workflow service.
|
|
30
|
+
|
|
31
|
+
- 8b9b2e9: Strip undefined values from workflow step data before dispatching to the queue service, preventing postgres UNDEFINED_VALUE errors.
|
|
32
|
+
- 8b9b2e9: Support sub-workflow invocation in graph-based workflow steps. When a step's rpcName refers to a registered workflow instead of an RPC function, `executeGraphStep` now starts it as a child workflow and polls for completion. Respects the `inline` meta flag on the sub-workflow.
|
|
33
|
+
|
|
3
34
|
## 0.12.8
|
|
4
35
|
|
|
5
36
|
### Patch Changes
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isProduction: () => boolean;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isProduction = () => process.env.NODE_ENV === 'production';
|
package/dist/errors/errors.d.ts
CHANGED
|
@@ -204,3 +204,17 @@ export declare class MaxComputeTimeReachedError extends PikkuError {
|
|
|
204
204
|
*/
|
|
205
205
|
export declare class MissingSchemaError extends PikkuError {
|
|
206
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* An AI provider has not been configured. Set up an AI provider (e.g. OpenAI, Anthropic) to use agent features.
|
|
209
|
+
* @group Error
|
|
210
|
+
*/
|
|
211
|
+
export declare class AIProviderNotConfiguredError extends PikkuError {
|
|
212
|
+
constructor();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The AI provider API key is missing or invalid.
|
|
216
|
+
* @group Error
|
|
217
|
+
*/
|
|
218
|
+
export declare class AIProviderAuthError extends PikkuError {
|
|
219
|
+
constructor(message?: string);
|
|
220
|
+
}
|
package/dist/errors/errors.js
CHANGED
|
@@ -347,3 +347,30 @@ addError(MissingSchemaError, {
|
|
|
347
347
|
status: 500,
|
|
348
348
|
message: 'A required schema was not found. Ensure schema generation has been run.',
|
|
349
349
|
});
|
|
350
|
+
/**
|
|
351
|
+
* An AI provider has not been configured. Set up an AI provider (e.g. OpenAI, Anthropic) to use agent features.
|
|
352
|
+
* @group Error
|
|
353
|
+
*/
|
|
354
|
+
export class AIProviderNotConfiguredError extends PikkuError {
|
|
355
|
+
constructor() {
|
|
356
|
+
super('No AI provider configured. Please set up an AI provider (e.g. OpenAI, Anthropic) and provide a valid API key to use this agent.');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
addError(AIProviderNotConfiguredError, {
|
|
360
|
+
status: 503,
|
|
361
|
+
message: 'No AI provider configured. Please set up an AI provider (e.g. OpenAI, Anthropic) and provide a valid API key to use this agent.',
|
|
362
|
+
});
|
|
363
|
+
/**
|
|
364
|
+
* The AI provider API key is missing or invalid.
|
|
365
|
+
* @group Error
|
|
366
|
+
*/
|
|
367
|
+
export class AIProviderAuthError extends PikkuError {
|
|
368
|
+
constructor(message) {
|
|
369
|
+
super(message ||
|
|
370
|
+
'AI provider API key is missing or invalid. Please check your API key configuration.');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
addError(AIProviderAuthError, {
|
|
374
|
+
status: 401,
|
|
375
|
+
message: 'AI provider API key is missing or invalid. Please check your API key configuration.',
|
|
376
|
+
});
|
|
@@ -2,11 +2,12 @@ import type { CoreServices, CoreUserSession, CorePikkuMiddleware, PikkuWiringTyp
|
|
|
2
2
|
import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
|
|
3
3
|
import type { CorePermissionGroup, CorePikkuFunctionConfig, CorePikkuPermission } from './functions.types.js';
|
|
4
4
|
import type { SessionService } from '../services/user-session-service.js';
|
|
5
|
+
import type { PikkuCredentialWireService } from '../services/credential-wire-service.js';
|
|
5
6
|
export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any>, packageName?: string | null) => void;
|
|
6
7
|
export declare const getFunctionNames: (packageName?: string | null) => string[];
|
|
7
8
|
export declare const getAllFunctionNames: () => string[];
|
|
8
9
|
export declare const runPikkuFuncDirectly: <In, Out>(funcName: string, allServices: CoreServices, wire: PikkuWire, data: In, userSession?: SessionService<CoreUserSession>, packageName?: string | null) => Promise<Out>;
|
|
9
|
-
export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags, wire, sessionService, packageName, }: {
|
|
10
|
+
export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTypes, wireId: string, funcName: string, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags, wire, sessionService, credentialWireService, packageName, }: {
|
|
10
11
|
singletonServices: CoreSingletonServices;
|
|
11
12
|
createWireServices?: CreateWireServices;
|
|
12
13
|
data: () => Promise<In> | In;
|
|
@@ -21,5 +22,6 @@ export declare const runPikkuFunc: <In = any, Out = any>(wireType: PikkuWiringTy
|
|
|
21
22
|
tags?: string[];
|
|
22
23
|
wire: PikkuWire;
|
|
23
24
|
sessionService?: SessionService<CoreUserSession>;
|
|
25
|
+
credentialWireService?: PikkuCredentialWireService;
|
|
24
26
|
packageName?: string | null;
|
|
25
27
|
}) => Promise<Out>;
|
|
@@ -6,6 +6,7 @@ import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js';
|
|
|
6
6
|
import { parseVersionedId } from '../version.js';
|
|
7
7
|
import { createFunctionSessionWireProps } from '../services/user-session-service.js';
|
|
8
8
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js';
|
|
9
|
+
import { createWireServicesCredentialWireProps } from '../services/credential-wire-service.js';
|
|
9
10
|
import { rpcService } from '../wirings/rpc/rpc-runner.js';
|
|
10
11
|
import { closeWireServices } from '../utils.js';
|
|
11
12
|
/**
|
|
@@ -70,7 +71,7 @@ export const runPikkuFuncDirectly = async (funcName, allServices, wire, data, us
|
|
|
70
71
|
};
|
|
71
72
|
return (await funcConfig.func(allServices, data, wireWithSession));
|
|
72
73
|
};
|
|
73
|
-
export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags = [], wire, sessionService, packageName = null, }) => {
|
|
74
|
+
export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServices, createWireServices, data, auth: wiringAuth, inheritedMiddleware, wireMiddleware, inheritedChannelMiddleware, wireChannelMiddleware, inheritedPermissions, wirePermissions, coerceDataFromSchema, tags = [], wire, sessionService, credentialWireService, packageName = null, }) => {
|
|
74
75
|
wire.wireType ??= wireType;
|
|
75
76
|
wire.wireId ??= wireId;
|
|
76
77
|
const funcMap = pikkuState(packageName, 'function', 'functions');
|
|
@@ -188,6 +189,9 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
188
189
|
packageName,
|
|
189
190
|
});
|
|
190
191
|
}
|
|
192
|
+
if (credentialWireService) {
|
|
193
|
+
Object.assign(resolvedWire, createWireServicesCredentialWireProps(credentialWireService));
|
|
194
|
+
}
|
|
191
195
|
const wireServices = await resolvedCreateWireServices?.(resolvedSingletonServices, resolvedWire);
|
|
192
196
|
try {
|
|
193
197
|
const services = wireServices && Object.keys(wireServices).length > 0
|
package/dist/handle-error.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isProduction } from './env.js';
|
|
1
2
|
import { getErrorResponse } from './errors/error-handler.js';
|
|
2
3
|
import { NotFoundError } from './errors/errors.js';
|
|
3
4
|
/**
|
|
@@ -41,7 +42,7 @@ export const handleHTTPError = (e, http, trackerId, logger, logWarningsForStatus
|
|
|
41
42
|
if (trackerId) {
|
|
42
43
|
logger.warn(`Error id: ${trackerId}`);
|
|
43
44
|
const errorBody = { errorId: trackerId };
|
|
44
|
-
if (exposeErrors && e instanceof Error) {
|
|
45
|
+
if (exposeErrors && !isProduction() && e instanceof Error) {
|
|
45
46
|
errorBody.message = e.message;
|
|
46
47
|
errorBody.stack = e.stack;
|
|
47
48
|
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { InvalidSessionError } from '../errors/errors.js';
|
|
2
2
|
import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js';
|
|
3
|
+
const constantTimeEqual = (a, b) => {
|
|
4
|
+
if (a.length !== b.length)
|
|
5
|
+
return false;
|
|
6
|
+
let result = 0;
|
|
7
|
+
for (let i = 0; i < a.length; i++) {
|
|
8
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
9
|
+
}
|
|
10
|
+
return result === 0;
|
|
11
|
+
};
|
|
3
12
|
/**
|
|
4
13
|
* Bearer token middleware that extracts and validates tokens from the Authorization header.
|
|
5
14
|
*
|
|
@@ -41,7 +50,7 @@ export const authBearer = pikkuMiddlewareFactory(({ token } = {}) => pikkuMiddle
|
|
|
41
50
|
throw new InvalidSessionError();
|
|
42
51
|
}
|
|
43
52
|
let userSession = null;
|
|
44
|
-
if (token && bearerToken
|
|
53
|
+
if (token && constantTimeEqual(bearerToken, token.value)) {
|
|
45
54
|
userSession = token.userSession;
|
|
46
55
|
}
|
|
47
56
|
else if (jwtService && !token) {
|
|
@@ -28,31 +28,40 @@ import { getRelativeTimeOffsetFromNow } from '../time-utils.js';
|
|
|
28
28
|
* ])
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
export const authCookie = pikkuMiddlewareFactory(({ name, options, expiresIn }) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
export const authCookie = pikkuMiddlewareFactory(({ name, options, expiresIn }) => {
|
|
32
|
+
const mergedOptions = {
|
|
33
|
+
httpOnly: true,
|
|
34
|
+
secure: true,
|
|
35
|
+
sameSite: 'lax',
|
|
36
|
+
path: '/',
|
|
37
|
+
...options,
|
|
38
|
+
};
|
|
39
|
+
return pikkuMiddleware(async ({ jwt: jwtService, logger }, { http, setSession, getSession, session, hasSessionChanged }, next) => {
|
|
40
|
+
if (!http?.request || !setSession || session) {
|
|
41
|
+
return next();
|
|
40
42
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const currentSession = await getSession?.();
|
|
48
|
-
if (jwtService) {
|
|
49
|
-
http.response.cookie(name, await jwtService.encode(expiresIn, currentSession), {
|
|
50
|
-
...options,
|
|
51
|
-
expires: getRelativeTimeOffsetFromNow(expiresIn),
|
|
52
|
-
});
|
|
43
|
+
const cookieValue = http.request.cookie(name);
|
|
44
|
+
if (cookieValue && jwtService) {
|
|
45
|
+
const userSession = await jwtService.decode(cookieValue);
|
|
46
|
+
if (userSession) {
|
|
47
|
+
setSession?.(userSession);
|
|
48
|
+
}
|
|
53
49
|
}
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
await next();
|
|
51
|
+
if (!http?.response) {
|
|
52
|
+
return;
|
|
56
53
|
}
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (hasSessionChanged?.()) {
|
|
55
|
+
const currentSession = await getSession?.();
|
|
56
|
+
if (jwtService) {
|
|
57
|
+
http.response.cookie(name, await jwtService.encode(expiresIn, currentSession), {
|
|
58
|
+
...mergedOptions,
|
|
59
|
+
expires: getRelativeTimeOffsetFromNow(expiresIn),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
logger.warn('No JWT service available, unable to set cookie');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { RequestTimeoutError } from '../errors/errors.js';
|
|
2
2
|
import { pikkuMiddleware, pikkuMiddlewareFactory } from '../types/core.types.js';
|
|
3
3
|
export const timeout = () => pikkuMiddlewareFactory(({ timeout }) => pikkuMiddleware(async (_services, _wire, next) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
let timeoutId;
|
|
5
|
+
await Promise.race([
|
|
6
|
+
next(),
|
|
7
|
+
new Promise((_resolve, reject) => {
|
|
8
|
+
timeoutId = setTimeout(() => reject(new RequestTimeoutError()), timeout);
|
|
9
|
+
}),
|
|
10
|
+
]).finally(() => {
|
|
11
|
+
if (timeoutId !== undefined) {
|
|
12
|
+
clearTimeout(timeoutId);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
9
15
|
}));
|
package/dist/pikku-state.js
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for managing dynamic/managed credentials.
|
|
3
|
+
* Used for OAuth tokens, per-user API keys, and other credentials
|
|
4
|
+
* that change at runtime (as opposed to SecretService which is for
|
|
5
|
+
* static, developer-configured values).
|
|
6
|
+
*/
|
|
7
|
+
export interface CredentialService {
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves a credential by name and optional user ID.
|
|
10
|
+
* @param name - The credential name (e.g. 'stripe', 'google-sheets').
|
|
11
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
12
|
+
* @returns The credential value, or null if not found.
|
|
13
|
+
*/
|
|
14
|
+
get<T = unknown>(name: string, userId?: string): Promise<T | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Stores a credential.
|
|
17
|
+
* @param name - The credential name.
|
|
18
|
+
* @param value - The credential value to store.
|
|
19
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
20
|
+
*/
|
|
21
|
+
set(name: string, value: unknown, userId?: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Deletes a credential.
|
|
24
|
+
* @param name - The credential name.
|
|
25
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
26
|
+
*/
|
|
27
|
+
delete(name: string, userId?: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Checks if a credential exists.
|
|
30
|
+
* @param name - The credential name.
|
|
31
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
32
|
+
*/
|
|
33
|
+
has(name: string, userId?: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves all credentials for a user.
|
|
36
|
+
* @param userId - The user ID.
|
|
37
|
+
* @returns A record of credential name to value.
|
|
38
|
+
*/
|
|
39
|
+
getAll(userId: string): Promise<Record<string, unknown>>;
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class PikkuCredentialWireService {
|
|
2
|
+
private credentials;
|
|
3
|
+
set(name: string, value: unknown): void;
|
|
4
|
+
getAll(): Record<string, unknown>;
|
|
5
|
+
getScoped(allowedNames: string[]): Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createMiddlewareCredentialWireProps(credentialWire: PikkuCredentialWireService): {
|
|
8
|
+
setCredential: (name: string, value: unknown) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function createWireServicesCredentialWireProps(credentialWire: PikkuCredentialWireService, allowedNames?: string[]): {
|
|
11
|
+
setCredential: (name: string, value: unknown) => void;
|
|
12
|
+
getCredentials: () => Record<string, unknown>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class PikkuCredentialWireService {
|
|
2
|
+
credentials = {};
|
|
3
|
+
set(name, value) {
|
|
4
|
+
this.credentials[name] = value;
|
|
5
|
+
}
|
|
6
|
+
getAll() {
|
|
7
|
+
return this.credentials;
|
|
8
|
+
}
|
|
9
|
+
getScoped(allowedNames) {
|
|
10
|
+
const scoped = {};
|
|
11
|
+
for (const name of allowedNames) {
|
|
12
|
+
if (name in this.credentials) {
|
|
13
|
+
scoped[name] = this.credentials[name];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return scoped;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function createMiddlewareCredentialWireProps(credentialWire) {
|
|
20
|
+
return {
|
|
21
|
+
setCredential: (name, value) => credentialWire.set(name, value),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function createWireServicesCredentialWireProps(credentialWire, allowedNames) {
|
|
25
|
+
return {
|
|
26
|
+
setCredential: (name, value) => credentialWire.set(name, value),
|
|
27
|
+
getCredentials: () => allowedNames
|
|
28
|
+
? credentialWire.getScoped(allowedNames)
|
|
29
|
+
: credentialWire.getAll(),
|
|
30
|
+
};
|
|
31
|
+
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ export { LogLevel } from './logger.js';
|
|
|
5
5
|
export { ScopedSecretService } from './scoped-secret-service.js';
|
|
6
6
|
export { PikkuSessionService, createMiddlewareSessionWireProps, createFunctionSessionWireProps, } from './user-session-service.js';
|
|
7
7
|
export { TypedSecretService } from './typed-secret-service.js';
|
|
8
|
+
export { PikkuCredentialWireService, createMiddlewareCredentialWireProps, createWireServicesCredentialWireProps, } from './credential-wire-service.js';
|
|
8
9
|
export { TypedVariablesService } from './typed-variables-service.js';
|
|
9
10
|
export { LocalSecretService } from './local-secrets.js';
|
|
11
|
+
export { LocalCredentialService } from './local-credential-service.js';
|
|
10
12
|
export { LocalVariablesService } from './local-variables.js';
|
|
11
13
|
export { ConsoleLogger } from './logger-console.js';
|
|
12
14
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
|
|
@@ -28,4 +30,7 @@ export type { AIStorageService } from './ai-storage-service.js';
|
|
|
28
30
|
export type { AIAgentRunnerParams, AIAgentRunnerResult, AIAgentStepResult, AIAgentRunnerService, } from './ai-agent-runner-service.js';
|
|
29
31
|
export type { CreateRunInput, AIRunStateService, } from './ai-run-state-service.js';
|
|
30
32
|
export type { CredentialStatus, CredentialMeta, } from './typed-secret-service.js';
|
|
33
|
+
export type { CredentialService } from './credential-service.js';
|
|
34
|
+
export { TypedCredentialService } from './typed-credential-service.js';
|
|
35
|
+
export type { CredentialStatusInfo, CredentialMetaInfo, } from './typed-credential-service.js';
|
|
31
36
|
export type { VariableStatus, VariableMeta } from './typed-variables-service.js';
|
package/dist/services/index.js
CHANGED
|
@@ -5,11 +5,14 @@ export { LogLevel } from './logger.js';
|
|
|
5
5
|
export { ScopedSecretService } from './scoped-secret-service.js';
|
|
6
6
|
export { PikkuSessionService, createMiddlewareSessionWireProps, createFunctionSessionWireProps, } from './user-session-service.js';
|
|
7
7
|
export { TypedSecretService } from './typed-secret-service.js';
|
|
8
|
+
export { PikkuCredentialWireService, createMiddlewareCredentialWireProps, createWireServicesCredentialWireProps, } from './credential-wire-service.js';
|
|
8
9
|
export { TypedVariablesService } from './typed-variables-service.js';
|
|
9
10
|
export { LocalSecretService } from './local-secrets.js';
|
|
11
|
+
export { LocalCredentialService } from './local-credential-service.js';
|
|
10
12
|
export { LocalVariablesService } from './local-variables.js';
|
|
11
13
|
export { ConsoleLogger } from './logger-console.js';
|
|
12
14
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js';
|
|
13
15
|
export { InMemoryTriggerService } from './in-memory-trigger-service.js';
|
|
14
16
|
export { InMemoryAIRunStateService } from './in-memory-ai-run-state-service.js';
|
|
15
17
|
export { LocalGatewayService } from './local-gateway-service.js';
|
|
18
|
+
export { TypedCredentialService } from './typed-credential-service.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContentService, Logger } from '@pikku/core/services';
|
|
1
|
+
import type { ContentService, JWTService, Logger } from '@pikku/core/services';
|
|
2
2
|
export interface LocalContentConfig {
|
|
3
3
|
localFileUploadPath: string;
|
|
4
4
|
uploadUrlPrefix: string;
|
|
@@ -9,10 +9,13 @@ export interface LocalContentConfig {
|
|
|
9
9
|
export declare class LocalContent implements ContentService {
|
|
10
10
|
private config;
|
|
11
11
|
private logger;
|
|
12
|
-
|
|
12
|
+
private jwt?;
|
|
13
|
+
constructor(config: LocalContentConfig, logger: Logger, jwt?: JWTService | undefined);
|
|
14
|
+
private safePath;
|
|
13
15
|
init(): Promise<void>;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
private signParams;
|
|
17
|
+
signURL(url: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>;
|
|
18
|
+
signContentKey(assetKey: string, dateLessThan: Date, dateGreaterThan?: Date): Promise<string>;
|
|
16
19
|
getUploadURL(assetKey: string): Promise<{
|
|
17
20
|
uploadUrl: string;
|
|
18
21
|
assetKey: string;
|
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
import { createReadStream, createWriteStream, promises } from 'fs';
|
|
2
2
|
import { mkdir, readFile } from 'fs/promises';
|
|
3
|
+
import { resolve, normalize } from 'path';
|
|
3
4
|
import { pipeline } from 'stream/promises';
|
|
4
5
|
export class LocalContent {
|
|
5
6
|
config;
|
|
6
7
|
logger;
|
|
7
|
-
|
|
8
|
+
jwt;
|
|
9
|
+
constructor(config, logger, jwt) {
|
|
8
10
|
this.config = config;
|
|
9
11
|
this.logger = logger;
|
|
12
|
+
this.jwt = jwt;
|
|
13
|
+
}
|
|
14
|
+
safePath(assetKey) {
|
|
15
|
+
const base = resolve(this.config.localFileUploadPath);
|
|
16
|
+
const target = resolve(base, normalize(assetKey));
|
|
17
|
+
if (!target.startsWith(base + '/') && target !== base) {
|
|
18
|
+
throw new Error('Invalid asset key');
|
|
19
|
+
}
|
|
20
|
+
return target;
|
|
10
21
|
}
|
|
11
22
|
async init() { }
|
|
12
|
-
async
|
|
13
|
-
|
|
23
|
+
async signParams(dateLessThan, dateGreaterThan) {
|
|
24
|
+
const signedAt = Date.now();
|
|
25
|
+
const expiresAt = dateLessThan.getTime();
|
|
26
|
+
const params = new URLSearchParams({
|
|
27
|
+
signedAt: String(signedAt),
|
|
28
|
+
expiresAt: String(expiresAt),
|
|
29
|
+
});
|
|
30
|
+
if (dateGreaterThan) {
|
|
31
|
+
params.set('notBefore', String(dateGreaterThan.getTime()));
|
|
32
|
+
}
|
|
33
|
+
if (this.jwt) {
|
|
34
|
+
const expiresInSeconds = Math.max(1, Math.floor((expiresAt - signedAt) / 1000));
|
|
35
|
+
const signature = await this.jwt.encode({ value: expiresInSeconds, unit: 'second' }, { signedAt, expiresAt });
|
|
36
|
+
params.set('signature', signature);
|
|
37
|
+
}
|
|
38
|
+
return params.toString();
|
|
39
|
+
}
|
|
40
|
+
async signURL(url, dateLessThan, dateGreaterThan) {
|
|
41
|
+
const params = await this.signParams(dateLessThan, dateGreaterThan);
|
|
42
|
+
return `${url}?${params}`;
|
|
14
43
|
}
|
|
15
|
-
async signContentKey(assetKey) {
|
|
16
|
-
|
|
17
|
-
? `${this.config.server}${this.config.assetUrlPrefix}/${assetKey}
|
|
18
|
-
: `${this.config.assetUrlPrefix}/${assetKey}
|
|
44
|
+
async signContentKey(assetKey, dateLessThan, dateGreaterThan) {
|
|
45
|
+
const base = this.config.server
|
|
46
|
+
? `${this.config.server}${this.config.assetUrlPrefix}/${assetKey}`
|
|
47
|
+
: `${this.config.assetUrlPrefix}/${assetKey}`;
|
|
48
|
+
return this.signURL(base, dateLessThan, dateGreaterThan);
|
|
19
49
|
}
|
|
20
50
|
async getUploadURL(assetKey) {
|
|
21
51
|
this.logger.debug(`Going to upload with key: ${assetKey}`);
|
|
@@ -26,7 +56,7 @@ export class LocalContent {
|
|
|
26
56
|
}
|
|
27
57
|
async writeFile(assetKey, stream) {
|
|
28
58
|
this.logger.debug(`Writing file: ${assetKey}`);
|
|
29
|
-
const path =
|
|
59
|
+
const path = this.safePath(assetKey);
|
|
30
60
|
try {
|
|
31
61
|
await this.createDirectoryForFile(path);
|
|
32
62
|
const fileStream = createWriteStream(path);
|
|
@@ -43,7 +73,7 @@ export class LocalContent {
|
|
|
43
73
|
async copyFile(assetKey, fromAbsolutePath) {
|
|
44
74
|
this.logger.debug(`Writing file: ${assetKey}`);
|
|
45
75
|
try {
|
|
46
|
-
const path =
|
|
76
|
+
const path = this.safePath(assetKey);
|
|
47
77
|
await this.createDirectoryForFile(path);
|
|
48
78
|
await promises.copyFile(fromAbsolutePath, path);
|
|
49
79
|
}
|
|
@@ -55,7 +85,7 @@ export class LocalContent {
|
|
|
55
85
|
}
|
|
56
86
|
async readFile(assetKey) {
|
|
57
87
|
this.logger.debug(`Getting key: ${assetKey}`);
|
|
58
|
-
const filePath =
|
|
88
|
+
const filePath = this.safePath(assetKey);
|
|
59
89
|
try {
|
|
60
90
|
const stream = createReadStream(filePath);
|
|
61
91
|
// Handle early stream errors (like file not found, permission denied, etc.)
|
|
@@ -70,14 +100,14 @@ export class LocalContent {
|
|
|
70
100
|
}
|
|
71
101
|
}
|
|
72
102
|
async readFileAsBuffer(assetKey) {
|
|
73
|
-
const filePath =
|
|
103
|
+
const filePath = this.safePath(assetKey);
|
|
74
104
|
this.logger.debug(`Reading file as buffer: ${assetKey}`);
|
|
75
105
|
return readFile(filePath);
|
|
76
106
|
}
|
|
77
107
|
async deleteFile(assetKey) {
|
|
78
108
|
this.logger.debug(`deleting key: ${assetKey}`);
|
|
79
109
|
try {
|
|
80
|
-
await promises.unlink(
|
|
110
|
+
await promises.unlink(this.safePath(assetKey));
|
|
81
111
|
return true;
|
|
82
112
|
}
|
|
83
113
|
catch (e) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CredentialService } from './credential-service.js';
|
|
2
|
+
export declare class LocalCredentialService implements CredentialService {
|
|
3
|
+
private store;
|
|
4
|
+
private makeKey;
|
|
5
|
+
get<T = unknown>(name: string, userId?: string): Promise<T | null>;
|
|
6
|
+
set(name: string, value: unknown, userId?: string): Promise<void>;
|
|
7
|
+
delete(name: string, userId?: string): Promise<void>;
|
|
8
|
+
has(name: string, userId?: string): Promise<boolean>;
|
|
9
|
+
getAll(userId: string): Promise<Record<string, unknown>>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class LocalCredentialService {
|
|
2
|
+
store = new Map();
|
|
3
|
+
makeKey(name, userId) {
|
|
4
|
+
return userId ? `${userId}:${name}` : name;
|
|
5
|
+
}
|
|
6
|
+
async get(name, userId) {
|
|
7
|
+
const key = this.makeKey(name, userId);
|
|
8
|
+
const value = this.store.get(key);
|
|
9
|
+
return value ?? null;
|
|
10
|
+
}
|
|
11
|
+
async set(name, value, userId) {
|
|
12
|
+
const key = this.makeKey(name, userId);
|
|
13
|
+
this.store.set(key, value);
|
|
14
|
+
}
|
|
15
|
+
async delete(name, userId) {
|
|
16
|
+
const key = this.makeKey(name, userId);
|
|
17
|
+
this.store.delete(key);
|
|
18
|
+
}
|
|
19
|
+
async has(name, userId) {
|
|
20
|
+
const key = this.makeKey(name, userId);
|
|
21
|
+
return this.store.has(key);
|
|
22
|
+
}
|
|
23
|
+
async getAll(userId) {
|
|
24
|
+
const prefix = `${userId}:`;
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [key, value] of this.store) {
|
|
27
|
+
if (key.startsWith(prefix)) {
|
|
28
|
+
result[key.slice(prefix.length)] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -30,7 +30,7 @@ export class LocalSecretService {
|
|
|
30
30
|
if (value) {
|
|
31
31
|
return JSON.parse(value);
|
|
32
32
|
}
|
|
33
|
-
throw new Error(
|
|
33
|
+
throw new Error('Requested secret not found');
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Retrieves a secret by key.
|
|
@@ -50,7 +50,7 @@ export class LocalSecretService {
|
|
|
50
50
|
if (value) {
|
|
51
51
|
return value;
|
|
52
52
|
}
|
|
53
|
-
throw new Error(
|
|
53
|
+
throw new Error('Requested secret not found');
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Stores a JSON value as a secret in local storage.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CredentialService } from './credential-service.js';
|
|
2
|
+
export interface CredentialStatusInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
isConfigured: boolean;
|
|
6
|
+
type: 'singleton' | 'wire';
|
|
7
|
+
oauth2?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export type CredentialMetaInfo = {
|
|
10
|
+
name: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
type: 'singleton' | 'wire';
|
|
13
|
+
oauth2?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare class TypedCredentialService<TMap = Record<string, unknown>> implements CredentialService {
|
|
16
|
+
private credentials;
|
|
17
|
+
private credentialsMeta;
|
|
18
|
+
constructor(credentials: CredentialService, credentialsMeta: Record<string, CredentialMetaInfo>);
|
|
19
|
+
get<K extends keyof TMap & string>(name: K, userId?: string): Promise<TMap[K] | null>;
|
|
20
|
+
get<T = unknown>(name: string, userId?: string): Promise<T | null>;
|
|
21
|
+
set<K extends string>(name: K, value: K extends keyof TMap ? TMap[K] : unknown, userId?: string): Promise<void>;
|
|
22
|
+
delete(name: string, userId?: string): Promise<void>;
|
|
23
|
+
has(name: string, userId?: string): Promise<boolean>;
|
|
24
|
+
getAll(userId: string): Promise<Record<string, unknown>>;
|
|
25
|
+
getAllStatus(userId?: string): Promise<CredentialStatusInfo[]>;
|
|
26
|
+
getMissing(userId?: string): Promise<CredentialStatusInfo[]>;
|
|
27
|
+
}
|