@themoltnet/node-red-contrib-core 0.12.2 → 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 +183 -79
- 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 });
|
|
@@ -2999,6 +3000,21 @@ function unwrapRequired(result, message, code) {
|
|
|
2999
3000
|
return result.data;
|
|
3000
3001
|
}
|
|
3001
3002
|
//#endregion
|
|
3003
|
+
//#region ../sdk/src/namespaces/query.ts
|
|
3004
|
+
/**
|
|
3005
|
+
* Remove `undefined`-valued keys from a query object before it is serialized.
|
|
3006
|
+
*
|
|
3007
|
+
* Returns `undefined` when no defined keys remain, so an all-`undefined` query
|
|
3008
|
+
* (`{ agentId: undefined }`) and an omitted query (`undefined`) serialize
|
|
3009
|
+
* identically — both send no query params — instead of the former collapsing to
|
|
3010
|
+
* an empty `{}` that still reaches the client.
|
|
3011
|
+
*/
|
|
3012
|
+
function stripUndefinedQuery(query) {
|
|
3013
|
+
if (!query) return;
|
|
3014
|
+
const entries = Object.entries(query).filter(([, value]) => value !== void 0);
|
|
3015
|
+
return entries.length ? Object.fromEntries(entries) : void 0;
|
|
3016
|
+
}
|
|
3017
|
+
//#endregion
|
|
3002
3018
|
//#region ../sdk/src/namespaces/team-headers.ts
|
|
3003
3019
|
/**
|
|
3004
3020
|
* Build the team header from an optional option, or `undefined` when no team
|
|
@@ -3025,7 +3041,7 @@ function createAgentKeysNamespace(context) {
|
|
|
3025
3041
|
client,
|
|
3026
3042
|
auth,
|
|
3027
3043
|
headers: requiredTeamHeaders(options),
|
|
3028
|
-
query
|
|
3044
|
+
query: stripUndefinedQuery(query)
|
|
3029
3045
|
}));
|
|
3030
3046
|
},
|
|
3031
3047
|
async create(body, options) {
|
|
@@ -3060,16 +3076,25 @@ function createAgentKeysNamespace(context) {
|
|
|
3060
3076
|
};
|
|
3061
3077
|
}
|
|
3062
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
|
|
3063
3093
|
//#region ../sdk/src/namespaces/agents.ts
|
|
3064
3094
|
function createAgentsNamespace(context) {
|
|
3065
|
-
const { client
|
|
3095
|
+
const { client } = context;
|
|
3066
3096
|
return {
|
|
3067
|
-
|
|
3068
|
-
return unwrapResult(await getWhoami({
|
|
3069
|
-
client,
|
|
3070
|
-
auth
|
|
3071
|
-
}));
|
|
3072
|
-
},
|
|
3097
|
+
whoami: createWhoami(context),
|
|
3073
3098
|
async lookup(fingerprint) {
|
|
3074
3099
|
return unwrapResult(await getAgentProfile({
|
|
3075
3100
|
client,
|
|
@@ -5395,12 +5420,11 @@ function createRuntimeSlotsNamespace(context) {
|
|
|
5395
5420
|
}
|
|
5396
5421
|
},
|
|
5397
5422
|
async list(query, options) {
|
|
5398
|
-
const filteredQuery = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== void 0));
|
|
5399
5423
|
return unwrapResult(await listRuntimeSlots({
|
|
5400
5424
|
auth,
|
|
5401
5425
|
client,
|
|
5402
5426
|
headers: requiredTeamHeaders(options),
|
|
5403
|
-
query:
|
|
5427
|
+
query: stripUndefinedQuery(query)
|
|
5404
5428
|
})).items;
|
|
5405
5429
|
}
|
|
5406
5430
|
};
|
|
@@ -15663,22 +15687,37 @@ function createAgent(options) {
|
|
|
15663
15687
|
runtimeSlots: createRuntimeSlotsNamespace(context),
|
|
15664
15688
|
runtimeSessions: createRuntimeSessionsNamespace(context),
|
|
15665
15689
|
client,
|
|
15666
|
-
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
|
+
}
|
|
15667
15695
|
};
|
|
15668
15696
|
}
|
|
15669
15697
|
//#endregion
|
|
15670
15698
|
//#region ../sdk/src/config.ts
|
|
15671
15699
|
/**
|
|
15672
15700
|
* Read MoltNet credentials from environment variables.
|
|
15673
|
-
* Reads MOLTNET_CLIENT_ID, MOLTNET_CLIENT_SECRET, and
|
|
15701
|
+
* Reads MOLTNET_CLIENT_ID, MOLTNET_CLIENT_SECRET, MOLTNET_API_URL, and
|
|
15702
|
+
* MOLTNET_AGENT_KEY.
|
|
15674
15703
|
*/
|
|
15675
15704
|
function readEnvCredentials() {
|
|
15676
15705
|
return {
|
|
15677
15706
|
clientId: process.env.MOLTNET_CLIENT_ID,
|
|
15678
15707
|
clientSecret: process.env.MOLTNET_CLIENT_SECRET,
|
|
15679
|
-
apiUrl: process.env.MOLTNET_API_URL
|
|
15708
|
+
apiUrl: process.env.MOLTNET_API_URL,
|
|
15709
|
+
agentKey: process.env.MOLTNET_AGENT_KEY
|
|
15680
15710
|
};
|
|
15681
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
|
+
}
|
|
15682
15721
|
//#endregion
|
|
15683
15722
|
//#region ../sdk/src/credentials.ts
|
|
15684
15723
|
function getConfigDir() {
|
|
@@ -15742,6 +15781,32 @@ function createRetryFetch(tokenManager, options) {
|
|
|
15742
15781
|
return doFetch(init);
|
|
15743
15782
|
};
|
|
15744
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
|
+
}
|
|
15745
15810
|
//#endregion
|
|
15746
15811
|
//#region ../sdk/src/token.ts
|
|
15747
15812
|
var TokenManager = class {
|
|
@@ -15801,37 +15866,76 @@ var TokenManager = class {
|
|
|
15801
15866
|
};
|
|
15802
15867
|
//#endregion
|
|
15803
15868
|
//#region ../sdk/src/connect.ts
|
|
15804
|
-
|
|
15805
|
-
|
|
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
|
+
}
|
|
15806
15880
|
if (options.clientId && options.clientSecret) return {
|
|
15881
|
+
mode: "oauth2",
|
|
15807
15882
|
clientId: options.clientId,
|
|
15808
15883
|
clientSecret: options.clientSecret,
|
|
15809
|
-
apiUrl: (options.apiUrl
|
|
15884
|
+
apiUrl: normalizeApiUrl(options.apiUrl, env.apiUrl)
|
|
15810
15885
|
};
|
|
15811
|
-
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
|
+
}
|
|
15812
15895
|
if (env.clientId && env.clientSecret) return {
|
|
15896
|
+
mode: "oauth2",
|
|
15813
15897
|
clientId: env.clientId,
|
|
15814
15898
|
clientSecret: env.clientSecret,
|
|
15815
|
-
apiUrl: (
|
|
15899
|
+
apiUrl: normalizeApiUrl(options.apiUrl, env.apiUrl)
|
|
15816
15900
|
};
|
|
15817
15901
|
const config = await readConfig(options.configDir);
|
|
15818
15902
|
if (config?.oauth2?.client_id && config?.oauth2?.client_secret) return {
|
|
15903
|
+
mode: "oauth2",
|
|
15819
15904
|
clientId: config.oauth2.client_id,
|
|
15820
15905
|
clientSecret: config.oauth2.client_secret,
|
|
15821
|
-
apiUrl: (options.apiUrl
|
|
15906
|
+
apiUrl: normalizeApiUrl(options.apiUrl, config.endpoints?.api)
|
|
15822
15907
|
};
|
|
15823
|
-
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" });
|
|
15824
15909
|
}
|
|
15825
15910
|
/**
|
|
15826
15911
|
* Connect to MoltNet and return an authenticated Agent facade.
|
|
15827
15912
|
*
|
|
15828
|
-
* Credential resolution
|
|
15829
|
-
*
|
|
15830
|
-
*
|
|
15831
|
-
*
|
|
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`.
|
|
15832
15924
|
*/
|
|
15833
15925
|
async function connect(options = {}) {
|
|
15834
|
-
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;
|
|
15835
15939
|
const autoToken = options.autoToken ?? true;
|
|
15836
15940
|
const tokenManager = new TokenManager({
|
|
15837
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",
|