@codemation/core-nodes 0.4.3 → 0.6.0

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 (55) hide show
  1. package/CHANGELOG.md +215 -0
  2. package/dist/index.cjs +3485 -474
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1763 -685
  5. package/dist/index.d.ts +1763 -685
  6. package/dist/index.js +3452 -479
  7. package/dist/index.js.map +1 -1
  8. package/package.json +8 -5
  9. package/src/authoring/defineRestNode.types.ts +204 -0
  10. package/src/chatModels/OpenAIChatModelFactory.ts +17 -8
  11. package/src/chatModels/OpenAiStrictJsonSchemaFactory.ts +123 -0
  12. package/src/credentials/ApiKeyCredentialType.ts +60 -0
  13. package/src/credentials/BasicAuthCredentialType.ts +51 -0
  14. package/src/credentials/BearerTokenCredentialType.ts +40 -0
  15. package/src/credentials/OAuth2ClientCredentialsTypeFactory.ts +117 -0
  16. package/src/credentials/OAuth2TokenExchangeFactory.ts +52 -0
  17. package/src/credentials/index.ts +4 -0
  18. package/src/http/HttpBodyBuilder.ts +90 -0
  19. package/src/http/HttpRequestExecutor.ts +150 -0
  20. package/src/http/HttpUrlBuilder.ts +22 -0
  21. package/src/http/httpRequest.types.ts +69 -0
  22. package/src/index.ts +10 -1
  23. package/src/nodes/AIAgentExecutionHelpersFactory.ts +45 -59
  24. package/src/nodes/AIAgentNode.ts +391 -288
  25. package/src/nodes/AgentMessageFactory.ts +57 -49
  26. package/src/nodes/AgentStructuredOutputRunner.ts +65 -71
  27. package/src/nodes/AgentToolExecutionCoordinator.ts +31 -16
  28. package/src/nodes/AssertionNode.ts +42 -0
  29. package/src/nodes/CronTriggerFactory.ts +45 -0
  30. package/src/nodes/CronTriggerNode.ts +40 -0
  31. package/src/nodes/HttpRequestNodeFactory.ts +99 -23
  32. package/src/nodes/IsTestRunNode.ts +25 -0
  33. package/src/nodes/NodeBackedToolRuntime.ts +40 -4
  34. package/src/nodes/TestTriggerNode.ts +33 -0
  35. package/src/nodes/WebhookTriggerFactory.ts +1 -1
  36. package/src/nodes/aggregate.ts +1 -1
  37. package/src/nodes/aiAgentSupport.types.ts +22 -2
  38. package/src/nodes/assertion.ts +42 -0
  39. package/src/nodes/collections/collectionDeleteNode.types.ts +23 -0
  40. package/src/nodes/collections/collectionFindOneNode.types.ts +26 -0
  41. package/src/nodes/collections/collectionGetNode.types.ts +26 -0
  42. package/src/nodes/collections/collectionInsertNode.types.ts +22 -0
  43. package/src/nodes/collections/collectionListNode.types.ts +30 -0
  44. package/src/nodes/collections/collectionUpdateNode.types.ts +23 -0
  45. package/src/nodes/collections/index.ts +6 -0
  46. package/src/nodes/httpRequest.ts +62 -1
  47. package/src/nodes/if.ts +1 -1
  48. package/src/nodes/isTestRun.ts +24 -0
  49. package/src/nodes/mapData.ts +1 -0
  50. package/src/nodes/merge.ts +1 -1
  51. package/src/nodes/noOp.ts +1 -0
  52. package/src/nodes/split.ts +1 -1
  53. package/src/nodes/testTrigger.ts +72 -0
  54. package/src/nodes/wait.ts +1 -0
  55. package/src/chatModels/OpenAIStructuredOutputMethodFactory.ts +0 -46
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/core-nodes",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -28,10 +28,12 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@langchain/core": "^1.1.31",
32
- "@langchain/openai": "^1.2.12",
31
+ "@ai-sdk/openai": "^3.0.53",
32
+ "@ai-sdk/provider": "^3.0.8",
33
+ "ai": "^6.0.168",
34
+ "croner": "^10.0.1",
33
35
  "lucide-react": "^0.577.0",
34
- "@codemation/core": "0.8.1"
36
+ "@codemation/core": "0.10.0"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@types/node": "^25.3.5",
@@ -49,6 +51,7 @@
49
51
  "build": "tsdown",
50
52
  "typecheck": "tsc -p tsconfig.json --noEmit",
51
53
  "lint": "eslint .",
52
- "test": "pnpm --filter @codemation/core build && pnpm build && vitest run"
54
+ "test": "pnpm --filter @codemation/core build && pnpm build && vitest run",
55
+ "test:unit": "vitest run"
53
56
  }
54
57
  }
@@ -0,0 +1,204 @@
1
+ import { defineNode } from "@codemation/core";
2
+ import type { DefinedNode, DefinedNodeCredentialBindings } from "@codemation/core";
3
+ import type { ZodType } from "zod";
4
+ import type { HttpBodySpec } from "../http/httpRequest.types";
5
+ import { HttpRequestExecutor } from "../http/HttpRequestExecutor";
6
+ import { HttpBodyBuilder } from "../http/HttpBodyBuilder";
7
+ import { HttpUrlBuilder } from "../http/HttpUrlBuilder";
8
+
9
+ type MaybePromise<T> = T | Promise<T>;
10
+
11
+ /**
12
+ * API endpoint descriptor.
13
+ */
14
+ export type RestNodeApi = Readonly<{
15
+ /**
16
+ * Base URL, e.g. `"https://api.slack.com"`.
17
+ */
18
+ baseUrl: string;
19
+ /**
20
+ * Path relative to `baseUrl`. May contain `{paramName}` placeholders that
21
+ * are substituted from `input` keys before the request is made.
22
+ * Example: `"/users/{userId}/profile"`
23
+ */
24
+ path: string;
25
+ /** HTTP method (default: GET). */
26
+ method?: string;
27
+ }>;
28
+
29
+ /**
30
+ * The HTTP result shape passed into the `response` mapper.
31
+ */
32
+ export type RestNodeResponseContext = Readonly<{
33
+ status: number;
34
+ ok: boolean;
35
+ statusText: string;
36
+ mimeType: string;
37
+ headers: Readonly<Record<string, string>>;
38
+ json?: unknown;
39
+ text?: string;
40
+ }>;
41
+
42
+ /**
43
+ * What the `request` callback may return to customise the request.
44
+ */
45
+ export type RestNodeRequestShape = Readonly<{
46
+ /** Additional path parameters to substitute (merged with `input`). */
47
+ pathParams?: Readonly<Record<string, string>>;
48
+ /** Extra query params. */
49
+ query?: Readonly<Record<string, string>>;
50
+ /** Extra headers. */
51
+ headers?: Readonly<Record<string, string>>;
52
+ /** Request body. */
53
+ body?: HttpBodySpec;
54
+ }>;
55
+
56
+ /**
57
+ * Error handling policy for non-2xx responses.
58
+ * - `"throw"` (default) — throws an `Error` for non-2xx responses.
59
+ * - `"passthrough"` — returns the result regardless of status.
60
+ */
61
+ export type RestNodeErrorPolicy = "throw" | "passthrough";
62
+
63
+ export interface DefineRestNodeOptions<
64
+ TKey extends string,
65
+ TCredentials extends DefinedNodeCredentialBindings | undefined,
66
+ TInputJson,
67
+ TOutputJson,
68
+ > {
69
+ readonly key: TKey;
70
+ readonly title: string;
71
+ readonly description?: string;
72
+ readonly icon?: string;
73
+ readonly api: RestNodeApi;
74
+ /**
75
+ * Credential bindings keyed by slot. Use the built-in credential types from
76
+ * `@codemation/core-nodes` (e.g. `bearerTokenCredentialType`) or any custom one.
77
+ * The slot key must match what the `request` callback's context uses.
78
+ */
79
+ readonly credentials?: TCredentials;
80
+ /**
81
+ * Zod schema for per-item input. Validated before `execute`.
82
+ */
83
+ readonly inputSchema?: ZodType<TInputJson>;
84
+ /**
85
+ * Builds the per-request customisations from the item input.
86
+ * Return `body`, `query`, `headers`, and/or `pathParams`.
87
+ */
88
+ request?(context: Readonly<{ input: TInputJson }>): MaybePromise<RestNodeRequestShape>;
89
+ /**
90
+ * Maps the HTTP response to the node's output JSON.
91
+ * When omitted, the output is `{ status, ok, statusText, mimeType, headers, json, text }`.
92
+ */
93
+ response?(context: RestNodeResponseContext & Readonly<{ input: TInputJson }>): MaybePromise<TOutputJson>;
94
+ /**
95
+ * How to handle non-2xx responses.
96
+ * @default "throw"
97
+ */
98
+ readonly errorPolicy?: RestNodeErrorPolicy;
99
+ }
100
+
101
+ /**
102
+ * Substitutes `{name}` placeholders in a path template using values from `params`.
103
+ */
104
+ function substitutePath(template: string, params: Readonly<Record<string, unknown>>): string {
105
+ return template.replace(/\{([^}]+)}/g, (_match, key: string) => {
106
+ const value = params[key];
107
+ return value !== undefined ? String(value) : `{${key}}`;
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Declarative helper for creating thin API-wrapper nodes.
113
+ *
114
+ * Usage:
115
+ * ```ts
116
+ * export const postMessage = defineRestNode({
117
+ * key: "slack.post-message",
118
+ * title: "Send Slack message",
119
+ * icon: "si:slack",
120
+ * api: { baseUrl: "https://slack.com/api", path: "/chat.postMessage", method: "POST" },
121
+ * credentials: { auth: bearerTokenCredentialType },
122
+ * inputSchema: z.object({ channel: z.string(), text: z.string() }),
123
+ * request: ({ input }) => ({
124
+ * body: { kind: "json", data: { channel: input.channel, text: input.text } },
125
+ * }),
126
+ * response: ({ json }) => ({ messageTs: (json as any).ts }),
127
+ * });
128
+ * ```
129
+ *
130
+ * - `defineRestNode` is a thin wrapper over `defineNode`; it does not introduce a new runtime kind.
131
+ * - Credential sessions are resolved via the `credentials` binding map (same as `defineNode`).
132
+ * - Path `{placeholder}` substitution is applied from `input` keys before the request is made.
133
+ * - Non-2xx responses throw an `Error` by default (`errorPolicy: "throw"`).
134
+ */
135
+ export function defineRestNode<
136
+ TKey extends string,
137
+ TCredentials extends DefinedNodeCredentialBindings | undefined,
138
+ TInputJson,
139
+ TOutputJson = RestNodeResponseContext,
140
+ >(
141
+ options: DefineRestNodeOptions<TKey, TCredentials, TInputJson, TOutputJson>,
142
+ ): DefinedNode<TKey, Record<string, never>, TInputJson, TOutputJson, TCredentials> {
143
+ const errorPolicy = options.errorPolicy ?? "throw";
144
+
145
+ return defineNode<TKey, Record<string, never>, TInputJson, TOutputJson, TCredentials>({
146
+ key: options.key,
147
+ title: options.title,
148
+ description: options.description,
149
+ icon: options.icon,
150
+ credentials: options.credentials,
151
+ inputSchema: options.inputSchema,
152
+ async execute({ input, item, ctx }, { credentials }) {
153
+ // Resolve credential if one is bound.
154
+ const credentialSlot = options.credentials ? Object.keys(options.credentials)[0] : undefined;
155
+ const credential = credentialSlot
156
+ ? await (credentials as Record<string, () => Promise<unknown>>)[credentialSlot]?.()
157
+ : undefined;
158
+
159
+ // Build path by substituting `{name}` placeholders from input.
160
+ const inputRecord = (input as Record<string, unknown>) ?? {};
161
+ const requestShape = options.request ? await options.request({ input }) : {};
162
+ const pathParams = { ...inputRecord, ...(requestShape.pathParams ?? {}) };
163
+ const resolvedPath = substitutePath(options.api.path, pathParams);
164
+ const resolvedUrl = `${options.api.baseUrl}${resolvedPath}`;
165
+
166
+ const executor = new HttpRequestExecutor(globalThis.fetch, new HttpBodyBuilder(), new HttpUrlBuilder());
167
+ const result = await executor.execute(
168
+ {
169
+ url: resolvedUrl,
170
+ method: (options.api.method ?? "GET").toUpperCase(),
171
+ headers: requestShape.headers,
172
+ query: requestShape.query,
173
+ body: requestShape.body,
174
+ credential: credential as Parameters<typeof executor.execute>[0]["credential"],
175
+ ctx: ctx as unknown as Parameters<typeof executor.execute>[0]["ctx"],
176
+ },
177
+ item,
178
+ );
179
+
180
+ if (errorPolicy === "throw" && !result.ok) {
181
+ throw new Error(`HTTP ${result.status} ${result.statusText} for ${result.method} ${result.url}`);
182
+ }
183
+
184
+ const responseCtx: RestNodeResponseContext = {
185
+ status: result.status,
186
+ ok: result.ok,
187
+ statusText: result.statusText,
188
+ mimeType: result.mimeType,
189
+ headers: result.headers,
190
+ ...(result.json !== undefined ? { json: result.json } : {}),
191
+ ...(result.text !== undefined ? { text: result.text } : {}),
192
+ };
193
+
194
+ if (options.response) {
195
+ return await options.response({ ...responseCtx, input });
196
+ }
197
+
198
+ // Wrap in `{ json: ... }` so the engine's Item-shape detection unwraps once
199
+ // and the response context becomes the item's payload as-is (preserving the
200
+ // inner `json` field on the response for callers).
201
+ return { json: responseCtx } as unknown as TOutputJson;
202
+ },
203
+ }) as unknown as DefinedNode<TKey, Record<string, never>, TInputJson, TOutputJson, TCredentials>;
204
+ }
@@ -1,6 +1,8 @@
1
- import type { ChatModelFactory, LangChainChatModelLike, NodeExecutionContext } from "@codemation/core";
1
+ import type { ChatLanguageModel, ChatModelFactory, NodeExecutionContext } from "@codemation/core";
2
2
  import { chatModel } from "@codemation/core";
3
- import { ChatOpenAI } from "@langchain/openai";
3
+
4
+ import { createOpenAI } from "@ai-sdk/openai";
5
+
4
6
  import type { OpenAiCredentialSession } from "./OpenAiCredentialSession";
5
7
  import type { OpenAIChatModelConfig } from "./openAiChatModelConfig";
6
8
 
@@ -8,14 +10,21 @@ import type { OpenAIChatModelConfig } from "./openAiChatModelConfig";
8
10
  export class OpenAIChatModelFactory implements ChatModelFactory<OpenAIChatModelConfig> {
9
11
  async create(
10
12
  args: Readonly<{ config: OpenAIChatModelConfig; ctx: NodeExecutionContext<any> }>,
11
- ): Promise<LangChainChatModelLike> {
13
+ ): Promise<ChatLanguageModel> {
12
14
  const session = await args.ctx.getCredential<OpenAiCredentialSession>(args.config.credentialSlotKey);
13
- return new ChatOpenAI({
15
+ const provider = createOpenAI({
14
16
  apiKey: session.apiKey,
15
- model: args.config.model,
16
- temperature: args.config.options?.temperature,
17
- maxTokens: args.config.options?.maxTokens,
18
- configuration: session.baseUrl ? { baseURL: session.baseUrl } : undefined,
17
+ baseURL: session.baseUrl,
19
18
  });
19
+ const languageModel = provider.chat(args.config.model);
20
+ return {
21
+ languageModel,
22
+ modelName: args.config.model,
23
+ provider: "openai",
24
+ defaultCallOptions: {
25
+ maxOutputTokens: args.config.options?.maxTokens,
26
+ temperature: args.config.options?.temperature,
27
+ },
28
+ };
20
29
  }
21
30
  }
@@ -0,0 +1,123 @@
1
+ import type { ZodSchemaAny } from "@codemation/core";
2
+ import { inject, injectable } from "@codemation/core";
3
+
4
+ import { AIAgentExecutionHelpersFactory } from "../nodes/AIAgentExecutionHelpersFactory";
5
+
6
+ /**
7
+ * Produces an OpenAI **strict mode**–compliant JSON Schema for an AIAgent `outputSchema`.
8
+ *
9
+ * Why this exists: AI SDK's default Zod → JSON Schema conversion (Zod v4's `toJSONSchema`) can
10
+ * emit `unevaluatedProperties: false` or skip `additionalProperties: false` on object branches.
11
+ * OpenAI's strict-mode validator rejects anything missing `additionalProperties: false` at
12
+ * `context=()` (the root) and requires **all properties** in `required`. We convert here so all
13
+ * legal Zod root shapes work (object, union, discriminated union, nullable-object wrapper, array,
14
+ * intersection, …) and hand AI SDK a pre-tagged `jsonSchema(...)` record that passes straight
15
+ * through to the provider.
16
+ *
17
+ * Rules enforced on the produced JSON Schema record:
18
+ * - Every `type: "object"` node (root and nested under `allOf`/`anyOf`/`oneOf`/`items`/`prefixItems`/`$defs`):
19
+ * - `additionalProperties: false`
20
+ * - `required` lists **every** key in `properties` (OpenAI strict requires all properties required;
21
+ * express optionality via `.nullable()` / `z.union([..., z.null()])`).
22
+ * - `properties` is always an object (empty object allowed).
23
+ * - `$schema`, `unevaluatedProperties`, and `default` are stripped (OpenAI rejects / ignores them).
24
+ * - `sanitizeJsonSchemaRequiredKeywordsForCfworker` invariants from
25
+ * {@link AIAgentExecutionHelpersFactory.createJsonSchemaRecord} are preserved as a starting point.
26
+ */
27
+ @injectable()
28
+ export class OpenAiStrictJsonSchemaFactory {
29
+ constructor(
30
+ @inject(AIAgentExecutionHelpersFactory)
31
+ private readonly executionHelpers: AIAgentExecutionHelpersFactory,
32
+ ) {}
33
+
34
+ createStructuredOutputRecord(
35
+ schema: ZodSchemaAny,
36
+ options: Readonly<{ schemaName: string; title?: string }>,
37
+ ): Record<string, unknown> {
38
+ const record = this.executionHelpers.createJsonSchemaRecord(schema, {
39
+ schemaName: options.schemaName,
40
+ requireObjectRoot: false,
41
+ });
42
+ this.strictifyRecursive(record);
43
+ if (options.title !== undefined) {
44
+ record.title = options.title;
45
+ }
46
+ return record;
47
+ }
48
+
49
+ private strictifyRecursive(node: unknown): void {
50
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
51
+ return;
52
+ }
53
+ const o = node as Record<string, unknown>;
54
+ this.stripOpenAiRejectedKeywords(o);
55
+ if (this.isObjectNode(o)) {
56
+ const props = this.readPropertiesObject(o);
57
+ o.properties = props;
58
+ o.additionalProperties = false;
59
+ o.required = Object.keys(props);
60
+ for (const value of Object.values(props)) {
61
+ this.strictifyRecursive(value);
62
+ }
63
+ }
64
+ this.recurseIntoComposites(o);
65
+ }
66
+
67
+ private stripOpenAiRejectedKeywords(o: Record<string, unknown>): void {
68
+ delete o["$schema"];
69
+ delete o["unevaluatedProperties"];
70
+ delete o["default"];
71
+ }
72
+
73
+ private isObjectNode(o: Record<string, unknown>): boolean {
74
+ const typeIsObject =
75
+ o.type === "object" || (Array.isArray(o.type) && (o.type as ReadonlyArray<unknown>).includes("object"));
76
+ const hasObjectProperties =
77
+ o.properties !== undefined && typeof o.properties === "object" && !Array.isArray(o.properties);
78
+ return typeIsObject || hasObjectProperties;
79
+ }
80
+
81
+ private readPropertiesObject(o: Record<string, unknown>): Record<string, unknown> {
82
+ if (o.properties && typeof o.properties === "object" && !Array.isArray(o.properties)) {
83
+ return o.properties as Record<string, unknown>;
84
+ }
85
+ return {};
86
+ }
87
+
88
+ private recurseIntoComposites(o: Record<string, unknown>): void {
89
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"] as const) {
90
+ const branch = o[key];
91
+ if (Array.isArray(branch)) {
92
+ for (const sub of branch) {
93
+ this.strictifyRecursive(sub);
94
+ }
95
+ }
96
+ }
97
+ if (o.not) {
98
+ this.strictifyRecursive(o.not);
99
+ }
100
+ if (o.items) {
101
+ if (Array.isArray(o.items)) {
102
+ for (const sub of o.items) {
103
+ this.strictifyRecursive(sub);
104
+ }
105
+ } else {
106
+ this.strictifyRecursive(o.items);
107
+ }
108
+ }
109
+ for (const key of ["if", "then", "else"] as const) {
110
+ if (o[key]) {
111
+ this.strictifyRecursive(o[key]);
112
+ }
113
+ }
114
+ for (const key of ["$defs", "definitions"] as const) {
115
+ const defs = o[key];
116
+ if (defs && typeof defs === "object" && !Array.isArray(defs)) {
117
+ for (const sub of Object.values(defs as Record<string, unknown>)) {
118
+ this.strictifyRecursive(sub);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,60 @@
1
+ import { defineCredential } from "@codemation/core";
2
+ import type { CredentialSession, HttpCredentialDelta } from "../http/httpRequest.types";
3
+
4
+ /**
5
+ * API key credential that injects a key either as an HTTP header or a query parameter.
6
+ */
7
+ export const apiKeyCredentialType = defineCredential({
8
+ key: "core-nodes.api-key",
9
+ label: "API Key",
10
+ description: "Authenticates requests by injecting an API key into a header or query parameter.",
11
+ public: {
12
+ placement: {
13
+ label: "Placement",
14
+ type: "string",
15
+ helpText: 'Where to send the key: "header" (default) or "query".',
16
+ placeholder: "header",
17
+ },
18
+ name: {
19
+ label: "Parameter name",
20
+ type: "string",
21
+ helpText: 'Header or query param name. Defaults to "X-API-Key" for headers, "api_key" for query.',
22
+ placeholder: "X-API-Key",
23
+ },
24
+ },
25
+ secret: {
26
+ apiKey: {
27
+ label: "API Key",
28
+ type: "password",
29
+ required: true,
30
+ helpText: "The secret API key value.",
31
+ },
32
+ },
33
+ async createSession(args): Promise<CredentialSession> {
34
+ const apiKey = String(args.material.apiKey ?? "");
35
+ if (!apiKey) {
36
+ throw new Error("API key credential material is incomplete: apiKey is required.");
37
+ }
38
+ const placement = String(args.publicConfig.placement ?? "header").toLowerCase();
39
+ const isQuery = placement === "query";
40
+ const defaultName = isQuery ? "api_key" : "X-API-Key";
41
+ const paramName = String(args.publicConfig.name ?? "").trim() || defaultName;
42
+
43
+ return {
44
+ applyToRequest: (_spec): HttpCredentialDelta => {
45
+ if (isQuery) {
46
+ return { query: { [paramName]: apiKey } };
47
+ }
48
+ return { headers: { [paramName]: apiKey } };
49
+ },
50
+ };
51
+ },
52
+ async test(args) {
53
+ const apiKey = String(args.material.apiKey ?? "");
54
+ return {
55
+ status: apiKey.length > 0 ? "healthy" : "failing",
56
+ message: apiKey.length > 0 ? "API key is configured." : "API key is missing.",
57
+ testedAt: new Date().toISOString(),
58
+ };
59
+ },
60
+ });
@@ -0,0 +1,51 @@
1
+ import { defineCredential } from "@codemation/core";
2
+ import type { CredentialSession, HttpCredentialDelta } from "../http/httpRequest.types";
3
+
4
+ /**
5
+ * HTTP Basic authentication credential.
6
+ * Session sets `Authorization: Basic <base64(username:password)>`.
7
+ */
8
+ export const basicAuthCredentialType = defineCredential({
9
+ key: "core-nodes.basic-auth",
10
+ label: "Basic Auth",
11
+ description: "Authenticates requests using HTTP Basic Authentication (username + password).",
12
+ public: {
13
+ username: {
14
+ label: "Username",
15
+ type: "string",
16
+ required: true,
17
+ helpText: "The username for HTTP Basic Authentication.",
18
+ },
19
+ },
20
+ secret: {
21
+ password: {
22
+ label: "Password",
23
+ type: "password",
24
+ required: true,
25
+ helpText: "The password for HTTP Basic Authentication.",
26
+ },
27
+ },
28
+ async createSession(args): Promise<CredentialSession> {
29
+ const username = String(args.publicConfig.username ?? "");
30
+ const password = String(args.material.password ?? "");
31
+ if (!username) {
32
+ throw new Error("Basic Auth credential is incomplete: username is required.");
33
+ }
34
+ const encoded = Buffer.from(`${username}:${password}`).toString("base64");
35
+ return {
36
+ applyToRequest: (_spec): HttpCredentialDelta => ({
37
+ headers: { authorization: `Basic ${encoded}` },
38
+ }),
39
+ };
40
+ },
41
+ async test(args) {
42
+ const username = String(args.publicConfig.username ?? "");
43
+ const password = String(args.material.password ?? "");
44
+ const ok = username.length > 0 && password.length > 0;
45
+ return {
46
+ status: ok ? "healthy" : "failing",
47
+ message: ok ? "Basic Auth credentials are configured." : "Username or password is missing.",
48
+ testedAt: new Date().toISOString(),
49
+ };
50
+ },
51
+ });
@@ -0,0 +1,40 @@
1
+ import { defineCredential } from "@codemation/core";
2
+ import type { CredentialSession, HttpCredentialDelta } from "../http/httpRequest.types";
3
+
4
+ /**
5
+ * Simple Bearer token credential.
6
+ * Session sets `Authorization: Bearer <token>` on every request.
7
+ */
8
+ export const bearerTokenCredentialType = defineCredential({
9
+ key: "core-nodes.bearer-token",
10
+ label: "Bearer Token",
11
+ description: "Authenticates requests using a static Bearer token in the Authorization header.",
12
+ public: {},
13
+ secret: {
14
+ token: {
15
+ label: "Token",
16
+ type: "password",
17
+ required: true,
18
+ helpText: "The Bearer token to include in the Authorization header.",
19
+ },
20
+ },
21
+ async createSession(args): Promise<CredentialSession> {
22
+ const token = String(args.material.token ?? "");
23
+ if (!token) {
24
+ throw new Error("Bearer token credential material is incomplete: token is required.");
25
+ }
26
+ return {
27
+ applyToRequest: (_spec): HttpCredentialDelta => ({
28
+ headers: { authorization: `Bearer ${token}` },
29
+ }),
30
+ };
31
+ },
32
+ async test(args) {
33
+ const token = String(args.material.token ?? "");
34
+ return {
35
+ status: token.length > 0 ? "healthy" : "failing",
36
+ message: token.length > 0 ? "Bearer token is configured." : "Token is missing.",
37
+ testedAt: new Date().toISOString(),
38
+ };
39
+ },
40
+ });
@@ -0,0 +1,117 @@
1
+ import { defineCredential } from "@codemation/core";
2
+ import type { CredentialSession, HttpCredentialDelta } from "../http/httpRequest.types";
3
+ import { OAuth2TokenExchangeFactory } from "./OAuth2TokenExchangeFactory";
4
+
5
+ /**
6
+ * OAuth2 client-credentials flow credential.
7
+ *
8
+ * This is a machine-to-machine flow: no user redirect occurs. The session
9
+ * POSTs to the configured `tokenUrl` with `client_credentials` grant, caches
10
+ * the resulting access token for the duration of the session, and injects it
11
+ * as `Authorization: Bearer <token>` on each request.
12
+ *
13
+ * Token caching is per-session only (one createSession call = one token fetch
14
+ * at most). Cross-session caching would require host-level state and is out of
15
+ * scope here. Because the engine creates a fresh session per execution, a new
16
+ * token is fetched once per node activation.
17
+ *
18
+ * NOTE: `auth` is intentionally omitted from the definition. The OAuth2
19
+ * `auth: { kind: "oauth2" }` shape signals an authorization-code / user-redirect
20
+ * flow; using it here would cause the host UI to render an OAuth consent button
21
+ * that goes nowhere. Client-credentials is a purely server-side flow.
22
+ */
23
+ export const oauth2ClientCredentialsType = defineCredential({
24
+ key: "core-nodes.oauth2-client-credentials",
25
+ label: "OAuth2 Client Credentials",
26
+ description:
27
+ "Machine-to-machine OAuth2 using the client_credentials grant. Exchanges client ID and secret for a bearer token before each workflow execution.",
28
+ public: {
29
+ tokenUrl: {
30
+ label: "Token URL",
31
+ type: "string",
32
+ required: true,
33
+ helpText: "The token endpoint URL, e.g. https://auth.example.com/oauth/token.",
34
+ },
35
+ scopes: {
36
+ label: "Scopes",
37
+ type: "string",
38
+ helpText: "Space-separated list of OAuth2 scopes to request (optional).",
39
+ },
40
+ audience: {
41
+ label: "Audience",
42
+ type: "string",
43
+ helpText: "Optional audience parameter sent to the token endpoint.",
44
+ visibility: "advanced",
45
+ },
46
+ },
47
+ secret: {
48
+ clientId: {
49
+ label: "Client ID",
50
+ type: "string",
51
+ required: true,
52
+ },
53
+ clientSecret: {
54
+ label: "Client Secret",
55
+ type: "password",
56
+ required: true,
57
+ },
58
+ },
59
+ async createSession(args): Promise<CredentialSession> {
60
+ const tokenUrl = String(args.publicConfig.tokenUrl ?? "");
61
+ const clientId = String(args.material.clientId ?? "");
62
+ const clientSecret = String(args.material.clientSecret ?? "");
63
+
64
+ if (!tokenUrl || !clientId || !clientSecret) {
65
+ throw new Error("OAuth2 client credentials are incomplete: tokenUrl, clientId, and clientSecret are required.");
66
+ }
67
+
68
+ // Fetch the token eagerly so any failure surfaces at session creation time.
69
+ const accessToken = await new OAuth2TokenExchangeFactory().create({
70
+ tokenUrl,
71
+ clientId,
72
+ clientSecret,
73
+ scopes: String(args.publicConfig.scopes ?? ""),
74
+ audience: String(args.publicConfig.audience ?? ""),
75
+ });
76
+
77
+ return {
78
+ applyToRequest: (_spec): HttpCredentialDelta => ({
79
+ headers: { authorization: `Bearer ${accessToken}` },
80
+ }),
81
+ };
82
+ },
83
+ async test(args) {
84
+ const tokenUrl = String(args.publicConfig.tokenUrl ?? "");
85
+ const clientId = String(args.material.clientId ?? "");
86
+ const clientSecret = String(args.material.clientSecret ?? "");
87
+
88
+ if (!tokenUrl || !clientId || !clientSecret) {
89
+ return {
90
+ status: "failing",
91
+ message: "tokenUrl, clientId, and clientSecret are all required.",
92
+ testedAt: new Date().toISOString(),
93
+ };
94
+ }
95
+
96
+ try {
97
+ await new OAuth2TokenExchangeFactory().create({
98
+ tokenUrl,
99
+ clientId,
100
+ clientSecret,
101
+ scopes: String(args.publicConfig.scopes ?? ""),
102
+ audience: String(args.publicConfig.audience ?? ""),
103
+ });
104
+ return {
105
+ status: "healthy",
106
+ message: "Token exchange succeeded.",
107
+ testedAt: new Date().toISOString(),
108
+ };
109
+ } catch (error) {
110
+ return {
111
+ status: "failing",
112
+ message: error instanceof Error ? error.message : String(error),
113
+ testedAt: new Date().toISOString(),
114
+ };
115
+ }
116
+ },
117
+ });