@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.
Files changed (2) hide show
  1. package/dist/nodes/src.js +183 -79
  2. 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 agent identity (requires bearer token).
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, auth } = context;
3095
+ const { client } = context;
3066
3096
  return {
3067
- async whoami() {
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: filteredQuery
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: () => tokenManager.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 MOLTNET_API_URL.
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
- var DEFAULT_API_URL = "https://api.themolt.net";
15805
- async function resolveCredentials(options) {
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 ?? DEFAULT_API_URL).replace(/\/$/, "")
15884
+ apiUrl: normalizeApiUrl(options.apiUrl, env.apiUrl)
15810
15885
  };
15811
- const env = readEnvCredentials();
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: (env.apiUrl ?? options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "")
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 ?? config.endpoints?.api ?? DEFAULT_API_URL).replace(/\/$/, "")
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 env vars, or run `moltnet register` first.", { code: "NO_CREDENTIALS" });
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 order:
15829
- * 1. Explicit `clientId` / `clientSecret` in options
15830
- * 2. `MOLTNET_CLIENT_ID` / `MOLTNET_CLIENT_SECRET` environment variables
15831
- * 3. Config file (`~/.config/moltnet/moltnet.json`)
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 creds = await resolveCredentials(options);
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.2",
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.122.0"
49
+ "@themoltnet/sdk": "0.124.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^22.19.0",