@elizaos/app-core 2.0.0-alpha.209 → 2.0.0-alpha.210
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/package.json +5 -5
- package/packages/app-core/src/api/n8n-routes.d.ts.map +1 -1
- package/packages/app-core/src/api/n8n-routes.js +7 -1
- package/packages/app-core/src/services/n8n-sidecar.d.ts +50 -6
- package/packages/app-core/src/services/n8n-sidecar.d.ts.map +1 -1
- package/packages/app-core/src/services/n8n-sidecar.js +253 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elizaos/app-core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.210",
|
|
4
4
|
"description": "Shared application core for elizaOS white-label agent apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -477,7 +477,7 @@
|
|
|
477
477
|
"@capacitor/keyboard": "8.0.3",
|
|
478
478
|
"@capacitor/preferences": "^8.0.1",
|
|
479
479
|
"@clack/prompts": "^1.0.0",
|
|
480
|
-
"@elizaos/agent": "^2.0.0-alpha.
|
|
480
|
+
"@elizaos/agent": "^2.0.0-alpha.210",
|
|
481
481
|
"@elizaos/app-companion": "^0.0.0",
|
|
482
482
|
"@elizaos/app-elizamaker": "^0.0.0",
|
|
483
483
|
"@elizaos/app-lifeops": "^0.0.0",
|
|
@@ -486,9 +486,9 @@
|
|
|
486
486
|
"@elizaos/app-task-coordinator": "^0.0.0",
|
|
487
487
|
"@elizaos/app-training": "^0.0.1",
|
|
488
488
|
"@elizaos/app-vincent": "^0.0.0",
|
|
489
|
-
"@elizaos/core": "^2.0.0-alpha.
|
|
490
|
-
"@elizaos/shared": "^2.0.0-alpha.
|
|
491
|
-
"@elizaos/ui": "^2.0.0-alpha.
|
|
489
|
+
"@elizaos/core": "^2.0.0-alpha.210",
|
|
490
|
+
"@elizaos/shared": "^2.0.0-alpha.210",
|
|
491
|
+
"@elizaos/ui": "^2.0.0-alpha.210",
|
|
492
492
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
493
493
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
494
494
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"n8n-routes.d.ts","sourceRoot":"","sources":["../../../../../src/api/n8n-routes.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"n8n-routes.d.ts","sourceRoot":"","sources":["../../../../../src/api/n8n-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -201,9 +201,15 @@ function resolveProxyTarget(ctx, subpath, sidecar, native) {
|
|
|
201
201
|
};
|
|
202
202
|
if (apiKey)
|
|
203
203
|
headers["X-N8N-API-KEY"] = apiKey;
|
|
204
|
+
// n8n serves TWO parallel workflow APIs:
|
|
205
|
+
// /rest/workflows — internal UI endpoint, requires JWT cookie auth.
|
|
206
|
+
// /api/v1/workflows — public API, accepts X-N8N-API-KEY.
|
|
207
|
+
// We provision an X-N8N-API-KEY during boot, so the public API is the
|
|
208
|
+
// only path that authenticates correctly. Hitting /rest/ was returning
|
|
209
|
+
// 401 "Unauthorized" even with a valid key — that's the wrong endpoint.
|
|
204
210
|
return {
|
|
205
211
|
target: {
|
|
206
|
-
url: `${host.replace(/\/+$/, "")}/
|
|
212
|
+
url: `${host.replace(/\/+$/, "")}/api/v1/workflows${subpath}`,
|
|
207
213
|
headers,
|
|
208
214
|
},
|
|
209
215
|
};
|
|
@@ -62,7 +62,22 @@ export interface N8nSidecarConfig {
|
|
|
62
62
|
startPort?: number;
|
|
63
63
|
/** Bind host for the child. Default 127.0.0.1. */
|
|
64
64
|
host?: string;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Binary used to run n8n. Default "npx".
|
|
67
|
+
*
|
|
68
|
+
* Why not "bunx"? bunx invokes n8n through Bun's runtime / shim, which
|
|
69
|
+
* fails two different ways depending on the n8n version:
|
|
70
|
+
* 1. n8n@1.70.x: bunx resolves `node` via $PATH before reading the
|
|
71
|
+
* package's `engines` field. On macOS with Homebrew that's v24,
|
|
72
|
+
* which 1.70 rejects ("Node.js version 24.5.0 is currently not
|
|
73
|
+
* supported").
|
|
74
|
+
* 2. n8n@1.108.x: bunx runs n8n under Bun's runtime which does not
|
|
75
|
+
* fully implement the `reflect-metadata` + TSyringe decorator
|
|
76
|
+
* pattern n8n relies on for DI, failing at bootstrap with
|
|
77
|
+
* "[DI] ErrorReporter is not decorated with Service".
|
|
78
|
+
* `npx --yes` uses npm's cache and execs n8n under the expected Node
|
|
79
|
+
* runtime, which works across every n8n + Node combo we care about.
|
|
80
|
+
*/
|
|
66
81
|
binary?: string;
|
|
67
82
|
/** State directory root; owner email/password + sqlite live here. */
|
|
68
83
|
stateDir?: string;
|
|
@@ -221,13 +236,42 @@ export declare class N8nSidecar {
|
|
|
221
236
|
*/
|
|
222
237
|
private validateApiKey;
|
|
223
238
|
/**
|
|
224
|
-
* Provision
|
|
225
|
-
*
|
|
226
|
-
* `/rest/me/api-keys
|
|
227
|
-
*
|
|
228
|
-
*
|
|
239
|
+
* Provision an API key by driving n8n's owner-setup → login → api-key flow.
|
|
240
|
+
*
|
|
241
|
+
* n8n ≥ 1.90-ish removed the anonymous `/rest/me/api-keys` endpoint. The
|
|
242
|
+
* supported path now requires:
|
|
243
|
+
* 1. POST /rest/owner/setup { email, firstName, lastName, password }
|
|
244
|
+
* – returns `Set-Cookie: n8n-auth=<JWT>` when no owner exists yet.
|
|
245
|
+
* 2. POST /rest/login { emailOrLdapLoginId, password }
|
|
246
|
+
* – returns the same cookie on restarts, once the owner is set.
|
|
247
|
+
* 3. GET /rest/api-keys/scopes
|
|
248
|
+
* – enumerates the scopes the current role is allowed to grant.
|
|
249
|
+
* 4. POST /rest/api-keys { label, scopes, expiresAt: null }
|
|
250
|
+
* – returns `data.rawApiKey` which stays valid across restarts until
|
|
251
|
+
* explicitly revoked.
|
|
252
|
+
*
|
|
253
|
+
* Credentials are persisted to `{stateDir}/owner.json` (mode-600) so the
|
|
254
|
+
* same login works on every subsequent boot; we never re-generate. Password
|
|
255
|
+
* is random per install — there's no user-facing n8n UI flow in Milady, so
|
|
256
|
+
* storing it here is safe for a local single-user sidecar.
|
|
229
257
|
*/
|
|
230
258
|
private provisionApiKey;
|
|
259
|
+
/**
|
|
260
|
+
* Load owner credentials from `{stateDir}/owner.json`, or generate a fresh
|
|
261
|
+
* pair and persist them mode-600. The email is deterministic (matches the
|
|
262
|
+
* label we show to the user); the password is a long random token.
|
|
263
|
+
*/
|
|
264
|
+
private loadOrCreateOwnerCreds;
|
|
265
|
+
/**
|
|
266
|
+
* Returns a `Cookie: n8n-auth=<jwt>` string by either creating the owner
|
|
267
|
+
* (first boot) or logging in (subsequent boots). Returns null if both
|
|
268
|
+
* fail so the caller can back off gracefully.
|
|
269
|
+
*/
|
|
270
|
+
private acquireOwnerCookie;
|
|
271
|
+
/** List scopes the current role may grant when creating an API key. */
|
|
272
|
+
private fetchApiKeyScopes;
|
|
273
|
+
/** 48 bytes of base64url entropy — ~64 chars, far above n8n's min length. */
|
|
274
|
+
private generateRandomPassword;
|
|
231
275
|
/** Stop the sidecar. Idempotent. */
|
|
232
276
|
stop(): Promise<void>;
|
|
233
277
|
/** Public helper so callers can gate feature activation on running state. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"n8n-sidecar.d.ts","sourceRoot":"","sources":["../../../../../src/services/n8n-sidecar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAqB,KAAK,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAY3E,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1E,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,gBAAgB,CAAC;IACzB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd
|
|
1
|
+
{"version":3,"file":"n8n-sidecar.d.ts","sourceRoot":"","sources":["../../../../../src/services/n8n-sidecar.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,21 +42,33 @@ import os from "node:os";
|
|
|
42
42
|
import path from "node:path";
|
|
43
43
|
import { logger } from "@elizaos/core";
|
|
44
44
|
// ── Implementation ───────────────────────────────────────────────────────────
|
|
45
|
-
// n8n
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
45
|
+
// n8n version selection — this range is narrower than it looks.
|
|
46
|
+
//
|
|
47
|
+
// 1.70.x declares `engines.node: ">=18.17 <= 22"` — rejects Node 23/24
|
|
48
|
+
// (which is what Homebrew ships). Fails at boot with
|
|
49
|
+
// "Node.js version 24.5.0 is currently not supported".
|
|
50
|
+
// 1.99.0 widens to ">=20.19 <= 24.x" and boots cleanly under Node 24 ✓
|
|
51
|
+
// 1.100.0 same engines range, boots cleanly ✓
|
|
52
|
+
// 1.108.0 has a DI-container bootstrap regression — throws
|
|
53
|
+
// "[DI] GlobalConfig is not decorated with Service" under every
|
|
54
|
+
// launcher (npx, npm install + node, bunx). Confirmed broken on
|
|
55
|
+
// both Node 22 and Node 24. See upstream n8n issue.
|
|
56
|
+
//
|
|
57
|
+
// 1.100.0 is the last version I verified end-to-end against Node 24 +
|
|
58
|
+
// npx --yes on 2026-04-19. Bump only with a re-run of the smoke test in
|
|
59
|
+
// docs/apps/n8n-sidecar.md. Validate `engines.node` via
|
|
60
|
+
// `npm view n8n@<v> engines`.
|
|
61
|
+
const DEFAULT_N8N_VERSION = "1.100.0";
|
|
52
62
|
const DEFAULT_START_PORT = 5678;
|
|
53
63
|
const DEFAULT_HOST = "127.0.0.1";
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
64
|
+
// Spawn n8n via `npx --yes n8n@<pinned>`. See `binary?` docs for why bunx
|
|
65
|
+
// is broken (Node-version mismatch + Bun runtime breaks tsyringe decorators).
|
|
66
|
+
const DEFAULT_BINARY = "npx";
|
|
67
|
+
// First-run `npx n8n@<pinned>` downloads ~300MB of n8n + nodes into the
|
|
68
|
+
// npm cache before boot. On a typical home connection that's 60–120s of
|
|
69
|
+
// download plus 15–30s of n8n boot. Keep 180s so the first-run path
|
|
70
|
+
// reliably lands inside the probe window on every desktop platform.
|
|
71
|
+
// Warm starts hit the npm cache and finish well under this budget.
|
|
60
72
|
const DEFAULT_PROBE_TIMEOUT_MS = 180_000;
|
|
61
73
|
const DEFAULT_PROBE_INTERVAL_MS = 750;
|
|
62
74
|
const DEFAULT_MAX_RETRIES = 3;
|
|
@@ -202,6 +214,28 @@ function fingerprint(secret) {
|
|
|
202
214
|
return "***";
|
|
203
215
|
return `${secret.slice(0, 4)}…${secret.slice(-2)} (len=${secret.length})`;
|
|
204
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Extract the n8n-auth cookie from a `Response` for re-use on subsequent
|
|
219
|
+
* calls. Returns a ready-to-send `Cookie:` header value, or null if the
|
|
220
|
+
* response didn't set one. Tolerates fetch implementations that expose
|
|
221
|
+
* multiple Set-Cookie values through `getSetCookie()` (Node 20.18+) or
|
|
222
|
+
* a single joined `set-cookie` header (older runtimes and the test fetch
|
|
223
|
+
* mock we use in unit tests).
|
|
224
|
+
*/
|
|
225
|
+
function extractAuthCookie(res) {
|
|
226
|
+
const headers = res.headers;
|
|
227
|
+
const list = typeof headers.getSetCookie === "function"
|
|
228
|
+
? headers.getSetCookie()
|
|
229
|
+
: ((headers.get("set-cookie") ?? "")
|
|
230
|
+
.split(/,(?=\s*[\w-]+=)/)
|
|
231
|
+
.filter((s) => s.length > 0));
|
|
232
|
+
for (const raw of list) {
|
|
233
|
+
const first = raw.split(";")[0]?.trim();
|
|
234
|
+
if (first?.startsWith("n8n-auth="))
|
|
235
|
+
return first;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
205
239
|
function resolveConfig(config) {
|
|
206
240
|
return {
|
|
207
241
|
enabled: config.enabled ?? true,
|
|
@@ -230,7 +264,11 @@ export class N8nSidecar {
|
|
|
230
264
|
retries: 0,
|
|
231
265
|
recentOutput: [],
|
|
232
266
|
};
|
|
233
|
-
|
|
267
|
+
// 40 was too small — n8n's migration output alone is ~60 stdout lines on a
|
|
268
|
+
// cold boot, which evicts every preceding error/log from the buffer before
|
|
269
|
+
// the UI can read it. Sized to comfortably hold migrations + the api-key
|
|
270
|
+
// provisioning trace + a reasonable error tail.
|
|
271
|
+
static RECENT_OUTPUT_CAP = 200;
|
|
234
272
|
/** Ring buffer of the child's recent stdout/stderr lines (see state.recentOutput). */
|
|
235
273
|
recentOutput = [];
|
|
236
274
|
child = null;
|
|
@@ -478,8 +516,10 @@ export class N8nSidecar {
|
|
|
478
516
|
async spawnChild(port) {
|
|
479
517
|
// n8n reads N8N_USER_MANAGEMENT_DISABLED to skip the owner-setup flow
|
|
480
518
|
// on first boot; we pair it with a random owner email so no real user
|
|
481
|
-
// data is needed. Using `
|
|
482
|
-
//
|
|
519
|
+
// data is needed. Using `npx n8n@<pinned>` pulls from the shared npm
|
|
520
|
+
// cache and runs under the native Node runtime, which n8n's DI stack
|
|
521
|
+
// depends on (bunx breaks both the Node version check for 1.70 and the
|
|
522
|
+
// tsyringe decorator handling for 1.100+ — see `binary?` docs above).
|
|
483
523
|
const env = {
|
|
484
524
|
...process.env,
|
|
485
525
|
N8N_PORT: String(port),
|
|
@@ -495,7 +535,17 @@ export class N8nSidecar {
|
|
|
495
535
|
DB_SQLITE_DATABASE: path.join(this.config.stateDir, "database.sqlite"),
|
|
496
536
|
};
|
|
497
537
|
const versioned = `n8n@${this.config.version}`;
|
|
498
|
-
|
|
538
|
+
// Arg style depends on the launcher:
|
|
539
|
+
// npx: --yes <pkg> <args...> (auto-confirm install prompt)
|
|
540
|
+
// bunx: -- <pkg> <args...> (still supported for manual overrides)
|
|
541
|
+
// Anything else is passed through as <pkg> <args...>.
|
|
542
|
+
const binaryBase = this.config.binary.split("/").pop() ?? this.config.binary;
|
|
543
|
+
const launcherArgs = binaryBase === "npx"
|
|
544
|
+
? ["--yes", versioned, "start"]
|
|
545
|
+
: binaryBase === "bunx"
|
|
546
|
+
? ["--", versioned, "start"]
|
|
547
|
+
: [versioned, "start"];
|
|
548
|
+
const child = this.deps.spawn(this.config.binary, launcherArgs, {
|
|
499
549
|
env,
|
|
500
550
|
stdio: ["ignore", "pipe", "pipe"],
|
|
501
551
|
detached: false,
|
|
@@ -721,34 +771,209 @@ export class N8nSidecar {
|
|
|
721
771
|
}
|
|
722
772
|
}
|
|
723
773
|
/**
|
|
724
|
-
* Provision
|
|
725
|
-
*
|
|
726
|
-
* `/rest/me/api-keys
|
|
727
|
-
*
|
|
728
|
-
*
|
|
774
|
+
* Provision an API key by driving n8n's owner-setup → login → api-key flow.
|
|
775
|
+
*
|
|
776
|
+
* n8n ≥ 1.90-ish removed the anonymous `/rest/me/api-keys` endpoint. The
|
|
777
|
+
* supported path now requires:
|
|
778
|
+
* 1. POST /rest/owner/setup { email, firstName, lastName, password }
|
|
779
|
+
* – returns `Set-Cookie: n8n-auth=<JWT>` when no owner exists yet.
|
|
780
|
+
* 2. POST /rest/login { emailOrLdapLoginId, password }
|
|
781
|
+
* – returns the same cookie on restarts, once the owner is set.
|
|
782
|
+
* 3. GET /rest/api-keys/scopes
|
|
783
|
+
* – enumerates the scopes the current role is allowed to grant.
|
|
784
|
+
* 4. POST /rest/api-keys { label, scopes, expiresAt: null }
|
|
785
|
+
* – returns `data.rawApiKey` which stays valid across restarts until
|
|
786
|
+
* explicitly revoked.
|
|
787
|
+
*
|
|
788
|
+
* Credentials are persisted to `{stateDir}/owner.json` (mode-600) so the
|
|
789
|
+
* same login works on every subsequent boot; we never re-generate. Password
|
|
790
|
+
* is random per install — there's no user-facing n8n UI flow in Milady, so
|
|
791
|
+
* storing it here is safe for a local single-user sidecar.
|
|
729
792
|
*/
|
|
730
793
|
async provisionApiKey(host) {
|
|
794
|
+
const log = (msg) => {
|
|
795
|
+
// Route through both the central logger and the sidecar's recentOutput
|
|
796
|
+
// ring so operators can see the failure in /api/n8n/status even if the
|
|
797
|
+
// dev-server log rotates or gets lost. The strings never contain the
|
|
798
|
+
// raw API key (that's fingerprinted at the ensureApiKey callsite).
|
|
799
|
+
logger.warn(`[n8n-sidecar] ${msg}`);
|
|
800
|
+
this.recordOutput(`[provisionApiKey] ${msg}`);
|
|
801
|
+
};
|
|
731
802
|
try {
|
|
732
|
-
const
|
|
803
|
+
const owner = await this.loadOrCreateOwnerCreds();
|
|
804
|
+
const cookie = await this.acquireOwnerCookie(host, owner, log);
|
|
805
|
+
if (!cookie) {
|
|
806
|
+
log("acquireOwnerCookie returned null — cannot create api key");
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
const scopes = await this.fetchApiKeyScopes(host, cookie);
|
|
810
|
+
if (!scopes || scopes.length === 0) {
|
|
811
|
+
log("/rest/api-keys/scopes returned no scopes");
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
const res = await this.deps.fetch(`${host}/rest/api-keys`, {
|
|
733
815
|
method: "POST",
|
|
734
|
-
headers: {
|
|
735
|
-
|
|
816
|
+
headers: {
|
|
817
|
+
"content-type": "application/json",
|
|
818
|
+
cookie,
|
|
819
|
+
},
|
|
820
|
+
body: JSON.stringify({
|
|
821
|
+
label: "milady-sidecar",
|
|
822
|
+
scopes,
|
|
823
|
+
expiresAt: null,
|
|
824
|
+
}),
|
|
736
825
|
signal: AbortSignal.timeout(5_000),
|
|
737
826
|
});
|
|
738
827
|
if (!res.ok) {
|
|
739
|
-
|
|
828
|
+
const bodyText = await res.text().catch(() => "");
|
|
829
|
+
log(`api-key create failed: ${res.status} ${res.statusText}${bodyText ? ` — ${bodyText.slice(0, 200)}` : ""}`);
|
|
740
830
|
return null;
|
|
741
831
|
}
|
|
742
832
|
const body = (await res.json());
|
|
743
|
-
|
|
833
|
+
const key = body.data?.rawApiKey ??
|
|
744
834
|
body.data?.apiKey ??
|
|
745
835
|
body.rawApiKey ??
|
|
746
836
|
body.apiKey ??
|
|
747
|
-
null
|
|
837
|
+
null;
|
|
838
|
+
if (!key) {
|
|
839
|
+
log("api-key create returned no rawApiKey in body");
|
|
840
|
+
}
|
|
841
|
+
return key;
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
log(`provisionApiKey threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Load owner credentials from `{stateDir}/owner.json`, or generate a fresh
|
|
850
|
+
* pair and persist them mode-600. The email is deterministic (matches the
|
|
851
|
+
* label we show to the user); the password is a long random token.
|
|
852
|
+
*/
|
|
853
|
+
async loadOrCreateOwnerCreds() {
|
|
854
|
+
const ownerPath = path.join(this.config.stateDir, "owner.json");
|
|
855
|
+
try {
|
|
856
|
+
const raw = await fs.readFile(ownerPath, "utf-8");
|
|
857
|
+
const parsed = JSON.parse(raw);
|
|
858
|
+
if (typeof parsed.email === "string" &&
|
|
859
|
+
typeof parsed.password === "string" &&
|
|
860
|
+
parsed.email.length > 0 &&
|
|
861
|
+
parsed.password.length > 0) {
|
|
862
|
+
return {
|
|
863
|
+
email: parsed.email,
|
|
864
|
+
firstName: typeof parsed.firstName === "string" ? parsed.firstName : "Milady",
|
|
865
|
+
lastName: typeof parsed.lastName === "string" ? parsed.lastName : "Local",
|
|
866
|
+
password: parsed.password,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
748
869
|
}
|
|
749
870
|
catch {
|
|
871
|
+
/* fall through to generate fresh */
|
|
872
|
+
}
|
|
873
|
+
const password = this.generateRandomPassword();
|
|
874
|
+
const creds = {
|
|
875
|
+
email: "milady@milady.local",
|
|
876
|
+
firstName: "Milady",
|
|
877
|
+
lastName: "Local",
|
|
878
|
+
password,
|
|
879
|
+
};
|
|
880
|
+
try {
|
|
881
|
+
await fs.mkdir(this.config.stateDir, { recursive: true });
|
|
882
|
+
await fs.writeFile(ownerPath, JSON.stringify(creds, null, 2), {
|
|
883
|
+
mode: 0o600,
|
|
884
|
+
});
|
|
885
|
+
await fs.chmod(ownerPath, 0o600).catch(() => undefined);
|
|
886
|
+
}
|
|
887
|
+
catch (err) {
|
|
888
|
+
logger.warn(`[n8n-sidecar] failed to persist owner creds: ${err instanceof Error ? err.message : String(err)}`);
|
|
889
|
+
}
|
|
890
|
+
return creds;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Returns a `Cookie: n8n-auth=<jwt>` string by either creating the owner
|
|
894
|
+
* (first boot) or logging in (subsequent boots). Returns null if both
|
|
895
|
+
* fail so the caller can back off gracefully.
|
|
896
|
+
*/
|
|
897
|
+
async acquireOwnerCookie(host, owner, log = () => undefined) {
|
|
898
|
+
// First boot: owner does not exist yet, /rest/owner/setup returns 200.
|
|
899
|
+
// Subsequent boots: returns 400 "instance owner already setup"; we fall
|
|
900
|
+
// through to /rest/login.
|
|
901
|
+
const setup = await this.deps
|
|
902
|
+
.fetch(`${host}/rest/owner/setup`, {
|
|
903
|
+
method: "POST",
|
|
904
|
+
headers: { "content-type": "application/json" },
|
|
905
|
+
body: JSON.stringify({
|
|
906
|
+
email: owner.email,
|
|
907
|
+
firstName: owner.firstName,
|
|
908
|
+
lastName: owner.lastName,
|
|
909
|
+
password: owner.password,
|
|
910
|
+
}),
|
|
911
|
+
signal: AbortSignal.timeout(10_000),
|
|
912
|
+
})
|
|
913
|
+
.catch((err) => {
|
|
914
|
+
log(`owner/setup fetch threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
750
915
|
return null;
|
|
916
|
+
});
|
|
917
|
+
if (setup?.ok) {
|
|
918
|
+
const cookie = extractAuthCookie(setup);
|
|
919
|
+
if (cookie)
|
|
920
|
+
return cookie;
|
|
921
|
+
log("owner/setup 200 but no n8n-auth cookie in response");
|
|
922
|
+
}
|
|
923
|
+
else if (setup) {
|
|
924
|
+
const text = await setup.text().catch(() => "");
|
|
925
|
+
log(`owner/setup ${setup.status}${text ? ` — ${text.slice(0, 160)}` : ""}`);
|
|
926
|
+
}
|
|
927
|
+
const login = await this.deps
|
|
928
|
+
.fetch(`${host}/rest/login`, {
|
|
929
|
+
method: "POST",
|
|
930
|
+
headers: { "content-type": "application/json" },
|
|
931
|
+
body: JSON.stringify({
|
|
932
|
+
emailOrLdapLoginId: owner.email,
|
|
933
|
+
password: owner.password,
|
|
934
|
+
}),
|
|
935
|
+
signal: AbortSignal.timeout(10_000),
|
|
936
|
+
})
|
|
937
|
+
.catch((err) => {
|
|
938
|
+
log(`login fetch threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
939
|
+
return null;
|
|
940
|
+
});
|
|
941
|
+
if (login?.ok) {
|
|
942
|
+
const cookie = extractAuthCookie(login);
|
|
943
|
+
if (cookie)
|
|
944
|
+
return cookie;
|
|
945
|
+
log("login 200 but no n8n-auth cookie in response");
|
|
946
|
+
}
|
|
947
|
+
else if (login) {
|
|
948
|
+
const text = await login.text().catch(() => "");
|
|
949
|
+
log(`login ${login.status}${text ? ` — ${text.slice(0, 160)}` : ""}`);
|
|
751
950
|
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
/** List scopes the current role may grant when creating an API key. */
|
|
954
|
+
async fetchApiKeyScopes(host, cookie) {
|
|
955
|
+
try {
|
|
956
|
+
const res = await this.deps.fetch(`${host}/rest/api-keys/scopes`, {
|
|
957
|
+
method: "GET",
|
|
958
|
+
headers: { cookie },
|
|
959
|
+
signal: AbortSignal.timeout(5_000),
|
|
960
|
+
});
|
|
961
|
+
if (!res.ok)
|
|
962
|
+
return null;
|
|
963
|
+
const body = (await res.json());
|
|
964
|
+
return Array.isArray(body.data) ? body.data : null;
|
|
965
|
+
}
|
|
966
|
+
catch {
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/** 48 bytes of base64url entropy — ~64 chars, far above n8n's min length. */
|
|
971
|
+
generateRandomPassword() {
|
|
972
|
+
// crypto is Node 20+ global; fallback to Math.random if unavailable is
|
|
973
|
+
// intentionally not provided — insecure passwords are worse than a crash.
|
|
974
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
975
|
+
const nodeCrypto = require("node:crypto");
|
|
976
|
+
return nodeCrypto.randomBytes(48).toString("base64url");
|
|
752
977
|
}
|
|
753
978
|
/** Stop the sidecar. Idempotent. */
|
|
754
979
|
async stop() {
|