@pikku/core 0.12.9 → 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 +7 -0
- package/dist/function/function-runner.d.ts +3 -1
- package/dist/function/function-runner.js +5 -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-credential-service.d.ts +10 -0
- package/dist/services/local-credential-service.js +33 -0
- 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 +106 -0
- package/dist/types/core.types.d.ts +7 -0
- package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
- 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 +4 -0
- 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 +1 -1
- package/dist/wirings/workflow/graph/graph-runner.js +15 -0
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.js +40 -8
- package/package.json +2 -1
- package/src/function/function-runner.ts +11 -0
- 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-credential-service.ts +41 -0
- package/src/services/typed-credential-service.ts +75 -0
- package/src/testing/service-tests.ts +139 -0
- package/src/types/core.types.ts +7 -0
- package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
- 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 +7 -0
- 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.ts +33 -1
- package/src/wirings/workflow/pikku-workflow-service.ts +55 -9
- 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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { OAuth2Client } from './oauth2-client.js';
|
|
2
|
+
const TOKEN_EXPIRY_BUFFER_MS = 60_000;
|
|
3
|
+
function getCredentialMeta(ctx, name) {
|
|
4
|
+
const meta = ctx.credentialsMeta[name];
|
|
5
|
+
if (!meta) {
|
|
6
|
+
throw new Error(`Credential '${name}' not found`);
|
|
7
|
+
}
|
|
8
|
+
if (!meta.oauth2) {
|
|
9
|
+
throw new Error(`Credential '${name}' is not an OAuth2 credential`);
|
|
10
|
+
}
|
|
11
|
+
return meta;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates OAuth2 route handlers for user credential management.
|
|
15
|
+
*
|
|
16
|
+
* Returns individual handler functions for connect/callback/disconnect/status
|
|
17
|
+
* that handle the OAuth2 authorization code flow and store tokens in CredentialService.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const oauth2 = createOAuth2Handler({ credentialsMeta })
|
|
22
|
+
*
|
|
23
|
+
* const oauth2Routes = defineHTTPRoutes({
|
|
24
|
+
* auth: true,
|
|
25
|
+
* basePath: '/credentials',
|
|
26
|
+
* routes: {
|
|
27
|
+
* connect: { method: 'get', route: '/:name/connect', func: oauth2.connect },
|
|
28
|
+
* callback: { method: 'get', route: '/:name/callback', func: oauth2.callback, auth: false },
|
|
29
|
+
* disconnect: { method: 'delete', route: '/:name', func: oauth2.disconnect },
|
|
30
|
+
* status: { method: 'get', route: '/:name/status', func: oauth2.status },
|
|
31
|
+
* },
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* wireHTTPRoutes({ routes: { credentials: oauth2Routes } })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const createOAuth2Handler = (options) => {
|
|
38
|
+
const basePath = options.basePath ?? '/credentials';
|
|
39
|
+
const ctx = {
|
|
40
|
+
credentialsMeta: options.credentialsMeta,
|
|
41
|
+
basePath,
|
|
42
|
+
};
|
|
43
|
+
const connectHandler = async (services, _data, wire) => {
|
|
44
|
+
const { name } = wire.http.request.params();
|
|
45
|
+
const meta = getCredentialMeta(ctx, name);
|
|
46
|
+
const queryParams = wire.http.request.query();
|
|
47
|
+
const redirectUrl = queryParams.redirect_url || queryParams.redirect;
|
|
48
|
+
const oauth2Client = new OAuth2Client(meta.oauth2, meta.oauth2.appCredentialSecretId, services.secrets);
|
|
49
|
+
const userId = wire.session?.userId;
|
|
50
|
+
if (!userId) {
|
|
51
|
+
throw new Error('Authentication required for OAuth2 connect');
|
|
52
|
+
}
|
|
53
|
+
const state = await services.jwt.encode({ value: 10, unit: 'minute' }, {
|
|
54
|
+
userId,
|
|
55
|
+
credentialName: name,
|
|
56
|
+
redirectUrl,
|
|
57
|
+
});
|
|
58
|
+
let origin = wire.http.request.header('origin');
|
|
59
|
+
if (!origin) {
|
|
60
|
+
const host = wire.http.request.header('host');
|
|
61
|
+
if (!host) {
|
|
62
|
+
throw new Error('Unable to determine request origin for OAuth2 callback');
|
|
63
|
+
}
|
|
64
|
+
const protocol = wire.http.request.header('x-forwarded-proto') || 'http';
|
|
65
|
+
origin = `${protocol}://${host}`;
|
|
66
|
+
}
|
|
67
|
+
const callbackUrl = `${origin}${basePath}/${name}/callback`;
|
|
68
|
+
const authUrl = await oauth2Client.getAuthorizationUrl(state, callbackUrl);
|
|
69
|
+
return Response.redirect(authUrl, 302);
|
|
70
|
+
};
|
|
71
|
+
const callbackHandler = async (services, _data, wire) => {
|
|
72
|
+
const { name } = wire.http.request.params();
|
|
73
|
+
const meta = getCredentialMeta(ctx, name);
|
|
74
|
+
const queryParams = wire.http.request.query();
|
|
75
|
+
const code = queryParams.code;
|
|
76
|
+
const state = queryParams.state;
|
|
77
|
+
if (!code || !state) {
|
|
78
|
+
throw new Error('Missing code or state parameter');
|
|
79
|
+
}
|
|
80
|
+
let statePayload;
|
|
81
|
+
try {
|
|
82
|
+
statePayload = await services.jwt.decode(state);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error('Invalid or expired OAuth2 state');
|
|
86
|
+
}
|
|
87
|
+
if (statePayload.credentialName !== name) {
|
|
88
|
+
throw new Error('Credential name mismatch in state');
|
|
89
|
+
}
|
|
90
|
+
const oauth2Client = new OAuth2Client(meta.oauth2, meta.oauth2.appCredentialSecretId, services.secrets);
|
|
91
|
+
let origin = wire.http.request.header('origin');
|
|
92
|
+
if (!origin) {
|
|
93
|
+
const host = wire.http.request.header('host');
|
|
94
|
+
if (!host) {
|
|
95
|
+
throw new Error('Unable to determine request origin for OAuth2 callback');
|
|
96
|
+
}
|
|
97
|
+
const protocol = wire.http.request.header('x-forwarded-proto') || 'http';
|
|
98
|
+
origin = `${protocol}://${host}`;
|
|
99
|
+
}
|
|
100
|
+
const callbackUrl = `${origin}${basePath}/${name}/callback`;
|
|
101
|
+
const tokens = await oauth2Client.exchangeCode(code, callbackUrl);
|
|
102
|
+
await services.credentialService.set(name, tokens, statePayload.userId);
|
|
103
|
+
services.logger.info(`OAuth2 tokens stored for user '${statePayload.userId}', credential '${name}'`);
|
|
104
|
+
if (statePayload.redirectUrl) {
|
|
105
|
+
return Response.redirect(statePayload.redirectUrl, 302);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
return { success: true, credentialName: name };
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const disconnectHandler = async (services, _data, wire) => {
|
|
112
|
+
const { name } = wire.http.request.params();
|
|
113
|
+
const userId = wire.session?.userId;
|
|
114
|
+
if (!userId) {
|
|
115
|
+
throw new Error('Authentication required');
|
|
116
|
+
}
|
|
117
|
+
await services.credentialService.delete(name, userId);
|
|
118
|
+
services.logger.info(`OAuth2 credential '${name}' disconnected for user '${userId}'`);
|
|
119
|
+
return { success: true };
|
|
120
|
+
};
|
|
121
|
+
const statusHandler = async (services, _data, wire) => {
|
|
122
|
+
const { name } = wire.http.request.params();
|
|
123
|
+
const userId = wire.session?.userId;
|
|
124
|
+
if (!userId) {
|
|
125
|
+
throw new Error('Authentication required');
|
|
126
|
+
}
|
|
127
|
+
const credential = await services.credentialService.get(name, userId);
|
|
128
|
+
if (!credential) {
|
|
129
|
+
return { connected: false };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
connected: true,
|
|
133
|
+
hasRefreshToken: !!credential.refreshToken,
|
|
134
|
+
expiresAt: credential.expiresAt,
|
|
135
|
+
isExpired: credential.expiresAt
|
|
136
|
+
? credential.expiresAt < Date.now() + TOKEN_EXPIRY_BUFFER_MS
|
|
137
|
+
: false,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
connect: { func: connectHandler },
|
|
142
|
+
callback: { func: callbackHandler },
|
|
143
|
+
disconnect: { func: disconnectHandler },
|
|
144
|
+
status: { func: statusHandler },
|
|
145
|
+
};
|
|
146
|
+
};
|
|
@@ -37,29 +37,3 @@ export type OAuth2Config = {
|
|
|
37
37
|
/** Additional query parameters for authorization URL */
|
|
38
38
|
additionalParams?: Record<string, string>;
|
|
39
39
|
};
|
|
40
|
-
/**
|
|
41
|
-
* Configuration for wireOAuth2Credential.
|
|
42
|
-
* Combines standard credential fields with OAuth2-specific config.
|
|
43
|
-
*/
|
|
44
|
-
export type CoreOAuth2Credential = {
|
|
45
|
-
/** Unique identifier for this credential */
|
|
46
|
-
name: string;
|
|
47
|
-
/** Human-readable name for UI display */
|
|
48
|
-
displayName: string;
|
|
49
|
-
/** Optional description for UI */
|
|
50
|
-
description?: string;
|
|
51
|
-
/** Key used with SecretService to retrieve app credentials (clientId/clientSecret) */
|
|
52
|
-
secretId: string;
|
|
53
|
-
/** Where access/refresh tokens are stored */
|
|
54
|
-
tokenSecretId: string;
|
|
55
|
-
/** OAuth2 authorization URL */
|
|
56
|
-
authorizationUrl: string;
|
|
57
|
-
/** OAuth2 token exchange URL */
|
|
58
|
-
tokenUrl: string;
|
|
59
|
-
/** Required scopes */
|
|
60
|
-
scopes: string[];
|
|
61
|
-
/** Use PKCE flow */
|
|
62
|
-
pkce?: boolean;
|
|
63
|
-
/** Additional query parameters for authorization URL */
|
|
64
|
-
additionalParams?: Record<string, string>;
|
|
65
|
-
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PikkuWorkflowService } from '../pikku-workflow-service.js';
|
|
2
2
|
import type { WorkflowRuntimeMeta, WorkflowRunWire } from '../workflow.types.js';
|
|
3
3
|
export declare class ChildWorkflowStartedException extends Error {
|
|
4
4
|
parentRunId: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WorkflowAsyncException, WorkflowSuspendedException, } from '../pikku-workflow-service.js';
|
|
1
2
|
import { pikkuState, getSingletonServices } from '../../../pikku-state.js';
|
|
2
3
|
import { RPCNotFoundError } from '../../rpc/rpc-runner.js';
|
|
3
4
|
export class ChildWorkflowStartedException extends Error {
|
|
@@ -362,6 +363,7 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
|
|
|
362
363
|
else {
|
|
363
364
|
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
364
365
|
graph: graphWire,
|
|
366
|
+
workflow: workflowService.createWorkflowWire(graphName, runId, rpcService),
|
|
365
367
|
});
|
|
366
368
|
}
|
|
367
369
|
if (wireState.branchKey) {
|
|
@@ -370,6 +372,10 @@ export async function executeGraphStep(workflowService, rpcService, runId, stepI
|
|
|
370
372
|
return result;
|
|
371
373
|
}
|
|
372
374
|
catch (error) {
|
|
375
|
+
if (error instanceof WorkflowAsyncException ||
|
|
376
|
+
error instanceof WorkflowSuspendedException) {
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
373
379
|
if (error instanceof ChildWorkflowStartedException) {
|
|
374
380
|
throw error;
|
|
375
381
|
}
|
|
@@ -447,6 +453,7 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
|
|
|
447
453
|
else {
|
|
448
454
|
result = await rpcService.rpcWithWire(rpcName, input, {
|
|
449
455
|
graph: graphWire,
|
|
456
|
+
workflow: workflowService.createWorkflowWire(graphName, runId, rpcService),
|
|
450
457
|
});
|
|
451
458
|
}
|
|
452
459
|
if (wireState.branchKey) {
|
|
@@ -455,6 +462,10 @@ async function executeGraphNodeInline(workflowService, rpcService, runId, graphN
|
|
|
455
462
|
await workflowService.setStepResult(stepState.stepId, result);
|
|
456
463
|
}
|
|
457
464
|
catch (error) {
|
|
465
|
+
if (error instanceof WorkflowAsyncException ||
|
|
466
|
+
error instanceof WorkflowSuspendedException) {
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
458
469
|
if (error instanceof RPCNotFoundError) {
|
|
459
470
|
await workflowService.setStepError(stepState.stepId, error);
|
|
460
471
|
await workflowService.updateRunStatus(runId, 'suspended', undefined, {
|
|
@@ -575,6 +586,10 @@ export async function runWorkflowGraph(workflowService, graphName, triggerInput,
|
|
|
575
586
|
await continueGraphInline(workflowService, rpcService, runId, graphName, nodes, triggerInput, entryNodes);
|
|
576
587
|
}
|
|
577
588
|
catch (error) {
|
|
589
|
+
if (error instanceof WorkflowAsyncException ||
|
|
590
|
+
error instanceof WorkflowSuspendedException) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
578
593
|
await workflowService.updateRunStatus(runId, 'failed', undefined, {
|
|
579
594
|
message: error.message,
|
|
580
595
|
stack: error.stack || '',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SerializedError } from '../../types/core.types.js';
|
|
2
|
-
import type { StepState, WorkflowRun, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
|
|
2
|
+
import type { PikkuWorkflowWire, StepState, WorkflowRun, WorkflowRunWire, WorkflowStatus, WorkflowVersionStatus, WorkflowStepOptions } from './workflow.types.js';
|
|
3
3
|
import type { WorkflowService } from '../../services/workflow-service.js';
|
|
4
4
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
5
5
|
/**
|
|
@@ -269,7 +269,7 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
269
269
|
private sleepStep;
|
|
270
270
|
private getSuspendStepName;
|
|
271
271
|
private suspendStep;
|
|
272
|
-
|
|
272
|
+
createWorkflowWire(name: string, runId: string, rpcService: any): PikkuWorkflowWire;
|
|
273
273
|
private verifyStepName;
|
|
274
274
|
private getConfig;
|
|
275
275
|
}
|
|
@@ -373,17 +373,49 @@ export class PikkuWorkflowService {
|
|
|
373
373
|
}
|
|
374
374
|
const meta = pikkuState(null, 'workflows', 'meta');
|
|
375
375
|
const workflowMeta = meta[run.workflow];
|
|
376
|
-
|
|
376
|
+
const isGraphWorkflow = workflowMeta?.source === 'graph' ||
|
|
377
|
+
workflowMeta?.source === 'ai-agent';
|
|
378
|
+
if (isGraphWorkflow &&
|
|
379
|
+
workflowMeta?.nodes &&
|
|
380
|
+
stepName in workflowMeta.nodes) {
|
|
377
381
|
result = await executeGraphStep(this, rpcService, runId, stepState.stepId, stepName, rpcName, data, run.workflow);
|
|
378
382
|
}
|
|
379
383
|
else {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
384
|
+
// Check if rpcName refers to a sub-workflow
|
|
385
|
+
const subWorkflowMeta = meta[rpcName];
|
|
386
|
+
if (subWorkflowMeta) {
|
|
387
|
+
const childWire = {
|
|
388
|
+
type: 'workflow',
|
|
389
|
+
id: rpcName,
|
|
390
|
+
parentRunId: runId,
|
|
391
|
+
parentStepId: stepState.stepId,
|
|
392
|
+
};
|
|
393
|
+
const shouldInline = subWorkflowMeta.inline || !getSingletonServices()?.queueService;
|
|
394
|
+
const { runId: childRunId } = await this.startWorkflow(rpcName, data, childWire, rpcService, { inline: shouldInline });
|
|
395
|
+
await this.setStepChildRunId(stepState.stepId, childRunId);
|
|
396
|
+
if (shouldInline) {
|
|
397
|
+
const childRun = await this.getRun(childRunId);
|
|
398
|
+
if (childRun?.status === 'failed') {
|
|
399
|
+
throw new Error(childRun.error?.message || 'Sub-workflow failed');
|
|
400
|
+
}
|
|
401
|
+
if (childRun?.status === 'cancelled') {
|
|
402
|
+
throw new Error('Sub-workflow was cancelled');
|
|
403
|
+
}
|
|
404
|
+
result = childRun?.output;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
throw new ChildWorkflowStartedException(runId, stepState.stepId, childRunId);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
result = await rpcService.rpcWithWire(rpcName, data, {
|
|
412
|
+
workflowStep: {
|
|
413
|
+
runId,
|
|
414
|
+
stepId: stepState.stepId,
|
|
415
|
+
attemptCount: stepState.attemptCount,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
}
|
|
387
419
|
}
|
|
388
420
|
// Store result and mark succeeded
|
|
389
421
|
await this.setStepResult(stepState.stepId, result);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.10",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"./cli": "./dist/wirings/cli/index.js",
|
|
36
36
|
"./cli/channel": "./dist/wirings/cli/channel/index.js",
|
|
37
37
|
"./node": "./dist/wirings/node/index.js",
|
|
38
|
+
"./credential": "./dist/wirings/credential/index.js",
|
|
38
39
|
"./secret": "./dist/wirings/secret/index.js",
|
|
39
40
|
"./variable": "./dist/wirings/variable/index.js",
|
|
40
41
|
"./oauth2": "./dist/wirings/oauth2/index.js",
|
|
@@ -28,6 +28,8 @@ import { parseVersionedId } from '../version.js'
|
|
|
28
28
|
import type { SessionService } from '../services/user-session-service.js'
|
|
29
29
|
import { createFunctionSessionWireProps } from '../services/user-session-service.js'
|
|
30
30
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js'
|
|
31
|
+
import type { PikkuCredentialWireService } from '../services/credential-wire-service.js'
|
|
32
|
+
import { createWireServicesCredentialWireProps } from '../services/credential-wire-service.js'
|
|
31
33
|
import { rpcService } from '../wirings/rpc/rpc-runner.js'
|
|
32
34
|
import { closeWireServices } from '../utils.js'
|
|
33
35
|
|
|
@@ -146,6 +148,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
146
148
|
tags = [],
|
|
147
149
|
wire,
|
|
148
150
|
sessionService,
|
|
151
|
+
credentialWireService,
|
|
149
152
|
packageName = null,
|
|
150
153
|
}: {
|
|
151
154
|
singletonServices: CoreSingletonServices
|
|
@@ -162,6 +165,7 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
162
165
|
tags?: string[]
|
|
163
166
|
wire: PikkuWire
|
|
164
167
|
sessionService?: SessionService<CoreUserSession>
|
|
168
|
+
credentialWireService?: PikkuCredentialWireService
|
|
165
169
|
packageName?: string | null
|
|
166
170
|
}
|
|
167
171
|
): Promise<Out> => {
|
|
@@ -312,6 +316,13 @@ export const runPikkuFunc = async <In = any, Out = any>(
|
|
|
312
316
|
})
|
|
313
317
|
}
|
|
314
318
|
|
|
319
|
+
if (credentialWireService) {
|
|
320
|
+
Object.assign(
|
|
321
|
+
resolvedWire,
|
|
322
|
+
createWireServicesCredentialWireProps(credentialWireService)
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
315
326
|
const wireServices = await resolvedCreateWireServices?.(
|
|
316
327
|
resolvedSingletonServices,
|
|
317
328
|
resolvedWire
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/**
|
|
17
|
+
* Stores a credential.
|
|
18
|
+
* @param name - The credential name.
|
|
19
|
+
* @param value - The credential value to store.
|
|
20
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
21
|
+
*/
|
|
22
|
+
set(name: string, value: unknown, userId?: string): Promise<void>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Deletes a credential.
|
|
26
|
+
* @param name - The credential name.
|
|
27
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
28
|
+
*/
|
|
29
|
+
delete(name: string, userId?: string): Promise<void>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks if a credential exists.
|
|
33
|
+
* @param name - The credential name.
|
|
34
|
+
* @param userId - Optional user ID for per-user credentials. Omit for platform-level.
|
|
35
|
+
*/
|
|
36
|
+
has(name: string, userId?: string): Promise<boolean>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Retrieves all credentials for a user.
|
|
40
|
+
* @param userId - The user ID.
|
|
41
|
+
* @returns A record of credential name to value.
|
|
42
|
+
*/
|
|
43
|
+
getAll(userId: string): Promise<Record<string, unknown>>
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class PikkuCredentialWireService {
|
|
2
|
+
private credentials: Record<string, unknown> = {}
|
|
3
|
+
|
|
4
|
+
set(name: string, value: unknown): void {
|
|
5
|
+
this.credentials[name] = value
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getAll(): Record<string, unknown> {
|
|
9
|
+
return this.credentials
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getScoped(allowedNames: string[]): Record<string, unknown> {
|
|
13
|
+
const scoped: Record<string, unknown> = {}
|
|
14
|
+
for (const name of allowedNames) {
|
|
15
|
+
if (name in this.credentials) {
|
|
16
|
+
scoped[name] = this.credentials[name]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return scoped
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createMiddlewareCredentialWireProps(
|
|
24
|
+
credentialWire: PikkuCredentialWireService
|
|
25
|
+
) {
|
|
26
|
+
return {
|
|
27
|
+
setCredential: (name: string, value: unknown) =>
|
|
28
|
+
credentialWire.set(name, value),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createWireServicesCredentialWireProps(
|
|
33
|
+
credentialWire: PikkuCredentialWireService,
|
|
34
|
+
allowedNames?: string[]
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
setCredential: (name: string, value: unknown) =>
|
|
38
|
+
credentialWire.set(name, value),
|
|
39
|
+
getCredentials: () =>
|
|
40
|
+
allowedNames
|
|
41
|
+
? credentialWire.getScoped(allowedNames)
|
|
42
|
+
: credentialWire.getAll(),
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/services/index.ts
CHANGED
|
@@ -10,8 +10,14 @@ export {
|
|
|
10
10
|
createFunctionSessionWireProps,
|
|
11
11
|
} from './user-session-service.js'
|
|
12
12
|
export { TypedSecretService } from './typed-secret-service.js'
|
|
13
|
+
export {
|
|
14
|
+
PikkuCredentialWireService,
|
|
15
|
+
createMiddlewareCredentialWireProps,
|
|
16
|
+
createWireServicesCredentialWireProps,
|
|
17
|
+
} from './credential-wire-service.js'
|
|
13
18
|
export { TypedVariablesService } from './typed-variables-service.js'
|
|
14
19
|
export { LocalSecretService } from './local-secrets.js'
|
|
20
|
+
export { LocalCredentialService } from './local-credential-service.js'
|
|
15
21
|
export { LocalVariablesService } from './local-variables.js'
|
|
16
22
|
export { ConsoleLogger } from './logger-console.js'
|
|
17
23
|
export { InMemoryWorkflowService } from './in-memory-workflow-service.js'
|
|
@@ -53,4 +59,10 @@ export type {
|
|
|
53
59
|
CredentialStatus,
|
|
54
60
|
CredentialMeta,
|
|
55
61
|
} from './typed-secret-service.js'
|
|
62
|
+
export type { CredentialService } from './credential-service.js'
|
|
63
|
+
export { TypedCredentialService } from './typed-credential-service.js'
|
|
64
|
+
export type {
|
|
65
|
+
CredentialStatusInfo,
|
|
66
|
+
CredentialMetaInfo,
|
|
67
|
+
} from './typed-credential-service.js'
|
|
56
68
|
export type { VariableStatus, VariableMeta } from './typed-variables-service.js'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CredentialService } from './credential-service.js'
|
|
2
|
+
|
|
3
|
+
export class LocalCredentialService implements CredentialService {
|
|
4
|
+
private store: Map<string, unknown> = new Map()
|
|
5
|
+
|
|
6
|
+
private makeKey(name: string, userId?: string): string {
|
|
7
|
+
return userId ? `${userId}:${name}` : name
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async get<T = unknown>(name: string, userId?: string): Promise<T | null> {
|
|
11
|
+
const key = this.makeKey(name, userId)
|
|
12
|
+
const value = this.store.get(key)
|
|
13
|
+
return (value as T) ?? null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async set(name: string, value: unknown, userId?: string): Promise<void> {
|
|
17
|
+
const key = this.makeKey(name, userId)
|
|
18
|
+
this.store.set(key, value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async delete(name: string, userId?: string): Promise<void> {
|
|
22
|
+
const key = this.makeKey(name, userId)
|
|
23
|
+
this.store.delete(key)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async has(name: string, userId?: string): Promise<boolean> {
|
|
27
|
+
const key = this.makeKey(name, userId)
|
|
28
|
+
return this.store.has(key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getAll(userId: string): Promise<Record<string, unknown>> {
|
|
32
|
+
const prefix = `${userId}:`
|
|
33
|
+
const result: Record<string, unknown> = {}
|
|
34
|
+
for (const [key, value] of this.store) {
|
|
35
|
+
if (key.startsWith(prefix)) {
|
|
36
|
+
result[key.slice(prefix.length)] = value
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CredentialService } from './credential-service.js'
|
|
2
|
+
|
|
3
|
+
export interface CredentialStatusInfo {
|
|
4
|
+
name: string
|
|
5
|
+
displayName: string
|
|
6
|
+
isConfigured: boolean
|
|
7
|
+
type: 'singleton' | 'wire'
|
|
8
|
+
oauth2?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type CredentialMetaInfo = {
|
|
12
|
+
name: string
|
|
13
|
+
displayName: string
|
|
14
|
+
type: 'singleton' | 'wire'
|
|
15
|
+
oauth2?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class TypedCredentialService<TMap = Record<string, unknown>>
|
|
19
|
+
implements CredentialService
|
|
20
|
+
{
|
|
21
|
+
constructor(
|
|
22
|
+
private credentials: CredentialService,
|
|
23
|
+
private credentialsMeta: Record<string, CredentialMetaInfo>
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async get<K extends keyof TMap & string>(
|
|
27
|
+
name: K,
|
|
28
|
+
userId?: string
|
|
29
|
+
): Promise<TMap[K] | null>
|
|
30
|
+
async get<T = unknown>(name: string, userId?: string): Promise<T | null>
|
|
31
|
+
async get(name: string, userId?: string): Promise<unknown> {
|
|
32
|
+
return this.credentials.get(name, userId)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async set<K extends string>(
|
|
36
|
+
name: K,
|
|
37
|
+
value: K extends keyof TMap ? TMap[K] : unknown,
|
|
38
|
+
userId?: string
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
return this.credentials.set(name, value, userId)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async delete(name: string, userId?: string): Promise<void> {
|
|
44
|
+
return this.credentials.delete(name, userId)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async has(name: string, userId?: string): Promise<boolean> {
|
|
48
|
+
return this.credentials.has(name, userId)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getAll(userId: string): Promise<Record<string, unknown>> {
|
|
52
|
+
return this.credentials.getAll(userId)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getAllStatus(userId?: string): Promise<CredentialStatusInfo[]> {
|
|
56
|
+
const results: CredentialStatusInfo[] = []
|
|
57
|
+
|
|
58
|
+
for (const [name, meta] of Object.entries(this.credentialsMeta)) {
|
|
59
|
+
results.push({
|
|
60
|
+
name,
|
|
61
|
+
displayName: meta.displayName,
|
|
62
|
+
isConfigured: await this.credentials.has(name, userId),
|
|
63
|
+
type: meta.type,
|
|
64
|
+
oauth2: meta.oauth2,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return results
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getMissing(userId?: string): Promise<CredentialStatusInfo[]> {
|
|
72
|
+
const all = await this.getAllStatus(userId)
|
|
73
|
+
return all.filter((c) => !c.isConfigured)
|
|
74
|
+
}
|
|
75
|
+
}
|