@thinremote/thinr-cli 1.3.4 → 1.4.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +117 -123
  3. package/bin/thinr.js +43 -105
  4. package/commands/_shared.js +10 -90
  5. package/commands/device/_shared.js +2 -2
  6. package/commands/device/exec.js +2 -2
  7. package/commands/device/fs.js +2 -2
  8. package/commands/device/index.js +5 -19
  9. package/commands/device/logs.js +4 -4
  10. package/commands/device/status.js +1 -1
  11. package/commands/device/transfer.js +2 -2
  12. package/commands/device/update.js +8 -2
  13. package/commands/fleet/upgrade.js +3 -3
  14. package/commands/playbook/run.js +2 -2
  15. package/commands/playbook/validate.js +2 -2
  16. package/commands/product/_shared.js +1 -1
  17. package/commands/product/exec.js +2 -2
  18. package/commands/product/fs.js +2 -2
  19. package/commands/product/index.js +4 -11
  20. package/commands/product/logs.js +2 -2
  21. package/commands/product/metric.js +2 -2
  22. package/commands/product/playbook.js +5 -5
  23. package/commands/product/property.js +4 -4
  24. package/commands/product/push.js +2 -2
  25. package/commands/product/resource.js +4 -4
  26. package/commands/product/status.js +3 -3
  27. package/dist/dashboard.js +12 -12
  28. package/docs/guides/thinremote-metrics-logs.md +59 -0
  29. package/docs/guides/thinremote-playbooks.md +133 -0
  30. package/docs/guides/thinremote-scripts.md +115 -0
  31. package/docs/guides/thinremote.md +116 -0
  32. package/lib/command-stream.js +3 -2
  33. package/lib/console.js +6 -6
  34. package/lib/constants.js +1 -19
  35. package/lib/dashboard/event-stream.js +5 -5
  36. package/lib/device-api.js +4 -4
  37. package/lib/guides.js +41 -0
  38. package/lib/identity.js +75 -0
  39. package/lib/local-files.js +54 -0
  40. package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
  41. package/lib/mcp/helpers.js +5 -25
  42. package/lib/mcp/playbook-ops.js +393 -0
  43. package/lib/mcp/registry.js +9 -43
  44. package/lib/mcp/resource-hooks.js +53 -0
  45. package/lib/mcp/resource-types.js +857 -0
  46. package/lib/mcp/tools-exec.js +217 -177
  47. package/lib/monitoring.js +3 -3
  48. package/lib/playbook/actions.js +30 -18
  49. package/lib/playbook/expression.js +1 -4
  50. package/lib/playbook/fleet.js +4 -1
  51. package/lib/playbook/loader.js +16 -8
  52. package/lib/playbook/runner.js +40 -8
  53. package/lib/playbook/schema.js +108 -34
  54. package/lib/product/log-presets.js +1 -1
  55. package/lib/product/logs.js +2 -2
  56. package/lib/product-orchestrator.js +14 -9
  57. package/lib/product.js +29 -227
  58. package/lib/proxy.js +4 -4
  59. package/lib/status.js +13 -5
  60. package/package.json +4 -2
  61. package/commands/device/delete.js +0 -53
  62. package/commands/device/list.js +0 -55
  63. package/commands/device/property.js +0 -52
  64. package/commands/device/resource.js +0 -52
  65. package/commands/logout.js +0 -47
  66. package/commands/product/list.js +0 -86
  67. package/commands/product/props.js +0 -125
  68. package/commands/profile.js +0 -173
  69. package/lib/alarms.js +0 -223
  70. package/lib/api.js +0 -89
  71. package/lib/auth.js +0 -428
  72. package/lib/bucket.js +0 -82
  73. package/lib/concurrency.js +0 -52
  74. package/lib/config.js +0 -230
  75. package/lib/devices.js +0 -60
  76. package/lib/errors.js +0 -65
  77. package/lib/format.js +0 -114
  78. package/lib/mcp/server.js +0 -67
  79. package/lib/mcp/tools-alarms.js +0 -507
  80. package/lib/mcp/tools-buckets.js +0 -99
  81. package/lib/mcp/tools-devices.js +0 -235
  82. package/lib/mcp/tools-filesystem.js +0 -281
  83. package/lib/mcp/tools-logs.js +0 -225
  84. package/lib/mcp/tools-playbook.js +0 -210
  85. package/lib/mcp/tools-products.js +0 -1557
  86. package/lib/mcp/tools-profile.js +0 -635
  87. package/lib/mcp/tools-profiles.js +0 -30
  88. package/lib/mcp/tools-properties.js +0 -79
  89. package/lib/mcp/tools-resources.js +0 -78
  90. package/lib/mcp/tools-scripts.js +0 -212
  91. package/lib/mcp/tools-tokens.js +0 -369
  92. package/lib/output.js +0 -114
  93. package/lib/paths.js +0 -32
  94. package/lib/profile.js +0 -657
  95. package/lib/property.js +0 -75
  96. package/lib/resource.js +0 -151
  97. package/lib/storage.js +0 -117
  98. package/lib/tokens.js +0 -366
@@ -1,86 +0,0 @@
1
- // @ts-check
2
- import { getProducts, getProductApi } from '../../lib/product.js';
3
- import { runPool } from '../../lib/concurrency.js';
4
- import { label, hint, success } from '../../lib/format.js';
5
- import {
6
- isJsonMode,
7
- printOk,
8
- printErr,
9
- createSpinner,
10
- classifyError,
11
- } from '../../lib/output.js';
12
- import {
13
- applyJsonFlag,
14
- ensureConfigured,
15
- getGlobalUser,
16
- parsePositiveInt,
17
- } from '../_shared.js';
18
-
19
- export function registerProductListCommand(product) {
20
- product
21
- .command('list')
22
- .helpGroup('Discovery:')
23
- .description('List every product configured on the server')
24
- .option('-j, --json', 'Output as JSON')
25
- .option(
26
- '-c, --concurrency <n>',
27
- 'Max parallel api-resource lookups (default: 10)',
28
- parsePositiveInt('concurrency'),
29
- 10,
30
- )
31
- .action(async (opts, cmd) => {
32
- applyJsonFlag(opts);
33
- ensureConfigured();
34
- const user = getGlobalUser(cmd);
35
-
36
- const spinner = createSpinner('Fetching products...').start();
37
- let products;
38
- try {
39
- products = await getProducts(user);
40
- } catch (error) {
41
- spinner.fail('Failed to fetch products');
42
- const { message, code } = classifyError(error);
43
- printErr(message, { code });
44
- return;
45
- }
46
-
47
- // Script counts come from each product's api resources map.
48
- // Parallelise so a large catalogue doesn't crawl serially.
49
- const scriptResults = await runPool(products, opts.concurrency, async (p) => {
50
- try {
51
- const apis = await getProductApi(p.product, user);
52
- return Object.keys(apis || {}).length;
53
- } catch {
54
- return null;
55
- }
56
- });
57
- const rows = products.map((p, i) => ({
58
- product: p.product,
59
- name: p.name && p.name !== p.product ? p.name : null,
60
- enabled: !!p.enabled,
61
- scripts: scriptResults[i]?.ok ? scriptResults[i].value : null,
62
- }));
63
-
64
- spinner.succeed(`Found ${rows.length} product(s)`);
65
-
66
- if (isJsonMode()) {
67
- printOk({ products: rows });
68
- return;
69
- }
70
-
71
- if (rows.length === 0) return;
72
-
73
- // Pad the product id column so the trailing name/scripts
74
- // bits align even when ids have very different widths.
75
- const idWidth = rows.reduce((w, r) => Math.max(w, r.product.length), 0);
76
- for (const r of rows) {
77
- const flag = r.enabled ? success('enabled ') : hint('disabled');
78
- const id = label(r.product.padEnd(idWidth));
79
- const name = r.name ? hint(`(${r.name})`) : '';
80
- const scripts =
81
- r.scripts != null && r.scripts > 0 ? hint(`${r.scripts} script(s)`) : '';
82
- const extras = [name, scripts].filter(Boolean).join(' ');
83
- console.log(` ${flag} ${id}${extras ? ' ' + extras : ''}`);
84
- }
85
- });
86
- }
@@ -1,125 +0,0 @@
1
- // @ts-check
2
- import {
3
- getProductProperties,
4
- getProductProperty,
5
- setProductProperty,
6
- deleteProductProperty,
7
- } from '../../lib/product.js';
8
- import { info, success } from '../../lib/format.js';
9
- import { isJsonMode, printOk, printErr, classifyError } from '../../lib/output.js';
10
- import { applyJsonFlag, ensureConfigured, getGlobalUser } from '../_shared.js';
11
-
12
- function parseJsonValue(raw) {
13
- if (raw == null) return raw;
14
- try {
15
- return JSON.parse(raw);
16
- } catch {
17
- // Fall back to raw string so `thinr product property-set p x foo` still works
18
- // for trivial scalar writes without shell-escaping quotes.
19
- return raw;
20
- }
21
- }
22
-
23
- export function registerProductPropertyAdminCommands(product) {
24
- product
25
- .command('property-list <productId>')
26
- .helpGroup('Product config:')
27
- .description('List the names of properties attached to a product')
28
- .option('-j, --json', 'Output as JSON')
29
- .action(async (productId, opts, cmd) => {
30
- applyJsonFlag(opts);
31
- ensureConfigured();
32
- const user = getGlobalUser(cmd);
33
- try {
34
- const list = await getProductProperties(productId, user);
35
- const names = Array.isArray(list) ? list.map((p) => p.property).filter(Boolean) : [];
36
- if (isJsonMode()) {
37
- printOk({ product: productId, properties: names });
38
- return;
39
- }
40
- if (names.length === 0) {
41
- console.log(`No properties on product ${info(productId)}`);
42
- return;
43
- }
44
- console.log(`Properties of product ${info(productId)}:`);
45
- for (const n of names) console.log(`- ${n}`);
46
- } catch (error) {
47
- const { message, code } = classifyError(error);
48
- printErr(message, { code });
49
- }
50
- });
51
-
52
- product
53
- .command('property-get <productId> <propertyId>')
54
- .helpGroup('Product config:')
55
- .description('Read a property attached to a product')
56
- .option('-j, --json', 'Output as JSON')
57
- .action(async (productId, propertyId, opts, cmd) => {
58
- applyJsonFlag(opts);
59
- ensureConfigured();
60
- const user = getGlobalUser(cmd);
61
- try {
62
- const value = await getProductProperty(productId, propertyId, user);
63
- if (isJsonMode()) {
64
- printOk({ product: productId, property: propertyId, value });
65
- return;
66
- }
67
- console.log(`Value of ${info(propertyId)} on product ${info(productId)}:`);
68
- console.log(JSON.stringify(value, null, 2));
69
- } catch (error) {
70
- const { message, code } = classifyError(error);
71
- printErr(message, { code });
72
- }
73
- });
74
-
75
- product
76
- .command('property-set <productId> <propertyId> <value>')
77
- .helpGroup('Product config:')
78
- .description('Create or overwrite a product property (value parsed as JSON)')
79
- .option('-j, --json', 'Output as JSON')
80
- .action(async (productId, propertyId, value, opts, cmd) => {
81
- applyJsonFlag(opts);
82
- ensureConfigured();
83
- const user = getGlobalUser(cmd);
84
- const parsed = parseJsonValue(value);
85
- try {
86
- const saved = await setProductProperty(productId, propertyId, parsed, user);
87
- if (isJsonMode()) {
88
- printOk({ product: productId, property: propertyId, value: saved.value });
89
- return;
90
- }
91
- console.log(
92
- `Saved property ${info(propertyId)} on product ${info(productId)} → ${success(JSON.stringify(saved.value))}`,
93
- );
94
- } catch (error) {
95
- const { message, code } = classifyError(error);
96
- printErr(message, { code });
97
- }
98
- });
99
-
100
- product
101
- .command('property-delete <productId> <propertyId>')
102
- .helpGroup('Product config:')
103
- .description('Delete a product property (idempotent)')
104
- .option('-j, --json', 'Output as JSON')
105
- .action(async (productId, propertyId, opts, cmd) => {
106
- applyJsonFlag(opts);
107
- ensureConfigured();
108
- const user = getGlobalUser(cmd);
109
- try {
110
- const removed = await deleteProductProperty(productId, propertyId, user);
111
- if (isJsonMode()) {
112
- printOk({ product: productId, property: propertyId, removed });
113
- return;
114
- }
115
- console.log(
116
- removed
117
- ? `Deleted ${info(propertyId)} from ${info(productId)}`
118
- : `Property ${info(propertyId)} was not set on ${info(productId)}`,
119
- );
120
- } catch (error) {
121
- const { message, code } = classifyError(error);
122
- printErr(message, { code });
123
- }
124
- });
125
- }
@@ -1,173 +0,0 @@
1
- import chalk from 'chalk';
2
- import { input } from '@inquirer/prompts';
3
- import {
4
- listProfiles,
5
- getActiveProfile,
6
- getProfile,
7
- setActiveProfile,
8
- setDefaultProfile,
9
- deleteProfile,
10
- } from '../lib/config.js';
11
- import { authenticate } from '../lib/auth.js';
12
- import { setJsonMode, isJsonMode, printOk, printErr } from '../lib/output.js';
13
- import { hint, success, label } from '../lib/format.js';
14
-
15
- function applyJsonFlag(opts) {
16
- if (opts.json) setJsonMode(true);
17
- }
18
-
19
- export function profileCommand(program) {
20
- const profile = program
21
- .command('profile')
22
- .helpGroup('Configuration:')
23
- .description('Manage configuration profiles');
24
-
25
- profile
26
- .command('list')
27
- .helpGroup('Profile:')
28
- .description('List configured profiles')
29
- .option('-j, --json', 'Output as JSON')
30
- .action((opts) => {
31
- applyJsonFlag(opts);
32
- const names = listProfiles();
33
- const active = getActiveProfile();
34
- if (isJsonMode()) {
35
- const entries = names.map((name) => {
36
- const data = getProfile(name) || {};
37
- return {
38
- name,
39
- active: name === active,
40
- username: data.username || null,
41
- server: data.server || null,
42
- };
43
- });
44
- printOk({ active, profiles: entries });
45
- return;
46
- }
47
- if (!names.length) {
48
- console.log(hint('No profiles configured. Run thinr to set one up.'));
49
- return;
50
- }
51
- for (const name of names) {
52
- const data = getProfile(name) || {};
53
- const marker = name === active ? success('* ') : ' ';
54
- const detail = hint(`${data.username || ''}@${data.server || name}`);
55
- console.log(`${marker}${label(name)} ${detail}`);
56
- }
57
- });
58
-
59
- profile
60
- .command('current')
61
- .helpGroup('Profile:')
62
- .description('Print the active profile')
63
- .option('-j, --json', 'Output as JSON')
64
- .action((opts) => {
65
- applyJsonFlag(opts);
66
- const name = getActiveProfile();
67
- if (isJsonMode()) {
68
- printOk({ active: name });
69
- return;
70
- }
71
- if (!name) {
72
- console.log(hint('No active profile.'));
73
- return;
74
- }
75
- console.log(name);
76
- });
77
-
78
- profile
79
- .command('use <name>')
80
- .helpGroup('Profile:')
81
- .description('Set the default profile')
82
- .option('-j, --json', 'Output as JSON')
83
- .action((name, opts) => {
84
- applyJsonFlag(opts);
85
- try {
86
- setDefaultProfile(name);
87
- } catch (error) {
88
- printErr(error.message, { code: 'not_found' });
89
- return;
90
- }
91
- if (isJsonMode()) {
92
- printOk({ active: name });
93
- return;
94
- }
95
- console.log(success(`Default profile set to ${name}`));
96
- });
97
-
98
- profile
99
- .command('add [name]')
100
- .description('Add a new profile via interactive authentication')
101
- .option('--no-activate', 'Do not set the new profile as default')
102
- .option('-j, --json', 'Output as JSON')
103
- .action(async (nameArg, opts) => {
104
- applyJsonFlag(opts);
105
- let name = nameArg;
106
- if (!name) {
107
- try {
108
- name = await input({
109
- message: 'Profile name:',
110
- validate: (v) => {
111
- const trimmed = (v || '').trim();
112
- if (!trimmed) return 'Profile name is required';
113
- if (/\s/.test(trimmed)) return 'Profile name cannot contain whitespace';
114
- return true;
115
- },
116
- });
117
- } catch (error) {
118
- printErr(error.message || 'Prompt cancelled');
119
- return;
120
- }
121
- }
122
- name = name.trim();
123
- if (getProfile(name)) {
124
- printErr(`Profile already exists: ${name}`, { code: 'already_exists' });
125
- return;
126
- }
127
- setActiveProfile(name);
128
- try {
129
- await authenticate();
130
- const saved = getProfile(name);
131
- if (!saved || !saved.token || !saved.username) {
132
- throw new Error('Authentication did not complete; profile discarded.');
133
- }
134
- } catch (error) {
135
- setActiveProfile(null);
136
- try {
137
- deleteProfile(name);
138
- } catch {
139
- // Best-effort cleanup — ignore secondary failures
140
- }
141
- printErr(error.message || 'Authentication failed');
142
- return;
143
- }
144
- const activate = opts.activate !== false;
145
- if (activate) setDefaultProfile(name);
146
- setActiveProfile(null);
147
- if (isJsonMode()) {
148
- printOk({ added: name, activated: activate, active: getActiveProfile() });
149
- return;
150
- }
151
- const suffix = activate ? ' and set as default' : '';
152
- console.log(chalk.green(`Profile ${name} added${suffix}`));
153
- });
154
-
155
- profile
156
- .command('delete <name>')
157
- .helpGroup('Profile:')
158
- .description('Remove a profile')
159
- .option('-j, --json', 'Output as JSON')
160
- .action((name, opts) => {
161
- applyJsonFlag(opts);
162
- const ok = deleteProfile(name);
163
- if (!ok) {
164
- printErr(`Profile not found: ${name}`, { code: 'not_found' });
165
- return;
166
- }
167
- if (isJsonMode()) {
168
- printOk({ removed: name });
169
- return;
170
- }
171
- console.log(success(`Removed profile ${name}`));
172
- });
173
- }
package/lib/alarms.js DELETED
@@ -1,223 +0,0 @@
1
- import api from './api.js';
2
- import { apiError } from './errors.js';
3
- import { v1 } from './paths.js';
4
-
5
- // Backend enums (see backend/src/thinger/features/alarms/alarm_rule.hpp).
6
- // Exported so the dashboard can render labels/colors without re-deriving
7
- // them from string heuristics.
8
- export const ALARM_STATE = {
9
- NONE: 0,
10
- ACTIVATED: 1,
11
- ACKNOWLEDGED: 2,
12
- LATCHED: 3,
13
- SHELVED: 4,
14
- CLEARED: 5,
15
- };
16
-
17
- export const ALARM_SEVERITY = {
18
- NONE: 0,
19
- LOW: 1,
20
- MEDIUM: 2,
21
- HIGH: 3,
22
- CRITICAL: 4,
23
- };
24
-
25
- export const ALARM_STATE_LABEL = {
26
- 0: 'none',
27
- 1: 'active',
28
- 2: 'ack',
29
- 3: 'latched',
30
- 4: 'shelved',
31
- 5: 'cleared',
32
- };
33
-
34
- export const ALARM_SEVERITY_LABEL = {
35
- 0: 'none',
36
- 1: 'low',
37
- 2: 'med',
38
- 3: 'high',
39
- 4: 'crit',
40
- };
41
-
42
- // Reverse maps so callers can pass human-readable strings ("ack", "shelved",
43
- // "high", …) instead of remembering the numeric enum values.
44
- const STATE_FROM_LABEL = {
45
- none: ALARM_STATE.NONE,
46
- active: ALARM_STATE.ACTIVATED,
47
- activated: ALARM_STATE.ACTIVATED,
48
- ack: ALARM_STATE.ACKNOWLEDGED,
49
- acknowledged: ALARM_STATE.ACKNOWLEDGED,
50
- latched: ALARM_STATE.LATCHED,
51
- shelved: ALARM_STATE.SHELVED,
52
- cleared: ALARM_STATE.CLEARED,
53
- };
54
-
55
- const SEVERITY_FROM_LABEL = {
56
- none: ALARM_SEVERITY.NONE,
57
- low: ALARM_SEVERITY.LOW,
58
- med: ALARM_SEVERITY.MEDIUM,
59
- medium: ALARM_SEVERITY.MEDIUM,
60
- high: ALARM_SEVERITY.HIGH,
61
- crit: ALARM_SEVERITY.CRITICAL,
62
- critical: ALARM_SEVERITY.CRITICAL,
63
- };
64
-
65
- export function parseAlarmState(value) {
66
- if (value == null) return undefined;
67
- if (typeof value === 'number') return value;
68
- const key = String(value).trim().toLowerCase();
69
- return STATE_FROM_LABEL[key];
70
- }
71
-
72
- export function parseAlarmSeverity(value) {
73
- if (value == null) return undefined;
74
- if (typeof value === 'number') return value;
75
- const key = String(value).trim().toLowerCase();
76
- return SEVERITY_FROM_LABEL[key];
77
- }
78
-
79
- // Repeats a query param as the server expects (`?state=1&state=2`).
80
- function appendMulti(params, key, values) {
81
- if (values == null) return;
82
- const arr = Array.isArray(values) ? values : [values];
83
- for (const v of arr) params.append(key, String(v));
84
- }
85
-
86
- /**
87
- * Query alarm instances from /v1/users/{user}/alarms/instances.
88
- *
89
- * `state` and `severity` are repeated query params — pass numbers or
90
- * arrays of numbers (matching the ALARM_STATE / ALARM_SEVERITY enums).
91
- *
92
- * @param {{
93
- * user?: string | null,
94
- * count?: number,
95
- * index?: number,
96
- * name?: string,
97
- * state?: number | number[],
98
- * severity?: number | number[],
99
- * sort?: string,
100
- * order?: string,
101
- * }} [opts]
102
- */
103
- export async function getAlarmInstances({
104
- user = null,
105
- count = 100,
106
- index = 0,
107
- name,
108
- state,
109
- severity,
110
- sort,
111
- order,
112
- } = {}) {
113
- const params = new URLSearchParams();
114
- params.set('count', String(count));
115
- params.set('index', String(index));
116
- if (name) params.set('name', name);
117
- if (sort) params.set('sort', sort);
118
- if (order) params.set('order', order);
119
- appendMulti(params, 'state', state);
120
- appendMulti(params, 'severity', severity);
121
- try {
122
- const res = await api.get(`${v1(user)}/alarms/instances?${params.toString()}`);
123
- return Array.isArray(res.data) ? res.data : [];
124
- } catch (e) {
125
- throw apiError(e);
126
- }
127
- }
128
-
129
- // Counter per state, e.g. { activated: 3, acknowledged: 1, ... }. The
130
- // server seeds zeros for every known state so consumers can render a
131
- // stable layout without null-checks.
132
- export async function getAlarmInstanceStats({ user = null } = {}) {
133
- try {
134
- const res = await api.get(`${v1(user)}/alarms/instances/stats`);
135
- return res.data || {};
136
- } catch (e) {
137
- throw apiError(e);
138
- }
139
- }
140
-
141
- export async function getAlarmInstance(instance, { user = null } = {}) {
142
- try {
143
- const res = await api.get(`${v1(user)}/alarms/instances/${encodeURIComponent(instance)}`);
144
- return res.data;
145
- } catch (e) {
146
- throw apiError(e, { notFound: `Alarm instance not found: ${instance}` });
147
- }
148
- }
149
-
150
- // PUT-style update. The server accepts only a small whitelist of fields
151
- // (see UPDATE_SCHEMA in alarms_instances.cpp): name, description, state,
152
- // annotation, reactivation. Anything else is rejected.
153
- export async function updateAlarmInstance(instance, body, { user = null } = {}) {
154
- try {
155
- const res = await api.put(
156
- `${v1(user)}/alarms/instances/${encodeURIComponent(instance)}`,
157
- body,
158
- );
159
- return res.data;
160
- } catch (e) {
161
- throw apiError(e, { notFound: `Alarm instance not found: ${instance}` });
162
- }
163
- }
164
-
165
- export async function deleteAlarmInstance(instance, { user = null } = {}) {
166
- try {
167
- await api.delete(`${v1(user)}/alarms/instances/${encodeURIComponent(instance)}`);
168
- return true;
169
- } catch (e) {
170
- throw apiError(e, { notFound: `Alarm instance not found: ${instance}` });
171
- }
172
- }
173
-
174
- export async function getAlarmRules({ user = null, count = 200, index = 0 } = {}) {
175
- const params = new URLSearchParams();
176
- params.set('count', String(count));
177
- params.set('index', String(index));
178
- try {
179
- const res = await api.get(`${v1(user)}/alarms/rules?${params.toString()}`);
180
- return Array.isArray(res.data) ? res.data : [];
181
- } catch (e) {
182
- throw apiError(e);
183
- }
184
- }
185
-
186
- export async function getAlarmRule(rule, { user = null } = {}) {
187
- try {
188
- const res = await api.get(`${v1(user)}/alarms/rules/${encodeURIComponent(rule)}`);
189
- return res.data;
190
- } catch (e) {
191
- throw apiError(e, { notFound: `Alarm rule not found: ${rule}` });
192
- }
193
- }
194
-
195
- // POST /v1/users/{user}/alarms/rules. The body must include `rule`,
196
- // `name`, `enabled` and an optional `config` object — see CREATE_SCHEMA
197
- // in alarms_rules.cpp for the full shape.
198
- export async function createAlarmRule(body, { user = null } = {}) {
199
- try {
200
- const res = await api.post(`${v1(user)}/alarms/rules`, body);
201
- return res.data;
202
- } catch (e) {
203
- throw apiError(e);
204
- }
205
- }
206
-
207
- export async function updateAlarmRule(rule, body, { user = null } = {}) {
208
- try {
209
- const res = await api.put(`${v1(user)}/alarms/rules/${encodeURIComponent(rule)}`, body);
210
- return res.data;
211
- } catch (e) {
212
- throw apiError(e, { notFound: `Alarm rule not found: ${rule}` });
213
- }
214
- }
215
-
216
- export async function deleteAlarmRule(rule, { user = null } = {}) {
217
- try {
218
- await api.delete(`${v1(user)}/alarms/rules/${encodeURIComponent(rule)}`);
219
- return true;
220
- } catch (e) {
221
- throw apiError(e, { notFound: `Alarm rule not found: ${rule}` });
222
- }
223
- }
package/lib/api.js DELETED
@@ -1,89 +0,0 @@
1
- import axios from 'axios';
2
- import { refreshToken, getAccessToken } from './auth.js';
3
- import { readConfig } from './config.js';
4
- import { TIMEOUTS } from './constants.js';
5
- import http from 'http';
6
- import https from 'https';
7
-
8
- const httpAgent = new http.Agent({ keepAlive: true });
9
- // Accept self-signed certificates when talking to local dev servers
10
- // (localhost / 127.0.0.1). Any other host still enforces full TLS
11
- // validation. Toggleable via THINR_INSECURE=1 for other local setups.
12
- const httpsAgent = new https.Agent({ keepAlive: true });
13
- const insecureAgent = new https.Agent({ keepAlive: true, rejectUnauthorized: false });
14
-
15
- const api = axios.create({
16
- // You can leave baseURL undefined initially
17
- timeout: TIMEOUTS.API_DEFAULT_MS,
18
- httpAgent,
19
- httpsAgent,
20
- });
21
-
22
- export function setBaseURL(url) {
23
- api.defaults.baseURL = url;
24
- }
25
-
26
- api.interceptors.request.use(
27
- (request) => {
28
- const token = getAccessToken();
29
- if (token) {
30
- request.headers['Authorization'] = `Bearer ${token}`;
31
- }
32
-
33
- // Allow self-signed TLS against localhost / 127.0.0.1 or when the user
34
- // opts in explicitly. Anything else keeps default certificate validation.
35
- // A malformed URL here is not worth aborting the request over — the
36
- // HTTPS layer will surface the real error downstream; we only care
37
- // about detecting localhost cheaply.
38
- try {
39
- const base = request.baseURL || api.defaults.baseURL || '';
40
- const url = new URL(request.url || '', base || 'https://placeholder/');
41
- const isLocal = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
42
- if (url.protocol === 'https:' && (isLocal || process.env.THINR_INSECURE === '1')) {
43
- request.httpsAgent = insecureAgent;
44
- }
45
- } catch {
46
- // Intentionally swallowed — see note above.
47
- }
48
-
49
- return request;
50
- },
51
- (error) => {
52
- return Promise.reject(error);
53
- },
54
- );
55
-
56
- api.interceptors.response.use(
57
- (response) => response,
58
- async (error) => {
59
- const originalRequest = error.config;
60
-
61
- // Only intervene on 401s; everything else surfaces unchanged.
62
- if (error.response?.status !== 401) return Promise.reject(error);
63
-
64
- // Don't recurse: refresh requests themselves and already-retried
65
- // requests must not trigger another refresh.
66
- if (originalRequest?._isRefresh || originalRequest?._retry) {
67
- return Promise.reject(error);
68
- }
69
-
70
- // Tokens issued via the username/password flow have no
71
- // refresh_token. Skip the refresh attempt and let the 401 reach
72
- // the caller — they can re-run `thinr` to re-authenticate.
73
- if (!readConfig().refresh_token) return Promise.reject(error);
74
-
75
- originalRequest._retry = true;
76
-
77
- try {
78
- const newToken = await refreshToken();
79
- // Patch only this request; subsequent requests pick up the new
80
- // token from config via the request interceptor.
81
- originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
82
- return api(originalRequest);
83
- } catch (refreshError) {
84
- return Promise.reject(refreshError);
85
- }
86
- },
87
- );
88
-
89
- export default api;