@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/function/function-runner.d.ts +3 -1
  3. package/dist/function/function-runner.js +5 -1
  4. package/dist/services/credential-service.d.ts +40 -0
  5. package/dist/services/credential-service.js +1 -0
  6. package/dist/services/credential-wire-service.d.ts +13 -0
  7. package/dist/services/credential-wire-service.js +31 -0
  8. package/dist/services/index.d.ts +5 -0
  9. package/dist/services/index.js +3 -0
  10. package/dist/services/local-credential-service.d.ts +10 -0
  11. package/dist/services/local-credential-service.js +33 -0
  12. package/dist/services/typed-credential-service.d.ts +27 -0
  13. package/dist/services/typed-credential-service.js +40 -0
  14. package/dist/testing/service-tests.d.ts +8 -0
  15. package/dist/testing/service-tests.js +106 -0
  16. package/dist/types/core.types.d.ts +7 -0
  17. package/dist/wirings/ai-agent/agent-dynamic-workflow.js +173 -53
  18. package/dist/wirings/credential/credential.types.d.ts +24 -0
  19. package/dist/wirings/credential/credential.types.js +1 -0
  20. package/dist/wirings/credential/index.d.ts +3 -0
  21. package/dist/wirings/credential/index.js +2 -0
  22. package/dist/wirings/credential/validate-credential-definitions.d.ts +6 -0
  23. package/dist/wirings/credential/validate-credential-definitions.js +38 -0
  24. package/dist/wirings/credential/wire-credential.d.ts +48 -0
  25. package/dist/wirings/credential/wire-credential.js +47 -0
  26. package/dist/wirings/http/http-runner.js +4 -0
  27. package/dist/wirings/oauth2/index.d.ts +2 -1
  28. package/dist/wirings/oauth2/index.js +1 -1
  29. package/dist/wirings/oauth2/oauth2-client.js +4 -8
  30. package/dist/wirings/oauth2/oauth2-routes.d.ts +35 -0
  31. package/dist/wirings/oauth2/oauth2-routes.js +146 -0
  32. package/dist/wirings/oauth2/oauth2.types.d.ts +0 -26
  33. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  34. package/dist/wirings/workflow/graph/graph-runner.js +15 -0
  35. package/dist/wirings/workflow/pikku-workflow-service.d.ts +2 -2
  36. package/dist/wirings/workflow/pikku-workflow-service.js +40 -8
  37. package/package.json +2 -1
  38. package/src/function/function-runner.ts +11 -0
  39. package/src/services/credential-service.ts +44 -0
  40. package/src/services/credential-wire-service.ts +44 -0
  41. package/src/services/index.ts +12 -0
  42. package/src/services/local-credential-service.ts +41 -0
  43. package/src/services/typed-credential-service.ts +75 -0
  44. package/src/testing/service-tests.ts +139 -0
  45. package/src/types/core.types.ts +7 -0
  46. package/src/wirings/ai-agent/agent-dynamic-workflow.ts +378 -237
  47. package/src/wirings/credential/credential.types.ts +28 -0
  48. package/src/wirings/credential/index.ts +8 -0
  49. package/src/wirings/credential/validate-credential-definitions.ts +64 -0
  50. package/src/wirings/credential/wire-credential.ts +49 -0
  51. package/src/wirings/http/http-runner.ts +7 -0
  52. package/src/wirings/oauth2/index.ts +2 -1
  53. package/src/wirings/oauth2/oauth2-client.ts +4 -8
  54. package/src/wirings/oauth2/oauth2-routes.ts +234 -0
  55. package/src/wirings/oauth2/oauth2.types.ts +0 -27
  56. package/src/wirings/workflow/graph/graph-runner.ts +33 -1
  57. package/src/wirings/workflow/pikku-workflow-service.ts +55 -9
  58. package/tsconfig.tsbuildinfo +1 -1
  59. package/dist/wirings/oauth2/wire-oauth2-credential.d.ts +0 -20
  60. package/dist/wirings/oauth2/wire-oauth2-credential.js +0 -21
  61. 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 { PikkuWorkflowService } from '../pikku-workflow-service.js';
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
- private createWorkflowWire;
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
- if (workflowMeta?.nodes && stepName in workflowMeta.nodes) {
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
- result = await rpcService.rpcWithWire(rpcName, data, {
381
- workflowStep: {
382
- runId,
383
- stepId: stepState.stepId,
384
- attemptCount: stepState.attemptCount,
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.9",
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
+ }
@@ -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
+ }