@ansible-flow/node 0.85.0 → 0.87.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Ansible Flow Designer for Node hosts: layout, the probe client, runs, the AWX client, the module catalog and SSH.
4
4
 
5
- Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.85.0
5
+ Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.87.0
6
6
  and released with it. Modules are imported by their path in the source tree, for example
7
7
  `@ansible-flow/node/ai/agents`.
8
8
 
@@ -0,0 +1,35 @@
1
+ import { type FlowMerge, type MergeSide } from '@ansible-flow/core/flow/merge';
2
+ /** A merge waiting for choices: the three versions, the file, and what was chosen so far. */
3
+ export interface MergeState {
4
+ /** The file's path in the repository, as git names it. */
5
+ file: string;
6
+ base: string;
7
+ ours: string;
8
+ theirs: string;
9
+ choices: Record<string, MergeSide>;
10
+ at: string;
11
+ }
12
+ export declare const MERGE_FOLDER = "afd-merge";
13
+ /** Where the merge of `file` waits, in the repository's git folder. */
14
+ export declare function mergeStatePath(gitDir: string, file: string): string;
15
+ export declare function readMergeState(gitDir: string, file: string): MergeState | undefined;
16
+ export declare function writeMergeState(gitDir: string, state: MergeState): void;
17
+ export declare function removeMergeState(gitDir: string, file: string): void;
18
+ export type Merged =
19
+ /** Not three flows this can read: the caller merges the text instead. */
20
+ {
21
+ kind: 'not-flows';
22
+ why: string;
23
+ } | {
24
+ kind: 'merged';
25
+ text: string;
26
+ merge: FlowMerge;
27
+ /** Errors in the merged flow that neither side had: a person looks before it is taken. */
28
+ broken: string[];
29
+ };
30
+ /** Merges three versions of a flow file's text, with `choices` made so far. */
31
+ export declare function mergeFlowTexts(base: string, ours: string, theirs: string, choices?: Record<string, MergeSide>): Merged;
32
+ export declare const MERGE_ATTRIBUTES: string[];
33
+ export declare const MERGE_DRIVER = "node .afd/afd-merge.js %O %A %B %P";
34
+ /** `.gitattributes` with the flow lines it lacks, or undefined when it has them all. */
35
+ export declare function withMergeAttributes(text: string): string | undefined;
@@ -0,0 +1,74 @@
1
+ // src/node/flowMerge.ts
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { parseFlow } from "@ansible-flow/core/flow/document";
5
+ import { mergeFlows } from "@ansible-flow/core/flow/merge";
6
+ import { writeMergedFlow } from "@ansible-flow/core/flow/mergeText";
7
+ import { validateFlow } from "@ansible-flow/core/flow/validate";
8
+ var MERGE_FOLDER = "afd-merge";
9
+ function mergeStatePath(gitDir, file) {
10
+ return join(gitDir, MERGE_FOLDER, `${encodeURIComponent(file.replaceAll("\\", "/"))}.json`);
11
+ }
12
+ function readMergeState(gitDir, file) {
13
+ const path = mergeStatePath(gitDir, file);
14
+ if (!existsSync(path)) return void 0;
15
+ try {
16
+ return JSON.parse(readFileSync(path, "utf8"));
17
+ } catch {
18
+ return void 0;
19
+ }
20
+ }
21
+ function writeMergeState(gitDir, state) {
22
+ const path = mergeStatePath(gitDir, state.file);
23
+ mkdirSync(join(gitDir, MERGE_FOLDER), { recursive: true });
24
+ writeFileSync(path, `${JSON.stringify(state, null, 1)}
25
+ `);
26
+ }
27
+ function removeMergeState(gitDir, file) {
28
+ rmSync(mergeStatePath(gitDir, file), { force: true });
29
+ }
30
+ var errorsOf = (doc) => new Set(
31
+ validateFlow(doc).findings.filter((f) => f.severity === "error").map((f) => f.message)
32
+ );
33
+ function mergeFlowTexts(base, ours, theirs, choices = {}) {
34
+ const docs = [base, ours, theirs].map((t2) => parseFlow(t2));
35
+ const names = ["the common version", "ours", "theirs"];
36
+ for (const [i, d] of docs.entries())
37
+ if (!d.doc) return { kind: "not-flows", why: `${names[i]} is not a flow this can read` };
38
+ const [b, o, t] = docs.map((d) => d.doc);
39
+ const merge = mergeFlows(b, o, t, choices);
40
+ let text;
41
+ try {
42
+ text = writeMergedFlow(ours, merge.doc);
43
+ } catch (e) {
44
+ return {
45
+ kind: "not-flows",
46
+ why: `the merged flow could not be written: ${e.message}`
47
+ };
48
+ }
49
+ const had = /* @__PURE__ */ new Set([...errorsOf(o), ...errorsOf(t)]);
50
+ const broken = [...errorsOf(merge.doc)].filter((m) => !had.has(m));
51
+ return { kind: "merged", text, merge, broken };
52
+ }
53
+ var MERGE_ATTRIBUTES = ["*.flow.yaml merge=ansible-flow", "*.flow.yml merge=ansible-flow"];
54
+ var MERGE_DRIVER = "node .afd/afd-merge.js %O %A %B %P";
55
+ function withMergeAttributes(text) {
56
+ const have = new Set(text.split(/\r?\n/).map((l) => l.trim().replace(/\s+/g, " ")));
57
+ const missing = MERGE_ATTRIBUTES.filter((l) => !have.has(l));
58
+ if (!missing.length) return void 0;
59
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
60
+ const head = text === "" || text.endsWith("\n") ? text : `${text}${eol}`;
61
+ return `${head}# Flows merge by their steps, not their lines (Ansible Flow Designer)${eol}${missing.join(eol)}${eol}`;
62
+ }
63
+ export {
64
+ MERGE_ATTRIBUTES,
65
+ MERGE_DRIVER,
66
+ MERGE_FOLDER,
67
+ mergeFlowTexts,
68
+ mergeStatePath,
69
+ readMergeState,
70
+ removeMergeState,
71
+ withMergeAttributes,
72
+ writeMergeState
73
+ };
74
+ //# sourceMappingURL=flowMerge.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/node/flowMerge.ts"],
4
+ "sourcesContent": ["// AFD-283: merging a flow file the way git asks a merge driver to, and the merge left for a person\n// to finish. The driver writes the merged flow over \"ours\"; when the two sides changed the same\n// place, it writes the flow with \"ours\" for each such place, keeps the three versions beside the\n// repository's git folder (never in the work tree), and tells git the file is not merged. The\n// designer reads that state, lets the person choose, and writes the file again.\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { parseFlow } from '../core/flow/document';\nimport { type FlowMerge, type MergeSide, mergeFlows } from '../core/flow/merge';\nimport { writeMergedFlow } from '../core/flow/mergeText';\nimport type { FlowDocument } from '../core/flow/types';\nimport { validateFlow } from '../core/flow/validate';\n\n/** A merge waiting for choices: the three versions, the file, and what was chosen so far. */\nexport interface MergeState {\n /** The file's path in the repository, as git names it. */\n file: string;\n base: string;\n ours: string;\n theirs: string;\n choices: Record<string, MergeSide>;\n at: string;\n}\n\nexport const MERGE_FOLDER = 'afd-merge';\n\n/** Where the merge of `file` waits, in the repository's git folder. */\nexport function mergeStatePath(gitDir: string, file: string): string {\n return join(gitDir, MERGE_FOLDER, `${encodeURIComponent(file.replaceAll('\\\\', '/'))}.json`);\n}\n\nexport function readMergeState(gitDir: string, file: string): MergeState | undefined {\n const path = mergeStatePath(gitDir, file);\n if (!existsSync(path)) return undefined;\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as MergeState;\n } catch {\n return undefined;\n }\n}\n\nexport function writeMergeState(gitDir: string, state: MergeState): void {\n const path = mergeStatePath(gitDir, state.file);\n mkdirSync(join(gitDir, MERGE_FOLDER), { recursive: true });\n writeFileSync(path, `${JSON.stringify(state, null, 1)}\\n`);\n}\n\nexport function removeMergeState(gitDir: string, file: string): void {\n rmSync(mergeStatePath(gitDir, file), { force: true });\n}\n\nexport type Merged =\n /** Not three flows this can read: the caller merges the text instead. */\n | { kind: 'not-flows'; why: string }\n | {\n kind: 'merged';\n text: string;\n merge: FlowMerge;\n /** Errors in the merged flow that neither side had: a person looks before it is taken. */\n broken: string[];\n };\n\nconst errorsOf = (doc: FlowDocument) =>\n new Set(\n validateFlow(doc)\n .findings.filter((f) => f.severity === 'error')\n .map((f) => f.message),\n );\n\n/** Merges three versions of a flow file's text, with `choices` made so far. */\nexport function mergeFlowTexts(\n base: string,\n ours: string,\n theirs: string,\n choices: Record<string, MergeSide> = {},\n): Merged {\n const docs = [base, ours, theirs].map((t) => parseFlow(t));\n const names = ['the common version', 'ours', 'theirs'];\n for (const [i, d] of docs.entries())\n if (!d.doc) return { kind: 'not-flows', why: `${names[i]} is not a flow this can read` };\n const [b, o, t] = docs.map((d) => d.doc as FlowDocument) as [\n FlowDocument,\n FlowDocument,\n FlowDocument,\n ];\n const merge = mergeFlows(b, o, t, choices);\n let text: string;\n try {\n text = writeMergedFlow(ours, merge.doc);\n } catch (e) {\n return {\n kind: 'not-flows',\n why: `the merged flow could not be written: ${(e as Error).message}`,\n };\n }\n const had = new Set([...errorsOf(o), ...errorsOf(t)]);\n const broken = [...errorsOf(merge.doc)].filter((m) => !had.has(m));\n return { kind: 'merged', text, merge, broken };\n}\n\n// --- setting a repository up (Set Up Flow Merging in Repository)\n\nexport const MERGE_ATTRIBUTES = ['*.flow.yaml merge=ansible-flow', '*.flow.yml merge=ansible-flow'];\nexport const MERGE_DRIVER = 'node .afd/afd-merge.js %O %A %B %P';\n\n/** `.gitattributes` with the flow lines it lacks, or undefined when it has them all. */\nexport function withMergeAttributes(text: string): string | undefined {\n const have = new Set(text.split(/\\r?\\n/).map((l) => l.trim().replace(/\\s+/g, ' ')));\n const missing = MERGE_ATTRIBUTES.filter((l) => !have.has(l));\n if (!missing.length) return undefined;\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const head = text === '' || text.endsWith('\\n') ? text : `${text}${eol}`;\n return `${head}# Flows merge by their steps, not their lines (Ansible Flow Designer)${eol}${missing.join(eol)}${eol}`;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["t"]
7
+ }
@@ -98,6 +98,27 @@ export interface LaunchRequest {
98
98
  export declare const RUNNER_API = "v1";
99
99
  /** Why this client and the server do not fit for a flow in `format`, or undefined. */
100
100
  export declare function incompatible(meta: RunnerMeta, format: number): string | undefined;
101
+ /** AFD-284: what happened to which flows between two commits, and who did it. */
102
+ export interface RunnerChanges {
103
+ from: string;
104
+ to: string;
105
+ flows: {
106
+ file: string;
107
+ change: 'added' | 'changed' | 'removed';
108
+ authors: string[];
109
+ at?: string;
110
+ commit?: string;
111
+ }[];
112
+ }
113
+ /** AFD-284: where the project's branch is (`commit`), and each time it moves (`moved`). */
114
+ export type ProjectEvent = {
115
+ type: 'commit';
116
+ commit: string | null;
117
+ } | {
118
+ type: 'moved';
119
+ from?: string;
120
+ to: string;
121
+ };
101
122
  export interface RunnerOptions {
102
123
  verifyTls?: boolean;
103
124
  /** Send the token over plain http to another machine (a lab only). */
@@ -112,6 +133,14 @@ export declare class RunnerClient {
112
133
  constructor(url: string, token: string, options?: RunnerOptions);
113
134
  /** A run's page on the server. */
114
135
  runUrl(id: string): string;
136
+ /** Refuses a URL the token must not go to: another origin, or plain http without consent. */
137
+ private guard;
138
+ /**
139
+ * AFD-284: follows a project's event stream (server-sent events) until `stop` is called, going
140
+ * back after a dropped connection (5 s, then up to a minute between tries). `commit` comes at
141
+ * once and after each reconnect, so a move missed while away is noticed.
142
+ */
143
+ follow(project: string, onEvent: (e: ProjectEvent) => void): () => void;
115
144
  /** The answer's body as text, and its status. */
116
145
  private send;
117
146
  request<T>(method: string, path: string, body?: unknown, raw?: {
@@ -142,6 +171,16 @@ export declare class RunnerClient {
142
171
  commit: string;
143
172
  expiresAt: string;
144
173
  }>;
174
+ /** AFD-284: the flows changed between `from` and the project's commit (or `to`). */
175
+ changes: (project: string, from: string, to?: string) => Promise<RunnerChanges>;
176
+ /** AFD-284: one flow as the server reads it at a commit (its document in `snapshot.doc`). */
177
+ flowAt: (project: string, path: string, commit: string) => Promise<{
178
+ commit: string;
179
+ file: string;
180
+ snapshot: {
181
+ doc?: unknown;
182
+ };
183
+ }>;
145
184
  /** What one host did in one step of the run. */
146
185
  hostOutput: (id: string, node: string, host: string) => Promise<{
147
186
  entries?: unknown[];
@@ -47,17 +47,86 @@ var RunnerClient = class {
47
47
  runUrl(id) {
48
48
  return `${this.base}/runs/${enc(id)}`;
49
49
  }
50
- /** The answer's body as text, and its status. */
51
- send(method, path, body, raw) {
52
- const url = new URL(`${this.base}${path}`);
50
+ /** Refuses a URL the token must not go to: another origin, or plain http without consent. */
51
+ guard(url) {
53
52
  if (url.origin !== this.origin)
54
- return Promise.reject(new RunnerError(`Not sending the token to ${url.origin}`));
53
+ return new RunnerError(`Not sending the token to ${url.origin}`);
55
54
  if (cleartextTo(url) && !this.options.allowHttp)
56
- return Promise.reject(
57
- new RunnerError(
58
- `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`
59
- )
55
+ return new RunnerError(
56
+ `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`
60
57
  );
58
+ return void 0;
59
+ }
60
+ /**
61
+ * AFD-284: follows a project's event stream (server-sent events) until `stop` is called, going
62
+ * back after a dropped connection (5 s, then up to a minute between tries). `commit` comes at
63
+ * once and after each reconnect, so a move missed while away is noticed.
64
+ */
65
+ follow(project, onEvent) {
66
+ let stopped = false;
67
+ let req;
68
+ let retry;
69
+ let wait = 5e3;
70
+ const url = new URL(`${this.base}/api/v1/projects/${enc(project)}/events`);
71
+ const problem = this.guard(url);
72
+ const again = () => {
73
+ if (stopped) return;
74
+ retry = setTimeout(connect, wait);
75
+ wait = Math.min(wait * 2, 6e4);
76
+ };
77
+ const connect = () => {
78
+ if (stopped || problem) return;
79
+ const lib = url.protocol === "https:" ? https : http;
80
+ req = lib.request(
81
+ url,
82
+ {
83
+ method: "GET",
84
+ headers: { Authorization: `Bearer ${this.token}`, Accept: "text/event-stream" },
85
+ ...url.protocol === "https:" ? { rejectUnauthorized: this.options.verifyTls !== false } : {}
86
+ },
87
+ (res) => {
88
+ if (res.statusCode !== 200) {
89
+ res.resume();
90
+ if (res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404) return;
91
+ again();
92
+ return;
93
+ }
94
+ wait = 5e3;
95
+ res.setEncoding("utf8");
96
+ let buffer = "";
97
+ res.on("data", (chunk) => {
98
+ buffer += chunk;
99
+ for (let at = buffer.indexOf("\n\n"); at >= 0; at = buffer.indexOf("\n\n")) {
100
+ const block = buffer.slice(0, at);
101
+ buffer = buffer.slice(at + 2);
102
+ const type = /^event: (.*)$/m.exec(block)?.[1];
103
+ const data = /^data: (.*)$/m.exec(block)?.[1];
104
+ if (!data || type !== "commit" && type !== "moved") continue;
105
+ try {
106
+ onEvent({ type, ...JSON.parse(data) });
107
+ } catch {
108
+ }
109
+ }
110
+ });
111
+ res.on("end", again);
112
+ res.on("error", again);
113
+ }
114
+ );
115
+ req.on("error", again);
116
+ req.end();
117
+ };
118
+ connect();
119
+ return () => {
120
+ stopped = true;
121
+ clearTimeout(retry);
122
+ req?.destroy();
123
+ };
124
+ }
125
+ /** The answer's body as text, and its status. */
126
+ send(method, path, body, raw) {
127
+ const url = new URL(`${this.base}${path}`);
128
+ const refused = this.guard(url);
129
+ if (refused) return Promise.reject(refused);
61
130
  const lib = url.protocol === "https:" ? https : http;
62
131
  const data = raw?.bytes ?? (body === void 0 ? void 0 : JSON.stringify(body));
63
132
  const type = raw?.type ?? "application/json";
@@ -153,6 +222,16 @@ var RunnerClient = class {
153
222
  void 0,
154
223
  { bytes, type: "application/x-git-bundle" }
155
224
  );
225
+ /** AFD-284: the flows changed between `from` and the project's commit (or `to`). */
226
+ changes = (project, from, to) => this.request(
227
+ "GET",
228
+ `/api/v1/projects/${enc(project)}/changes?from=${enc(from)}${to ? `&to=${enc(to)}` : ""}`
229
+ );
230
+ /** AFD-284: one flow as the server reads it at a commit (its document in `snapshot.doc`). */
231
+ flowAt = (project, path, commit) => this.request(
232
+ "GET",
233
+ `/api/v1/projects/${enc(project)}/flows/view?path=${enc(path)}&commit=${enc(commit)}`
234
+ );
156
235
  /** What one host did in one step of the run. */
157
236
  hostOutput = (id, node, host) => this.request(
158
237
  "GET",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/node/runner/client.ts"],
4
- "sourcesContent": ["// A client of Ansible Flow Server (AFD-276): the routes the extension's server target uses, as\n// the designer's contract with the server names them (contract/server-api.json, AFD-280).\n// node:http(s) rather than fetch so TLS verification can be turned off for a lab server, as the\n// AWX client does; the token travels only in the Authorization header, to the configured server.\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport { cleartextTo } from '../awx/client';\nimport { TIMEOUTS } from '../timeouts';\n\nexport class RunnerError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n /** The server's error code (`not_found`, `read_only_token`, \u2026). */\n readonly code?: string,\n /** A launch's problems, by field. */\n readonly problems?: { field?: string; message: string }[],\n ) {\n super(message);\n this.name = 'RunnerError';\n }\n}\n\nexport interface RunnerMeta {\n name: string;\n version: string;\n api: string;\n designer: { version: string; format: number; formats?: number[]; bundle?: string };\n features?: string[];\n}\n\nexport interface RunnerProject {\n id: string;\n key: string;\n name: string;\n gitPath: string;\n gitRef: string;\n commit?: string;\n}\n\nexport interface RunnerConfig {\n id: string;\n name: string;\n flowFile: string;\n mode: 'check' | 'real';\n inventory?: string;\n gitRef?: string;\n commit?: string;\n}\n\n/** What a launch would run (the server's PreparedLaunch, without its inputs). */\nexport interface RunnerLaunch {\n commit: string;\n ref?: string;\n flow: { file: string; key: string; name: string; version: number };\n mode: 'check' | 'real';\n inventory?: string;\n targets: string[];\n}\n\nexport interface RunnerRun {\n id: string;\n number: number;\n project: string;\n commit: string;\n flow: { file: string; key: string; name: string };\n mode: 'check' | 'real';\n state: 'queued' | 'running' | 'waiting' | 'succeeded' | 'failed' | 'cancelled' | 'lost';\n error?: string;\n endedAt?: string;\n /** AFD-282: a check run of someone's unpushed work. */\n dev?: true;\n}\n\nexport interface RunnerGate {\n node: { id: string; key: string; title: string };\n status: string;\n mayDecide: boolean;\n why?: string;\n}\n\nexport interface LaunchRequest {\n mode?: 'check' | 'real';\n /** AFD-282: a dev bundle's id: the server runs its commit, in check mode. */\n dev?: string;\n limit?: string;\n /** The flow's inputs as typed text, by name. */\n inputs?: Record<string, string>;\n}\n\n/** The REST API this client speaks. */\nexport const RUNNER_API = 'v1';\n\n/** Why this client and the server do not fit for a flow in `format`, or undefined. */\nexport function incompatible(meta: RunnerMeta, format: number): string | undefined {\n if (meta.name !== 'Ansible Flow Server') return 'The address is not an Ansible Flow Server';\n if (meta.api !== RUNNER_API)\n return `The server speaks API ${meta.api}; this extension speaks ${RUNNER_API}: update the one that is older`;\n const formats = meta.designer.formats ?? [meta.designer.format];\n if (!formats.includes(format))\n return `The flow is in format ${format}; the server (${meta.version}) reads formats ${formats.join(', ')}: upgrade the server`;\n return undefined;\n}\n\nexport interface RunnerOptions {\n verifyTls?: boolean;\n /** Send the token over plain http to another machine (a lab only). */\n allowHttp?: boolean;\n timeoutMs?: number;\n}\n\nconst MAX_BODY = 32 * 1024 * 1024;\nconst enc = encodeURIComponent;\n\nexport class RunnerClient {\n readonly base: string;\n private readonly origin: string;\n\n constructor(\n url: string,\n private readonly token: string,\n private readonly options: RunnerOptions = {},\n ) {\n this.base = url.replace(/\\/+$/, '');\n this.origin = new URL(this.base).origin;\n }\n\n /** A run's page on the server. */\n runUrl(id: string): string {\n return `${this.base}/runs/${enc(id)}`;\n }\n\n /** The answer's body as text, and its status. */\n private send(\n method: string,\n path: string,\n body?: unknown,\n /** A body sent as it is, with its content type (a git bundle). */\n raw?: { bytes: Buffer; type: string },\n ): Promise<{ status: number; text: string }> {\n const url = new URL(`${this.base}${path}`);\n if (url.origin !== this.origin)\n return Promise.reject(new RunnerError(`Not sending the token to ${url.origin}`));\n if (cleartextTo(url) && !this.options.allowHttp)\n return Promise.reject(\n new RunnerError(\n `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`,\n ),\n );\n const lib = url.protocol === 'https:' ? https : http;\n const data = raw?.bytes ?? (body === undefined ? undefined : JSON.stringify(body));\n const type = raw?.type ?? 'application/json';\n return new Promise((resolve, reject) => {\n const req = lib.request(\n url,\n {\n method,\n headers: {\n Authorization: `Bearer ${this.token}`,\n Accept: 'application/json',\n ...(data ? { 'Content-Type': type, 'Content-Length': Buffer.byteLength(data) } : {}),\n },\n timeout: this.options.timeoutMs ?? TIMEOUTS.runnerRequest,\n ...(url.protocol === 'https:'\n ? { rejectUnauthorized: this.options.verifyTls !== false }\n : {}),\n },\n (res) => {\n const chunks: Buffer[] = [];\n let size = 0;\n res.on('data', (c: Buffer) => {\n size += c.length;\n if (size > MAX_BODY) {\n req.destroy(new Error('the answer is too large'));\n return;\n }\n chunks.push(c);\n });\n res.on('end', () =>\n resolve({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString('utf8') }),\n );\n res.on('error', reject);\n },\n );\n req.on('timeout', () => req.destroy(new Error('no answer in time')));\n req.on('error', (e) =>\n reject(new RunnerError(`The server at ${this.base} does not answer: ${e.message}`)),\n );\n req.end(data);\n });\n }\n\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n raw?: { bytes: Buffer; type: string },\n ): Promise<T> {\n const { status, text } = await this.send(method, path, body, raw);\n let json: unknown;\n try {\n json = text ? JSON.parse(text) : undefined;\n } catch {\n json = undefined;\n }\n if (status >= 200 && status < 300) return json as T;\n const e = (json ?? {}) as {\n error?: string;\n message?: string;\n problems?: { field?: string; message: string }[];\n };\n const why =\n status === 401\n ? 'The server does not take the token: set it again (it may have expired or been revoked)'\n : status === 413\n ? 'The work is larger than the server takes (AFS_DEV_BUNDLE_MAX_MB)'\n : (e.message ?? `The server answered ${status}`);\n throw new RunnerError(why, status, e.error, e.problems);\n }\n\n /** The output of a request whose answer is plain text. */\n async text(path: string): Promise<string> {\n const { status, text } = await this.send('GET', path);\n if (status >= 200 && status < 300) return text;\n throw new RunnerError(`The server answered ${status}`, status);\n }\n\n meta = () => this.request<RunnerMeta>('GET', '/api/v1/meta');\n me = () => this.request<{ login: string; name: string }>('GET', '/api/v1/session');\n projects = (workspace: string) =>\n this.request<RunnerProject[]>('GET', `/api/v1/workspaces/${enc(workspace)}/projects`);\n project = (id: string) => this.request<RunnerProject>('GET', `/api/v1/projects/${enc(id)}`);\n configs = (project: string) =>\n this.request<RunnerConfig[]>('GET', `/api/v1/projects/${enc(project)}/launch-configs`);\n check = async (config: string, req: LaunchRequest) =>\n (\n await this.request<{ launch: RunnerLaunch }>(\n 'POST',\n `/api/v1/launch-configs/${enc(config)}/check`,\n req,\n )\n ).launch;\n launch = (config: string, req: LaunchRequest) =>\n this.request<RunnerRun>('POST', `/api/v1/launch-configs/${enc(config)}/launch`, req);\n run = (id: string) => this.request<RunnerRun>('GET', `/api/v1/runs/${enc(id)}`);\n /** How the run went, as the designer shows it (the server's RunView); 404 until it has one. */\n view = async (id: string) => {\n try {\n return await this.request<Record<string, unknown>>('GET', `/api/v1/runs/${enc(id)}/view`);\n } catch (e) {\n if (e instanceof RunnerError && e.status === 404) return undefined;\n throw e;\n }\n };\n gates = (id: string) => this.request<RunnerGate[]>('GET', `/api/v1/runs/${enc(id)}/approvals`);\n cancel = (id: string) => this.request<RunnerRun>('POST', `/api/v1/runs/${enc(id)}/cancel`);\n decide = (id: string, node: string, decision: 'approved' | 'rejected', comment?: string) =>\n this.request<unknown>('POST', `/api/v1/runs/${enc(id)}/approvals/${enc(node)}`, {\n decision,\n ...(comment?.trim() ? { comment: comment.trim() } : {}),\n });\n /** AFD-282: unpushed work for a dev run; resolves with the bundle's id and commit. */\n devBundle = (project: string, bytes: Buffer) =>\n this.request<{ id: string; commit: string; expiresAt: string }>(\n 'POST',\n `/api/v1/projects/${enc(project)}/dev-bundles`,\n undefined,\n { bytes, type: 'application/x-git-bundle' },\n );\n /** What one host did in one step of the run. */\n hostOutput = (id: string, node: string, host: string) =>\n this.request<{ entries?: unknown[] }>(\n 'GET',\n `/api/v1/runs/${enc(id)}/hosts/${enc(node)}/${enc(host)}`,\n );\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// A client of Ansible Flow Server (AFD-276): the routes the extension's server target uses, as\n// the designer's contract with the server names them (contract/server-api.json, AFD-280).\n// node:http(s) rather than fetch so TLS verification can be turned off for a lab server, as the\n// AWX client does; the token travels only in the Authorization header, to the configured server.\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport { cleartextTo } from '../awx/client';\nimport { TIMEOUTS } from '../timeouts';\n\nexport class RunnerError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n /** The server's error code (`not_found`, `read_only_token`, \u2026). */\n readonly code?: string,\n /** A launch's problems, by field. */\n readonly problems?: { field?: string; message: string }[],\n ) {\n super(message);\n this.name = 'RunnerError';\n }\n}\n\nexport interface RunnerMeta {\n name: string;\n version: string;\n api: string;\n designer: { version: string; format: number; formats?: number[]; bundle?: string };\n features?: string[];\n}\n\nexport interface RunnerProject {\n id: string;\n key: string;\n name: string;\n gitPath: string;\n gitRef: string;\n commit?: string;\n}\n\nexport interface RunnerConfig {\n id: string;\n name: string;\n flowFile: string;\n mode: 'check' | 'real';\n inventory?: string;\n gitRef?: string;\n commit?: string;\n}\n\n/** What a launch would run (the server's PreparedLaunch, without its inputs). */\nexport interface RunnerLaunch {\n commit: string;\n ref?: string;\n flow: { file: string; key: string; name: string; version: number };\n mode: 'check' | 'real';\n inventory?: string;\n targets: string[];\n}\n\nexport interface RunnerRun {\n id: string;\n number: number;\n project: string;\n commit: string;\n flow: { file: string; key: string; name: string };\n mode: 'check' | 'real';\n state: 'queued' | 'running' | 'waiting' | 'succeeded' | 'failed' | 'cancelled' | 'lost';\n error?: string;\n endedAt?: string;\n /** AFD-282: a check run of someone's unpushed work. */\n dev?: true;\n}\n\nexport interface RunnerGate {\n node: { id: string; key: string; title: string };\n status: string;\n mayDecide: boolean;\n why?: string;\n}\n\nexport interface LaunchRequest {\n mode?: 'check' | 'real';\n /** AFD-282: a dev bundle's id: the server runs its commit, in check mode. */\n dev?: string;\n limit?: string;\n /** The flow's inputs as typed text, by name. */\n inputs?: Record<string, string>;\n}\n\n/** The REST API this client speaks. */\nexport const RUNNER_API = 'v1';\n\n/** Why this client and the server do not fit for a flow in `format`, or undefined. */\nexport function incompatible(meta: RunnerMeta, format: number): string | undefined {\n if (meta.name !== 'Ansible Flow Server') return 'The address is not an Ansible Flow Server';\n if (meta.api !== RUNNER_API)\n return `The server speaks API ${meta.api}; this extension speaks ${RUNNER_API}: update the one that is older`;\n const formats = meta.designer.formats ?? [meta.designer.format];\n if (!formats.includes(format))\n return `The flow is in format ${format}; the server (${meta.version}) reads formats ${formats.join(', ')}: upgrade the server`;\n return undefined;\n}\n\n/** AFD-284: what happened to which flows between two commits, and who did it. */\nexport interface RunnerChanges {\n from: string;\n to: string;\n flows: {\n file: string;\n change: 'added' | 'changed' | 'removed';\n authors: string[];\n at?: string;\n commit?: string;\n }[];\n}\n\n/** AFD-284: where the project's branch is (`commit`), and each time it moves (`moved`). */\nexport type ProjectEvent =\n | { type: 'commit'; commit: string | null }\n | { type: 'moved'; from?: string; to: string };\n\nexport interface RunnerOptions {\n verifyTls?: boolean;\n /** Send the token over plain http to another machine (a lab only). */\n allowHttp?: boolean;\n timeoutMs?: number;\n}\n\nconst MAX_BODY = 32 * 1024 * 1024;\nconst enc = encodeURIComponent;\n\nexport class RunnerClient {\n readonly base: string;\n private readonly origin: string;\n\n constructor(\n url: string,\n private readonly token: string,\n private readonly options: RunnerOptions = {},\n ) {\n this.base = url.replace(/\\/+$/, '');\n this.origin = new URL(this.base).origin;\n }\n\n /** A run's page on the server. */\n runUrl(id: string): string {\n return `${this.base}/runs/${enc(id)}`;\n }\n\n /** Refuses a URL the token must not go to: another origin, or plain http without consent. */\n private guard(url: URL): RunnerError | undefined {\n if (url.origin !== this.origin)\n return new RunnerError(`Not sending the token to ${url.origin}`);\n if (cleartextTo(url) && !this.options.allowHttp)\n return new RunnerError(\n `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`,\n );\n return undefined;\n }\n\n /**\n * AFD-284: follows a project's event stream (server-sent events) until `stop` is called, going\n * back after a dropped connection (5 s, then up to a minute between tries). `commit` comes at\n * once and after each reconnect, so a move missed while away is noticed.\n */\n follow(project: string, onEvent: (e: ProjectEvent) => void): () => void {\n let stopped = false;\n let req: http.ClientRequest | undefined;\n let retry: NodeJS.Timeout | undefined;\n let wait = 5_000;\n const url = new URL(`${this.base}/api/v1/projects/${enc(project)}/events`);\n const problem = this.guard(url);\n const again = () => {\n if (stopped) return;\n retry = setTimeout(connect, wait);\n wait = Math.min(wait * 2, 60_000);\n };\n const connect = () => {\n if (stopped || problem) return;\n const lib = url.protocol === 'https:' ? https : http;\n req = lib.request(\n url,\n {\n method: 'GET',\n headers: { Authorization: `Bearer ${this.token}`, Accept: 'text/event-stream' },\n ...(url.protocol === 'https:'\n ? { rejectUnauthorized: this.options.verifyTls !== false }\n : {}),\n },\n (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n // A token the server refuses, or a project gone: trying again changes nothing.\n if (res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404) return;\n again();\n return;\n }\n wait = 5_000;\n res.setEncoding('utf8');\n let buffer = '';\n res.on('data', (chunk: string) => {\n buffer += chunk;\n for (let at = buffer.indexOf('\\n\\n'); at >= 0; at = buffer.indexOf('\\n\\n')) {\n const block = buffer.slice(0, at);\n buffer = buffer.slice(at + 2);\n const type = /^event: (.*)$/m.exec(block)?.[1];\n const data = /^data: (.*)$/m.exec(block)?.[1];\n if (!data || (type !== 'commit' && type !== 'moved')) continue;\n try {\n onEvent({ type, ...JSON.parse(data) } as ProjectEvent);\n } catch {\n // A line it cannot read: the next event says where the branch is.\n }\n }\n });\n res.on('end', again);\n res.on('error', again);\n },\n );\n req.on('error', again);\n req.end();\n };\n connect();\n return () => {\n stopped = true;\n clearTimeout(retry);\n req?.destroy();\n };\n }\n\n /** The answer's body as text, and its status. */\n private send(\n method: string,\n path: string,\n body?: unknown,\n /** A body sent as it is, with its content type (a git bundle). */\n raw?: { bytes: Buffer; type: string },\n ): Promise<{ status: number; text: string }> {\n const url = new URL(`${this.base}${path}`);\n const refused = this.guard(url);\n if (refused) return Promise.reject(refused);\n const lib = url.protocol === 'https:' ? https : http;\n const data = raw?.bytes ?? (body === undefined ? undefined : JSON.stringify(body));\n const type = raw?.type ?? 'application/json';\n return new Promise((resolve, reject) => {\n const req = lib.request(\n url,\n {\n method,\n headers: {\n Authorization: `Bearer ${this.token}`,\n Accept: 'application/json',\n ...(data ? { 'Content-Type': type, 'Content-Length': Buffer.byteLength(data) } : {}),\n },\n timeout: this.options.timeoutMs ?? TIMEOUTS.runnerRequest,\n ...(url.protocol === 'https:'\n ? { rejectUnauthorized: this.options.verifyTls !== false }\n : {}),\n },\n (res) => {\n const chunks: Buffer[] = [];\n let size = 0;\n res.on('data', (c: Buffer) => {\n size += c.length;\n if (size > MAX_BODY) {\n req.destroy(new Error('the answer is too large'));\n return;\n }\n chunks.push(c);\n });\n res.on('end', () =>\n resolve({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString('utf8') }),\n );\n res.on('error', reject);\n },\n );\n req.on('timeout', () => req.destroy(new Error('no answer in time')));\n req.on('error', (e) =>\n reject(new RunnerError(`The server at ${this.base} does not answer: ${e.message}`)),\n );\n req.end(data);\n });\n }\n\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n raw?: { bytes: Buffer; type: string },\n ): Promise<T> {\n const { status, text } = await this.send(method, path, body, raw);\n let json: unknown;\n try {\n json = text ? JSON.parse(text) : undefined;\n } catch {\n json = undefined;\n }\n if (status >= 200 && status < 300) return json as T;\n const e = (json ?? {}) as {\n error?: string;\n message?: string;\n problems?: { field?: string; message: string }[];\n };\n const why =\n status === 401\n ? 'The server does not take the token: set it again (it may have expired or been revoked)'\n : status === 413\n ? 'The work is larger than the server takes (AFS_DEV_BUNDLE_MAX_MB)'\n : (e.message ?? `The server answered ${status}`);\n throw new RunnerError(why, status, e.error, e.problems);\n }\n\n /** The output of a request whose answer is plain text. */\n async text(path: string): Promise<string> {\n const { status, text } = await this.send('GET', path);\n if (status >= 200 && status < 300) return text;\n throw new RunnerError(`The server answered ${status}`, status);\n }\n\n meta = () => this.request<RunnerMeta>('GET', '/api/v1/meta');\n me = () => this.request<{ login: string; name: string }>('GET', '/api/v1/session');\n projects = (workspace: string) =>\n this.request<RunnerProject[]>('GET', `/api/v1/workspaces/${enc(workspace)}/projects`);\n project = (id: string) => this.request<RunnerProject>('GET', `/api/v1/projects/${enc(id)}`);\n configs = (project: string) =>\n this.request<RunnerConfig[]>('GET', `/api/v1/projects/${enc(project)}/launch-configs`);\n check = async (config: string, req: LaunchRequest) =>\n (\n await this.request<{ launch: RunnerLaunch }>(\n 'POST',\n `/api/v1/launch-configs/${enc(config)}/check`,\n req,\n )\n ).launch;\n launch = (config: string, req: LaunchRequest) =>\n this.request<RunnerRun>('POST', `/api/v1/launch-configs/${enc(config)}/launch`, req);\n run = (id: string) => this.request<RunnerRun>('GET', `/api/v1/runs/${enc(id)}`);\n /** How the run went, as the designer shows it (the server's RunView); 404 until it has one. */\n view = async (id: string) => {\n try {\n return await this.request<Record<string, unknown>>('GET', `/api/v1/runs/${enc(id)}/view`);\n } catch (e) {\n if (e instanceof RunnerError && e.status === 404) return undefined;\n throw e;\n }\n };\n gates = (id: string) => this.request<RunnerGate[]>('GET', `/api/v1/runs/${enc(id)}/approvals`);\n cancel = (id: string) => this.request<RunnerRun>('POST', `/api/v1/runs/${enc(id)}/cancel`);\n decide = (id: string, node: string, decision: 'approved' | 'rejected', comment?: string) =>\n this.request<unknown>('POST', `/api/v1/runs/${enc(id)}/approvals/${enc(node)}`, {\n decision,\n ...(comment?.trim() ? { comment: comment.trim() } : {}),\n });\n /** AFD-282: unpushed work for a dev run; resolves with the bundle's id and commit. */\n devBundle = (project: string, bytes: Buffer) =>\n this.request<{ id: string; commit: string; expiresAt: string }>(\n 'POST',\n `/api/v1/projects/${enc(project)}/dev-bundles`,\n undefined,\n { bytes, type: 'application/x-git-bundle' },\n );\n /** AFD-284: the flows changed between `from` and the project's commit (or `to`). */\n changes = (project: string, from: string, to?: string) =>\n this.request<RunnerChanges>(\n 'GET',\n `/api/v1/projects/${enc(project)}/changes?from=${enc(from)}${to ? `&to=${enc(to)}` : ''}`,\n );\n /** AFD-284: one flow as the server reads it at a commit (its document in `snapshot.doc`). */\n flowAt = (project: string, path: string, commit: string) =>\n this.request<{ commit: string; file: string; snapshot: { doc?: unknown } }>(\n 'GET',\n `/api/v1/projects/${enc(project)}/flows/view?path=${enc(path)}&commit=${enc(commit)}`,\n );\n /** What one host did in one step of the run. */\n hostOutput = (id: string, node: string, host: string) =>\n this.request<{ entries?: unknown[] }>(\n 'GET',\n `/api/v1/runs/${enc(id)}/hosts/${enc(node)}/${enc(host)}`,\n );\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansible-flow/node",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "Ansible Flow Designer for Node hosts: layout, the probe client, runs, the AWX client, the module catalog and SSH.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ChistokhinSV/ansible-flow-designer/blob/main/docs/packages.md",
@@ -26,7 +26,7 @@
26
26
  "node": ">=22.13"
27
27
  },
28
28
  "dependencies": {
29
- "@ansible-flow/core": "0.85.0",
29
+ "@ansible-flow/core": "0.87.0",
30
30
  "elkjs": "0.12.0",
31
31
  "jsonc-parser": "3.3.1",
32
32
  "yaml": "2.9.1"