@themoltnet/node-red-contrib-core 0.12.3 → 0.12.4
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/dist/nodes/src.js +166 -76
- package/package.json +2 -2
package/dist/nodes/src.js
CHANGED
|
@@ -4,6 +4,59 @@ import crypto, { createHash as createHash$1 } from "crypto";
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
+
//#region ../sdk/src/errors.ts
|
|
8
|
+
var MoltNetError = class extends Error {
|
|
9
|
+
code;
|
|
10
|
+
statusCode;
|
|
11
|
+
detail;
|
|
12
|
+
/**
|
|
13
|
+
* Populated when the server returned a `VALIDATION_FAILED` problem
|
|
14
|
+
* (status 400) with field-level errors. Empty / undefined for every
|
|
15
|
+
* other problem kind. Proposer scripts surface these to operators so
|
|
16
|
+
* they don't have to re-run with curl to see what was rejected.
|
|
17
|
+
*/
|
|
18
|
+
validationErrors;
|
|
19
|
+
constructor(message, options) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "MoltNetError";
|
|
22
|
+
this.code = options.code;
|
|
23
|
+
this.statusCode = options.statusCode;
|
|
24
|
+
this.detail = options.detail;
|
|
25
|
+
this.validationErrors = options.validationErrors;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var NetworkError = class extends MoltNetError {
|
|
29
|
+
constructor(message, options) {
|
|
30
|
+
super(message, {
|
|
31
|
+
code: "NETWORK_ERROR",
|
|
32
|
+
detail: options?.detail
|
|
33
|
+
});
|
|
34
|
+
this.name = "NetworkError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var AuthenticationError = class extends MoltNetError {
|
|
38
|
+
constructor(message, options) {
|
|
39
|
+
super(message, {
|
|
40
|
+
code: "AUTH_FAILED",
|
|
41
|
+
statusCode: options?.statusCode,
|
|
42
|
+
detail: options?.detail
|
|
43
|
+
});
|
|
44
|
+
this.name = "AuthenticationError";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function problemToError(problem, statusCode) {
|
|
48
|
+
const title = problem.title ?? "Request failed";
|
|
49
|
+
const message = problem.detail ? `${title}: ${problem.detail}` : title;
|
|
50
|
+
const rawErrors = problem.errors;
|
|
51
|
+
const validationErrors = Array.isArray(rawErrors) ? rawErrors.filter((e) => typeof e === "object" && e !== null && typeof e.field === "string" && typeof e.message === "string") : void 0;
|
|
52
|
+
return new MoltNetError(message, {
|
|
53
|
+
code: problem.type ?? problem.code ?? "UNKNOWN",
|
|
54
|
+
statusCode,
|
|
55
|
+
detail: problem.detail,
|
|
56
|
+
validationErrors
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
7
60
|
//#region ../api-client/src/generated/core/bodySerializer.gen.ts
|
|
8
61
|
var jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
9
62
|
Object.entries({
|
|
@@ -716,7 +769,7 @@ var rotateAgentKey = (options) => (options.client ?? client).post({
|
|
|
716
769
|
...options
|
|
717
770
|
});
|
|
718
771
|
/**
|
|
719
|
-
* Get the authenticated
|
|
772
|
+
* Get the authenticated caller identity and context. Works for both agents (identity plus, under agent-key auth, the credential binding) and humans, via bearer, session, or cookie auth.
|
|
720
773
|
*/
|
|
721
774
|
var getWhoami = (options) => (options?.client ?? client).get({
|
|
722
775
|
security: [
|
|
@@ -2896,63 +2949,11 @@ function createRateLimitFetch(options) {
|
|
|
2896
2949
|
});
|
|
2897
2950
|
}
|
|
2898
2951
|
//#endregion
|
|
2899
|
-
//#region ../sdk/src/errors.ts
|
|
2900
|
-
var MoltNetError = class extends Error {
|
|
2901
|
-
code;
|
|
2902
|
-
statusCode;
|
|
2903
|
-
detail;
|
|
2904
|
-
/**
|
|
2905
|
-
* Populated when the server returned a `VALIDATION_FAILED` problem
|
|
2906
|
-
* (status 400) with field-level errors. Empty / undefined for every
|
|
2907
|
-
* other problem kind. Proposer scripts surface these to operators so
|
|
2908
|
-
* they don't have to re-run with curl to see what was rejected.
|
|
2909
|
-
*/
|
|
2910
|
-
validationErrors;
|
|
2911
|
-
constructor(message, options) {
|
|
2912
|
-
super(message);
|
|
2913
|
-
this.name = "MoltNetError";
|
|
2914
|
-
this.code = options.code;
|
|
2915
|
-
this.statusCode = options.statusCode;
|
|
2916
|
-
this.detail = options.detail;
|
|
2917
|
-
this.validationErrors = options.validationErrors;
|
|
2918
|
-
}
|
|
2919
|
-
};
|
|
2920
|
-
var NetworkError = class extends MoltNetError {
|
|
2921
|
-
constructor(message, options) {
|
|
2922
|
-
super(message, {
|
|
2923
|
-
code: "NETWORK_ERROR",
|
|
2924
|
-
detail: options?.detail
|
|
2925
|
-
});
|
|
2926
|
-
this.name = "NetworkError";
|
|
2927
|
-
}
|
|
2928
|
-
};
|
|
2929
|
-
var AuthenticationError = class extends MoltNetError {
|
|
2930
|
-
constructor(message, options) {
|
|
2931
|
-
super(message, {
|
|
2932
|
-
code: "AUTH_FAILED",
|
|
2933
|
-
statusCode: options?.statusCode,
|
|
2934
|
-
detail: options?.detail
|
|
2935
|
-
});
|
|
2936
|
-
this.name = "AuthenticationError";
|
|
2937
|
-
}
|
|
2938
|
-
};
|
|
2939
|
-
function problemToError(problem, statusCode) {
|
|
2940
|
-
const title = problem.title ?? "Request failed";
|
|
2941
|
-
const message = problem.detail ? `${title}: ${problem.detail}` : title;
|
|
2942
|
-
const rawErrors = problem.errors;
|
|
2943
|
-
const validationErrors = Array.isArray(rawErrors) ? rawErrors.filter((e) => typeof e === "object" && e !== null && typeof e.field === "string" && typeof e.message === "string") : void 0;
|
|
2944
|
-
return new MoltNetError(message, {
|
|
2945
|
-
code: problem.type ?? problem.code ?? "UNKNOWN",
|
|
2946
|
-
statusCode,
|
|
2947
|
-
detail: problem.detail,
|
|
2948
|
-
validationErrors
|
|
2949
|
-
});
|
|
2950
|
-
}
|
|
2951
|
-
//#endregion
|
|
2952
2952
|
//#region ../sdk/src/agent-context.ts
|
|
2953
2953
|
function unwrapResult(result) {
|
|
2954
2954
|
if (result.error !== void 0 && result.error !== null) {
|
|
2955
2955
|
const error = result.error;
|
|
2956
|
+
if (error instanceof MoltNetError) throw error;
|
|
2956
2957
|
if (isProblemDetails(error)) throw problemToError(error, error.status);
|
|
2957
2958
|
if (error instanceof Error && result.response === void 0) {
|
|
2958
2959
|
const networkError = new NetworkError(error.message, { detail: error.cause ? stringifyUnknown(error.cause) : void 0 });
|
|
@@ -3075,16 +3076,25 @@ function createAgentKeysNamespace(context) {
|
|
|
3075
3076
|
};
|
|
3076
3077
|
}
|
|
3077
3078
|
//#endregion
|
|
3079
|
+
//#region ../sdk/src/namespaces/whoami.ts
|
|
3080
|
+
/**
|
|
3081
|
+
* Build a `whoami()` accessor bound to an authenticated context. Returns the
|
|
3082
|
+
* caller's identity and context: `subjectType`, `currentTeamId`, and, for an
|
|
3083
|
+
* agent authenticated via an agent key, its `credentialBinding`.
|
|
3084
|
+
*/
|
|
3085
|
+
function createWhoami(context) {
|
|
3086
|
+
const { client, auth } = context;
|
|
3087
|
+
return async () => unwrapResult(await getWhoami({
|
|
3088
|
+
client,
|
|
3089
|
+
auth
|
|
3090
|
+
}));
|
|
3091
|
+
}
|
|
3092
|
+
//#endregion
|
|
3078
3093
|
//#region ../sdk/src/namespaces/agents.ts
|
|
3079
3094
|
function createAgentsNamespace(context) {
|
|
3080
|
-
const { client
|
|
3095
|
+
const { client } = context;
|
|
3081
3096
|
return {
|
|
3082
|
-
|
|
3083
|
-
return unwrapResult(await getWhoami({
|
|
3084
|
-
client,
|
|
3085
|
-
auth
|
|
3086
|
-
}));
|
|
3087
|
-
},
|
|
3097
|
+
whoami: createWhoami(context),
|
|
3088
3098
|
async lookup(fingerprint) {
|
|
3089
3099
|
return unwrapResult(await getAgentProfile({
|
|
3090
3100
|
client,
|
|
@@ -15677,22 +15687,37 @@ function createAgent(options) {
|
|
|
15677
15687
|
runtimeSlots: createRuntimeSlotsNamespace(context),
|
|
15678
15688
|
runtimeSessions: createRuntimeSessionsNamespace(context),
|
|
15679
15689
|
client,
|
|
15680
|
-
getToken: () =>
|
|
15690
|
+
getToken: () => {
|
|
15691
|
+
if (tokenManager) return tokenManager.getToken();
|
|
15692
|
+
if (auth) return auth();
|
|
15693
|
+
return Promise.reject(new MoltNetError("No token source configured", { code: "NO_TOKEN_SOURCE" }));
|
|
15694
|
+
}
|
|
15681
15695
|
};
|
|
15682
15696
|
}
|
|
15683
15697
|
//#endregion
|
|
15684
15698
|
//#region ../sdk/src/config.ts
|
|
15685
15699
|
/**
|
|
15686
15700
|
* Read MoltNet credentials from environment variables.
|
|
15687
|
-
* Reads MOLTNET_CLIENT_ID, MOLTNET_CLIENT_SECRET, and
|
|
15701
|
+
* Reads MOLTNET_CLIENT_ID, MOLTNET_CLIENT_SECRET, MOLTNET_API_URL, and
|
|
15702
|
+
* MOLTNET_AGENT_KEY.
|
|
15688
15703
|
*/
|
|
15689
15704
|
function readEnvCredentials() {
|
|
15690
15705
|
return {
|
|
15691
15706
|
clientId: process.env.MOLTNET_CLIENT_ID,
|
|
15692
15707
|
clientSecret: process.env.MOLTNET_CLIENT_SECRET,
|
|
15693
|
-
apiUrl: process.env.MOLTNET_API_URL
|
|
15708
|
+
apiUrl: process.env.MOLTNET_API_URL,
|
|
15709
|
+
agentKey: process.env.MOLTNET_AGENT_KEY
|
|
15694
15710
|
};
|
|
15695
15711
|
}
|
|
15712
|
+
/**
|
|
15713
|
+
* Resolve the API base URL from an ordered list of candidates, falling back to
|
|
15714
|
+
* the hosted default, with any trailing slash stripped. Candidates are tried in
|
|
15715
|
+
* order — pass them highest-precedence first (typically explicit option, then
|
|
15716
|
+
* env, then config file) so every caller shares one precedence rule.
|
|
15717
|
+
*/
|
|
15718
|
+
function normalizeApiUrl(...candidates) {
|
|
15719
|
+
return (candidates.find((c) => c) ?? "https://api.themolt.net").replace(/\/$/, "");
|
|
15720
|
+
}
|
|
15696
15721
|
//#endregion
|
|
15697
15722
|
//#region ../sdk/src/credentials.ts
|
|
15698
15723
|
function getConfigDir() {
|
|
@@ -15756,6 +15781,32 @@ function createRetryFetch(tokenManager, options) {
|
|
|
15756
15781
|
return doFetch(init);
|
|
15757
15782
|
};
|
|
15758
15783
|
}
|
|
15784
|
+
/**
|
|
15785
|
+
* Create a fetch wrapper for agent-key (static-bearer) authentication.
|
|
15786
|
+
*
|
|
15787
|
+
* A static key cannot be refreshed, so there is no token-invalidation/replay
|
|
15788
|
+
* (that half of {@link createRetryFetch} is intentionally omitted). What remains
|
|
15789
|
+
* is orthogonal to token refresh and still matters for a long-running client:
|
|
15790
|
+
*
|
|
15791
|
+
* - **429**: delegates to `createRateLimitFetch` (Retry-After / backoff), unless
|
|
15792
|
+
* `retry` is `false`.
|
|
15793
|
+
* - **401**: the key was rejected (revoked, expired, or not authorized for the
|
|
15794
|
+
* requested team). Rather than silently returning a bare 401 on every call,
|
|
15795
|
+
* throw an actionable {@link AuthenticationError}. The key value is never
|
|
15796
|
+
* included in the message.
|
|
15797
|
+
*/
|
|
15798
|
+
function createAgentKeyFetch(retry) {
|
|
15799
|
+
const rateLimitFetch = retry === false ? fetch : createRateLimitFetch({
|
|
15800
|
+
maxRetries: retry?.maxRateLimitRetries,
|
|
15801
|
+
baseDelayMs: retry?.baseDelayMs,
|
|
15802
|
+
maxDelayMs: retry?.maxDelayMs
|
|
15803
|
+
});
|
|
15804
|
+
return async (input, init) => {
|
|
15805
|
+
const response = await rateLimitFetch(input, init);
|
|
15806
|
+
if (response.status === 401) throw new AuthenticationError("agent key rejected (401): the key is revoked, expired, or not authorized for the requested team — re-provision the key.", { statusCode: 401 });
|
|
15807
|
+
return response;
|
|
15808
|
+
};
|
|
15809
|
+
}
|
|
15759
15810
|
//#endregion
|
|
15760
15811
|
//#region ../sdk/src/token.ts
|
|
15761
15812
|
var TokenManager = class {
|
|
@@ -15815,37 +15866,76 @@ var TokenManager = class {
|
|
|
15815
15866
|
};
|
|
15816
15867
|
//#endregion
|
|
15817
15868
|
//#region ../sdk/src/connect.ts
|
|
15818
|
-
|
|
15819
|
-
|
|
15869
|
+
async function resolveConnection(options) {
|
|
15870
|
+
const env = readEnvCredentials();
|
|
15871
|
+
const explicitAgentKey = options.agentKey?.trim();
|
|
15872
|
+
if (explicitAgentKey) {
|
|
15873
|
+
const config = await readConfig(options.configDir);
|
|
15874
|
+
return {
|
|
15875
|
+
mode: "agentKey",
|
|
15876
|
+
agentKey: explicitAgentKey,
|
|
15877
|
+
apiUrl: normalizeApiUrl(options.apiUrl, env.apiUrl, config?.endpoints?.api)
|
|
15878
|
+
};
|
|
15879
|
+
}
|
|
15820
15880
|
if (options.clientId && options.clientSecret) return {
|
|
15881
|
+
mode: "oauth2",
|
|
15821
15882
|
clientId: options.clientId,
|
|
15822
15883
|
clientSecret: options.clientSecret,
|
|
15823
|
-
apiUrl: (options.apiUrl
|
|
15884
|
+
apiUrl: normalizeApiUrl(options.apiUrl, env.apiUrl)
|
|
15824
15885
|
};
|
|
15825
|
-
const
|
|
15886
|
+
const envAgentKey = env.agentKey?.trim();
|
|
15887
|
+
if (envAgentKey) {
|
|
15888
|
+
const config = await readConfig(options.configDir);
|
|
15889
|
+
return {
|
|
15890
|
+
mode: "agentKey",
|
|
15891
|
+
agentKey: envAgentKey,
|
|
15892
|
+
apiUrl: normalizeApiUrl(options.apiUrl, env.apiUrl, config?.endpoints?.api)
|
|
15893
|
+
};
|
|
15894
|
+
}
|
|
15826
15895
|
if (env.clientId && env.clientSecret) return {
|
|
15896
|
+
mode: "oauth2",
|
|
15827
15897
|
clientId: env.clientId,
|
|
15828
15898
|
clientSecret: env.clientSecret,
|
|
15829
|
-
apiUrl: (
|
|
15899
|
+
apiUrl: normalizeApiUrl(options.apiUrl, env.apiUrl)
|
|
15830
15900
|
};
|
|
15831
15901
|
const config = await readConfig(options.configDir);
|
|
15832
15902
|
if (config?.oauth2?.client_id && config?.oauth2?.client_secret) return {
|
|
15903
|
+
mode: "oauth2",
|
|
15833
15904
|
clientId: config.oauth2.client_id,
|
|
15834
15905
|
clientSecret: config.oauth2.client_secret,
|
|
15835
|
-
apiUrl: (options.apiUrl
|
|
15906
|
+
apiUrl: normalizeApiUrl(options.apiUrl, config.endpoints?.api)
|
|
15836
15907
|
};
|
|
15837
|
-
throw new MoltNetError("No credentials found. Provide clientId/clientSecret, set MOLTNET_CLIENT_ID/MOLTNET_CLIENT_SECRET
|
|
15908
|
+
throw new MoltNetError("No credentials found. Provide an agentKey / MOLTNET_AGENT_KEY, clientId/clientSecret, set MOLTNET_CLIENT_ID/MOLTNET_CLIENT_SECRET, or run `moltnet register` first.", { code: "NO_CREDENTIALS" });
|
|
15838
15909
|
}
|
|
15839
15910
|
/**
|
|
15840
15911
|
* Connect to MoltNet and return an authenticated Agent facade.
|
|
15841
15912
|
*
|
|
15842
|
-
* Credential resolution
|
|
15843
|
-
*
|
|
15844
|
-
*
|
|
15845
|
-
*
|
|
15913
|
+
* Credential resolution, highest precedence first. Explicit in-code options —
|
|
15914
|
+
* of either kind — always win over the environment and config file:
|
|
15915
|
+
* 1. Explicit `agentKey` option → agent-key mode (static bearer)
|
|
15916
|
+
* 2. Explicit `clientId` / `clientSecret` → OAuth2 client-credentials
|
|
15917
|
+
* 3. `MOLTNET_AGENT_KEY` env → agent-key mode
|
|
15918
|
+
* 4. `MOLTNET_CLIENT_ID` / `MOLTNET_CLIENT_SECRET` env → OAuth2
|
|
15919
|
+
* 5. Config file (`~/.config/moltnet/moltnet.json`) → OAuth2
|
|
15920
|
+
*
|
|
15921
|
+
* In agent-key mode the key is sent directly as a bearer token — no OAuth2
|
|
15922
|
+
* round-trip — and 429 backoff still applies; a rejected key surfaces an
|
|
15923
|
+
* `AuthenticationError`.
|
|
15846
15924
|
*/
|
|
15847
15925
|
async function connect(options = {}) {
|
|
15848
|
-
const
|
|
15926
|
+
const resolved = await resolveConnection(options);
|
|
15927
|
+
if (resolved.mode === "agentKey") {
|
|
15928
|
+
const client = createClient({
|
|
15929
|
+
baseUrl: resolved.apiUrl,
|
|
15930
|
+
fetch: createAgentKeyFetch(options.retry)
|
|
15931
|
+
});
|
|
15932
|
+
const auth = () => Promise.resolve(resolved.agentKey);
|
|
15933
|
+
return createAgent({
|
|
15934
|
+
client,
|
|
15935
|
+
auth
|
|
15936
|
+
});
|
|
15937
|
+
}
|
|
15938
|
+
const creds = resolved;
|
|
15849
15939
|
const autoToken = options.autoToken ?? true;
|
|
15850
15940
|
const tokenManager = new TokenManager({
|
|
15851
15941
|
clientId: creds.clientId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/node-red-contrib-core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Node-RED nodes for the MoltNet API",
|
|
6
6
|
"keywords": [
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"main": "dist/nodes/agent.js",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@themoltnet/sdk": "0.
|
|
49
|
+
"@themoltnet/sdk": "0.124.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^22.19.0",
|