@norbix.ai/cli 0.2.0 → 1.0.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.
package/README.md CHANGED
@@ -40,6 +40,7 @@ norbix scheduler list
40
40
  | `norbix config list/get/set/unset` | Manage the local config file |
41
41
  | `norbix db find/get/count/insert/update/delete/aggregate` | Work with database collections |
42
42
  | `norbix files list/info/upload/download/sign/delete/publish/unpublish` | Upload, download, manage and publish files |
43
+ | `norbix files integrations test <id>` | Check that a files integration really works (live upload/read/list/delete probe) |
43
44
  | `norbix users list/get/invite/block/unblock/delete` | Manage project users (membership) |
44
45
  | `norbix env list/use/delete` | Manage project environments |
45
46
  | `norbix logs list/trail` | Read project logs, follow one correlation ID |
@@ -124,6 +125,29 @@ pipe into `jq`:
124
125
  norbix db find orders --json | jq '.list.items[] | ._id'
125
126
  ```
126
127
 
128
+ ## Errors
129
+
130
+ A failed call prints one line: the error code, the message, and the HTTP
131
+ status.
132
+
133
+ ```sh
134
+ $ norbix files info nbin_1 a/b.txt
135
+ › Error: CM-ERRORS-FILES-016: File not found: "a/b.txt" does not exist in
136
+ › Local (nbin_1). (HTTP 404)
137
+ ```
138
+
139
+ The code and the message are the gateway's own — they come out of
140
+ `responseStatus.errors[]`, where the gateway puts them. Before this, every
141
+ failure read "Request failed with status 404" with no code.
142
+
143
+ Exit codes do not change: `1` for a failed call, `2` for a check that ran and
144
+ came back negative (`norbix files integrations test`).
145
+
146
+ **A refusal the gateway answers with HTTP 200** and
147
+ `responseStatus.isSuccess = false` now exits non-zero as well, wherever it did
148
+ not before — `norbix files publish` / `unpublish` were the two commands that
149
+ used to treat it as a success.
150
+
127
151
  ## Development
128
152
 
129
153
  ```sh
@@ -148,7 +172,7 @@ norbix files download invoices/2026/invoice.pdf
148
172
 
149
173
  Or pass `--integration <id>` (env var: `NORBIX_FILES_INTEGRATION_ID`).
150
174
 
151
- ### The eight file commands
175
+ ### The file commands
152
176
 
153
177
  | Command | What it does |
154
178
  | --- | --- |
@@ -160,6 +184,7 @@ Or pass `--integration <id>` (env var: `NORBIX_FILES_INTEGRATION_ID`).
160
184
  | `norbix files delete <path> [--yes]` | Delete one file. Asks first unless you pass `--yes`. |
161
185
  | `norbix files publish <path> [--folder]` | Give the file — or the whole folder — a link anyone can open, and print it. |
162
186
  | `norbix files unpublish <path> [--folder]` | Take that link away again. |
187
+ | `norbix files integrations test <id>` | Check that the storage behind an integration really works. See below. |
163
188
 
164
189
  Upload and download take more than one step, so the file bytes never pass
165
190
  through Norbix:
@@ -212,6 +237,27 @@ Four rules worth knowing:
212
237
  * Every dead link answers the same plain `404`: unknown, renamed, unpublished,
213
238
  deleted. A more precise answer would tell a stranger that the file is there.
214
239
 
240
+ ### Testing an integration
241
+
242
+ `norbix files integrations test <id>` runs a live probe against the storage
243
+ behind the integration: it uploads a small file, reads it, lists its folder and
244
+ deletes it again. One line per step:
245
+
246
+ ```sh
247
+ norbix files integrations test 55555555-5555-5555-5555-555555555555
248
+ # UploadFile OK
249
+ # GetFile FAILED
250
+ # - NoSuchKey: the object was not found
251
+ # GetAllFiles NOT_TESTED
252
+ # DeleteFile NOT_TESTED
253
+ # › Error: The files integration test failed: GetFile FAILED (2 later steps not tested).
254
+ ```
255
+
256
+ `NOT_TESTED` means the step was skipped because an earlier one failed. The
257
+ command exits `0` only when every step is `OK`, so it works in a script or a
258
+ CI job. `--json` prints the steps as data and keeps the same exit code. The
259
+ probe writes to the storage, so the key needs the `files:create` permission.
260
+
215
261
  ## Tests
216
262
 
217
263
  ```sh
package/dist/base.js CHANGED
@@ -157,12 +157,23 @@ export class BaseCommand extends Command {
157
157
  this.log(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
158
158
  }
159
159
  }
160
+ /**
161
+ * One line for a failed call: `<errorCode>: <message> (HTTP <status>)`.
162
+ *
163
+ * The code comes first because it is the part a reader can search for and a
164
+ * script can match on. It is the gateway's own code — the SDK reads it out
165
+ * of `responseStatus.errors[]`, where the gateway puts it (10b-files slice
166
+ * ERRORS, issue #66). The exit codes do not change.
167
+ */
160
168
  async catch(error) {
161
169
  if (error instanceof NorbixError) {
162
- const status = error.status;
163
- const suffix = status ? ` (HTTP ${status})` : '';
170
+ const { code, status } = error;
171
+ const prefix = code ? `${code}: ` : '';
172
+ // The SDK's last-resort message already ends in "(HTTP <status>)" —
173
+ // do not say it twice.
174
+ const suffix = status && !error.message.includes(`(HTTP ${status})`) ? ` (HTTP ${status})` : '';
164
175
  const hint = status === 401 ? '\nYour session may have expired. Run `norbix login` again.' : '';
165
- return this.error(`${error.message}${suffix}${hint}`);
176
+ return this.error(`${prefix}${error.message}${suffix}${hint}`);
166
177
  }
167
178
  return super.catch(error);
168
179
  }
@@ -0,0 +1,85 @@
1
+ import { Args } from '@oclif/core';
2
+ import { BaseCommand } from '../../../base.js';
3
+ import { callTestFilesIntegration, normaliseResult, } from '../../../lib/filesIntegrationTest.js';
4
+ export default class FilesIntegrationsTest extends BaseCommand {
5
+ static description = `Check that a files integration really works
6
+
7
+ Runs a live probe against the storage: uploads a small file, reads it, lists
8
+ its folder and deletes it again. Prints one line per step — OK, FAILED or
9
+ NOT_TESTED (skipped because an earlier step failed) — with the errors of a
10
+ failed step. Exits non-zero unless every step is OK.
11
+
12
+ Needs the files:create permission, because the probe writes to the storage.`;
13
+ static examples = [
14
+ '<%= config.bin %> files integrations test 55555555-5555-5555-5555-555555555555',
15
+ '<%= config.bin %> files integrations test 55555555-5555-5555-5555-555555555555 --json',
16
+ ];
17
+ static args = {
18
+ id: Args.string({ required: true, description: 'Files integration ID' }),
19
+ };
20
+ async run() {
21
+ const { args, flags } = await this.parse(FilesIntegrationsTest);
22
+ const ctx = this.resolveContext(flags);
23
+ // Builds nothing itself, but it is what refuses early and with a useful
24
+ // sentence when there is no project, region or credentials.
25
+ this.client(flags);
26
+ // TODO(10b-API-TEST): switch to `client.api.files.testFilesIntegration`
27
+ // once @norbix.ai/ts releases it (sdk-ts PR #44) — see the lib file.
28
+ const res = await callTestFilesIntegration(ctx, args.id);
29
+ const output = summarise(args.id, res);
30
+ if (this.jsonEnabled()) {
31
+ // oclif prints the returned value; the exit code still tells a script
32
+ // whether the integration works.
33
+ if (!output.ok)
34
+ process.exitCode = 2;
35
+ return output;
36
+ }
37
+ for (const line of render(output.steps))
38
+ this.log(line);
39
+ if (!output.ok) {
40
+ this.error(failureMessage(output));
41
+ }
42
+ this.log(`\nAll ${output.steps.length} steps passed.`);
43
+ return output;
44
+ }
45
+ }
46
+ function summarise(id, res) {
47
+ const steps = (res.items ?? []).map((item) => ({
48
+ operation: item.operation,
49
+ result: normaliseResult(item.result),
50
+ errors: item.errors ?? [],
51
+ }));
52
+ const errors = (res.responseStatus?.errors ?? [])
53
+ .map((e) => e.message ?? e.errorCode ?? '')
54
+ .filter(Boolean);
55
+ const gatewayOk = res.responseStatus?.isSuccess !== false;
56
+ if (!gatewayOk && errors.length === 0) {
57
+ errors.push('the gateway answered isSuccess: false without an error message');
58
+ }
59
+ const ok = gatewayOk && steps.length > 0 && steps.every((s) => s.result === 'OK');
60
+ return { filesIntegrationId: id, ok, steps, errors };
61
+ }
62
+ function render(steps) {
63
+ const width = Math.max(0, ...steps.map((s) => s.operation.length));
64
+ const lines = [];
65
+ for (const step of steps) {
66
+ lines.push(`${step.operation.padEnd(width)} ${step.result}`);
67
+ for (const error of step.errors)
68
+ lines.push(`${' '.repeat(width)} - ${error}`);
69
+ }
70
+ return lines;
71
+ }
72
+ function failureMessage(output) {
73
+ if (output.errors.length > 0) {
74
+ return `The files integration test was refused: ${output.errors.join('; ')}`;
75
+ }
76
+ if (output.steps.length === 0) {
77
+ return 'The gateway returned no test steps, so nothing was proven.';
78
+ }
79
+ const notOk = output.steps.filter((s) => s.result !== 'OK');
80
+ const failed = notOk.filter((s) => s.result !== 'NOT_TESTED');
81
+ const skipped = notOk.length - failed.length;
82
+ const named = (failed.length > 0 ? failed : notOk).map((s) => `${s.operation} ${s.result}`).join(', ');
83
+ const tail = failed.length > 0 && skipped > 0 ? ` (${skipped} later step${skipped === 1 ? '' : 's'} not tested)` : '';
84
+ return `The files integration test failed: ${named}${tail}.`;
85
+ }
@@ -0,0 +1,43 @@
1
+ import { gatewayError } from './gatewayError.js';
2
+ /** The API version the SDK talks by default. Kept in one place. */
3
+ const API_VERSION = 'v2';
4
+ export async function callTestFilesIntegration(ctx, filesIntegrationId) {
5
+ const token = ctx.bearerToken ?? ctx.apiKey;
6
+ const headers = new Headers({ Accept: 'application/json' });
7
+ if (token)
8
+ headers.set('Authorization', `Bearer ${token}`);
9
+ if (ctx.projectId) {
10
+ headers.set('norbix-project-id', ctx.projectId);
11
+ headers.set('X-CM-ProjectId', ctx.projectId);
12
+ }
13
+ if (ctx.accountId) {
14
+ headers.set('norbix-account-id', ctx.accountId);
15
+ headers.set('X-CM-AccountId', ctx.accountId);
16
+ }
17
+ if (ctx.env && ctx.env !== 'PROD')
18
+ headers.set('norbix-env', ctx.env);
19
+ if (ctx.region)
20
+ headers.set('nb-region', ctx.region);
21
+ const base = ctx.apiUrl.endsWith('/') ? ctx.apiUrl.slice(0, -1) : ctx.apiUrl;
22
+ const url = `${base}/${API_VERSION}/files/${encodeURIComponent(filesIntegrationId)}/test`;
23
+ const response = await fetch(url, { headers, method: 'POST' });
24
+ const text = await response.text();
25
+ let raw;
26
+ if (text) {
27
+ try {
28
+ raw = JSON.parse(text);
29
+ }
30
+ catch {
31
+ raw = text;
32
+ }
33
+ }
34
+ if (!response.ok)
35
+ throw gatewayError(response.status, raw, url);
36
+ return (raw && typeof raw === 'object' ? raw : {});
37
+ }
38
+ /** `Failed` / `NotTested` / `NOT_TESTED` → `FAILED` / `NOT_TESTED`. */
39
+ export function normaliseResult(result) {
40
+ if (!result)
41
+ return 'UNKNOWN';
42
+ return result.replaceAll(/([a-z])([A-Z])/g, '$1_$2').replaceAll(/[\s-]+/g, '_').toUpperCase();
43
+ }
@@ -0,0 +1,58 @@
1
+ import { NorbixAuthError, NorbixError, NorbixValidationError } from '@norbix.ai/ts';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ /** The `responseStatus` block of a body, whatever the casing of the key. */
6
+ function responseStatusOf(raw) {
7
+ if (!isRecord(raw))
8
+ return undefined;
9
+ for (const key of Object.keys(raw)) {
10
+ if (key.toLowerCase() === 'responsestatus') {
11
+ const value = raw[key];
12
+ return isRecord(value) ? value : undefined;
13
+ }
14
+ }
15
+ return undefined;
16
+ }
17
+ function text(value) {
18
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
19
+ }
20
+ function itemsOf(value) {
21
+ if (!Array.isArray(value))
22
+ return [];
23
+ return value.filter((entry) => isRecord(entry)).map((entry) => entry);
24
+ }
25
+ /**
26
+ * True when the gateway said the call failed inside the body.
27
+ *
28
+ * A business refusal — an unknown id, a rule that says no — comes back as HTTP
29
+ * 200 with `responseStatus.isSuccess = false` (10b-files, issue #67).
30
+ */
31
+ export function saysItFailed(raw) {
32
+ const status = responseStatusOf(raw);
33
+ return isRecord(status) && status.isSuccess === false;
34
+ }
35
+ /** The message and the error code a gateway answer carries. */
36
+ export function readGatewayError(raw, status) {
37
+ const responseStatus = responseStatusOf(raw);
38
+ // `source` is responseStatus when the body has one, the body itself when it
39
+ // has none — so the top-level fields are read only in the second case.
40
+ const source = responseStatus ?? (isRecord(raw) ? raw : undefined);
41
+ const errors = itemsOf(source?.errors);
42
+ const first = errors.find((e) => e.message !== undefined || e.errorCode !== undefined);
43
+ return {
44
+ code: first?.errorCode ?? text(source?.errorCode),
45
+ errors,
46
+ message: first?.message ?? text(source?.message) ?? `Request failed (HTTP ${status})`,
47
+ };
48
+ }
49
+ /** Build the right NorbixError for a status and an already-parsed body. */
50
+ export function gatewayError(status, raw, url) {
51
+ const { code, message } = readGatewayError(raw, status);
52
+ const opts = { code, message, raw, status, url };
53
+ if (status === 401 || status === 403)
54
+ return new NorbixAuthError(opts);
55
+ if (status === 400)
56
+ return new NorbixValidationError(opts);
57
+ return new NorbixError(opts);
58
+ }
@@ -1,4 +1,4 @@
1
- import { NorbixError } from '@norbix.ai/ts';
1
+ import { gatewayError, saysItFailed } from './gatewayError.js';
2
2
  const ROUTES = {
3
3
  makeFilePrivate: 'files/item/private',
4
4
  makeFilePublic: 'files/item/public',
@@ -27,23 +27,23 @@ export async function callPublicFiles(ctx, operation, body) {
27
27
  const url = `${base}/${HUB_VERSION}/${ROUTES[operation]}`;
28
28
  const response = await fetch(url, { body: JSON.stringify(body), headers, method: 'POST' });
29
29
  const text = await response.text();
30
- let payload = {};
30
+ let raw;
31
31
  if (text) {
32
32
  try {
33
- payload = JSON.parse(text);
33
+ raw = JSON.parse(text);
34
34
  }
35
35
  catch {
36
- payload = {};
36
+ raw = text;
37
37
  }
38
38
  }
39
- if (!response.ok) {
40
- throw new NorbixError({
41
- code: payload.status,
42
- message: payload.message ?? text ?? 'Request failed',
43
- status: response.status,
44
- });
45
- }
46
- return payload;
39
+ if (!response.ok)
40
+ throw gatewayError(response.status, raw, url);
41
+ // A 2xx does not mean the call worked: the gateway answers a business
42
+ // refusal with HTTP 200 and responseStatus.isSuccess = false, and the
43
+ // command must exit non-zero for it (10b-files, issue #67).
44
+ if (saysItFailed(raw))
45
+ throw gatewayError(response.status, raw, url);
46
+ return (raw && typeof raw === 'object' ? raw : {});
47
47
  }
48
48
  /**
49
49
  * The address anyone can open. The gateway builds the same one and puts it on
@@ -9084,18 +9084,18 @@
9084
9084
  "unarchive.js"
9085
9085
  ]
9086
9086
  },
9087
- "users:block": {
9087
+ "webhooks:disable": {
9088
9088
  "aliases": [],
9089
9089
  "args": {
9090
- "id": {
9091
- "description": "User ID",
9092
- "name": "id",
9090
+ "destinationId": {
9091
+ "description": "Destination ID",
9092
+ "name": "destinationId",
9093
9093
  "required": true
9094
9094
  }
9095
9095
  },
9096
- "description": "Block a user (they can no longer log in)",
9096
+ "description": "Disable a webhook destination",
9097
9097
  "examples": [
9098
- "<%= config.bin %> users block 66b2f0a1..."
9098
+ "<%= config.bin %> webhooks disable 66b2f0a1..."
9099
9099
  ],
9100
9100
  "flags": {
9101
9101
  "json": {
@@ -9162,7 +9162,7 @@
9162
9162
  },
9163
9163
  "hasDynamicHelp": false,
9164
9164
  "hiddenAliases": [],
9165
- "id": "users:block",
9165
+ "id": "webhooks:disable",
9166
9166
  "pluginAlias": "@norbix.ai/cli",
9167
9167
  "pluginName": "@norbix.ai/cli",
9168
9168
  "pluginType": "core",
@@ -9172,22 +9172,22 @@
9172
9172
  "relativePath": [
9173
9173
  "dist",
9174
9174
  "commands",
9175
- "users",
9176
- "block.js"
9175
+ "webhooks",
9176
+ "disable.js"
9177
9177
  ]
9178
9178
  },
9179
- "users:delete": {
9179
+ "webhooks:enable": {
9180
9180
  "aliases": [],
9181
9181
  "args": {
9182
- "id": {
9183
- "description": "User ID",
9184
- "name": "id",
9182
+ "destinationId": {
9183
+ "description": "Destination ID",
9184
+ "name": "destinationId",
9185
9185
  "required": true
9186
9186
  }
9187
9187
  },
9188
- "description": "Delete a user",
9188
+ "description": "Enable a webhook destination",
9189
9189
  "examples": [
9190
- "<%= config.bin %> users delete 66b2f0a1... --yes"
9190
+ "<%= config.bin %> webhooks enable 66b2f0a1..."
9191
9191
  ],
9192
9192
  "flags": {
9193
9193
  "json": {
@@ -9250,18 +9250,11 @@
9250
9250
  "hasDynamicHelp": false,
9251
9251
  "multiple": false,
9252
9252
  "type": "option"
9253
- },
9254
- "yes": {
9255
- "char": "y",
9256
- "description": "Skip the confirmation prompt",
9257
- "name": "yes",
9258
- "allowNo": false,
9259
- "type": "boolean"
9260
9253
  }
9261
9254
  },
9262
9255
  "hasDynamicHelp": false,
9263
9256
  "hiddenAliases": [],
9264
- "id": "users:delete",
9257
+ "id": "webhooks:enable",
9265
9258
  "pluginAlias": "@norbix.ai/cli",
9266
9259
  "pluginName": "@norbix.ai/cli",
9267
9260
  "pluginType": "core",
@@ -9271,22 +9264,22 @@
9271
9264
  "relativePath": [
9272
9265
  "dist",
9273
9266
  "commands",
9274
- "users",
9275
- "delete.js"
9267
+ "webhooks",
9268
+ "enable.js"
9276
9269
  ]
9277
9270
  },
9278
- "users:get": {
9271
+ "webhooks:remove": {
9279
9272
  "aliases": [],
9280
9273
  "args": {
9281
- "id": {
9282
- "description": "User ID",
9283
- "name": "id",
9274
+ "destinationId": {
9275
+ "description": "Destination ID",
9276
+ "name": "destinationId",
9284
9277
  "required": true
9285
9278
  }
9286
9279
  },
9287
- "description": "Show one user",
9280
+ "description": "Remove a webhook destination",
9288
9281
  "examples": [
9289
- "<%= config.bin %> users get 66b2f0a1..."
9282
+ "<%= config.bin %> webhooks remove 66b2f0a1... --yes"
9290
9283
  ],
9291
9284
  "flags": {
9292
9285
  "json": {
@@ -9349,11 +9342,18 @@
9349
9342
  "hasDynamicHelp": false,
9350
9343
  "multiple": false,
9351
9344
  "type": "option"
9345
+ },
9346
+ "yes": {
9347
+ "char": "y",
9348
+ "description": "Skip the confirmation prompt",
9349
+ "name": "yes",
9350
+ "allowNo": false,
9351
+ "type": "boolean"
9352
9352
  }
9353
9353
  },
9354
9354
  "hasDynamicHelp": false,
9355
9355
  "hiddenAliases": [],
9356
- "id": "users:get",
9356
+ "id": "webhooks:remove",
9357
9357
  "pluginAlias": "@norbix.ai/cli",
9358
9358
  "pluginName": "@norbix.ai/cli",
9359
9359
  "pluginType": "core",
@@ -9363,22 +9363,17 @@
9363
9363
  "relativePath": [
9364
9364
  "dist",
9365
9365
  "commands",
9366
- "users",
9367
- "get.js"
9366
+ "webhooks",
9367
+ "remove.js"
9368
9368
  ]
9369
9369
  },
9370
- "users:invite": {
9370
+ "webhooks:secret": {
9371
9371
  "aliases": [],
9372
- "args": {
9373
- "email": {
9374
- "description": "Email address to invite",
9375
- "name": "email",
9376
- "required": true
9377
- }
9378
- },
9379
- "description": "Invite a user by email",
9372
+ "args": {},
9373
+ "description": "Reveal the webhook signing secret, or rotate it with --rotate",
9380
9374
  "examples": [
9381
- "<%= config.bin %> users invite alice@example.com"
9375
+ "<%= config.bin %> webhooks secret",
9376
+ "<%= config.bin %> webhooks secret --rotate --yes"
9382
9377
  ],
9383
9378
  "flags": {
9384
9379
  "json": {
@@ -9441,11 +9436,24 @@
9441
9436
  "hasDynamicHelp": false,
9442
9437
  "multiple": false,
9443
9438
  "type": "option"
9439
+ },
9440
+ "rotate": {
9441
+ "description": "Rotate the secret (old secret stops working)",
9442
+ "name": "rotate",
9443
+ "allowNo": false,
9444
+ "type": "boolean"
9445
+ },
9446
+ "yes": {
9447
+ "char": "y",
9448
+ "description": "Skip the confirmation prompt",
9449
+ "name": "yes",
9450
+ "allowNo": false,
9451
+ "type": "boolean"
9444
9452
  }
9445
9453
  },
9446
9454
  "hasDynamicHelp": false,
9447
9455
  "hiddenAliases": [],
9448
- "id": "users:invite",
9456
+ "id": "webhooks:secret",
9449
9457
  "pluginAlias": "@norbix.ai/cli",
9450
9458
  "pluginName": "@norbix.ai/cli",
9451
9459
  "pluginType": "core",
@@ -9455,17 +9463,16 @@
9455
9463
  "relativePath": [
9456
9464
  "dist",
9457
9465
  "commands",
9458
- "users",
9459
- "invite.js"
9466
+ "webhooks",
9467
+ "secret.js"
9460
9468
  ]
9461
9469
  },
9462
- "users:list": {
9470
+ "webhooks:show": {
9463
9471
  "aliases": [],
9464
9472
  "args": {},
9465
- "description": "List project users",
9473
+ "description": "Show the project's webhook integration (destinations, settings)",
9466
9474
  "examples": [
9467
- "<%= config.bin %> users list",
9468
- "<%= config.bin %> users list --role admin --page-size 100"
9475
+ "<%= config.bin %> webhooks show"
9469
9476
  ],
9470
9477
  "flags": {
9471
9478
  "json": {
@@ -9528,30 +9535,95 @@
9528
9535
  "hasDynamicHelp": false,
9529
9536
  "multiple": false,
9530
9537
  "type": "option"
9538
+ }
9539
+ },
9540
+ "hasDynamicHelp": false,
9541
+ "hiddenAliases": [],
9542
+ "id": "webhooks:show",
9543
+ "pluginAlias": "@norbix.ai/cli",
9544
+ "pluginName": "@norbix.ai/cli",
9545
+ "pluginType": "core",
9546
+ "strict": true,
9547
+ "enableJsonFlag": true,
9548
+ "isESM": true,
9549
+ "relativePath": [
9550
+ "dist",
9551
+ "commands",
9552
+ "webhooks",
9553
+ "show.js"
9554
+ ]
9555
+ },
9556
+ "users:block": {
9557
+ "aliases": [],
9558
+ "args": {
9559
+ "id": {
9560
+ "description": "User ID",
9561
+ "name": "id",
9562
+ "required": true
9563
+ }
9564
+ },
9565
+ "description": "Block a user (they can no longer log in)",
9566
+ "examples": [
9567
+ "<%= config.bin %> users block 66b2f0a1..."
9568
+ ],
9569
+ "flags": {
9570
+ "json": {
9571
+ "description": "Format output as json.",
9572
+ "helpGroup": "GLOBAL",
9573
+ "name": "json",
9574
+ "allowNo": false,
9575
+ "type": "boolean"
9531
9576
  },
9532
- "role": {
9533
- "description": "Only users with this role (repeatable)",
9534
- "name": "role",
9577
+ "project": {
9578
+ "description": "Project ID",
9579
+ "env": "NORBIX_PROJECT_ID",
9580
+ "helpGroup": "GLOBAL",
9581
+ "name": "project",
9535
9582
  "hasDynamicHelp": false,
9536
- "multiple": true,
9583
+ "multiple": false,
9537
9584
  "type": "option"
9538
9585
  },
9539
- "permissions": {
9540
- "description": "Include permissions in the output",
9541
- "name": "permissions",
9542
- "allowNo": false,
9543
- "type": "boolean"
9586
+ "env": {
9587
+ "description": "Project environment (e.g. PROD, TEST)",
9588
+ "env": "NORBIX_ENV",
9589
+ "helpGroup": "GLOBAL",
9590
+ "name": "env",
9591
+ "hasDynamicHelp": false,
9592
+ "multiple": false,
9593
+ "type": "option"
9544
9594
  },
9545
- "page-size": {
9546
- "description": "Records per page",
9547
- "name": "page-size",
9595
+ "region": {
9596
+ "description": "Region code (e.g. nb-eu-germany)",
9597
+ "env": "NORBIX_REGION",
9598
+ "helpGroup": "GLOBAL",
9599
+ "name": "region",
9548
9600
  "hasDynamicHelp": false,
9549
9601
  "multiple": false,
9550
9602
  "type": "option"
9551
9603
  },
9552
- "after": {
9553
- "description": "Cursor: fetch the page after this item",
9554
- "name": "after",
9604
+ "api-key": {
9605
+ "description": "API key (overrides profile and session)",
9606
+ "env": "NORBIX_API_KEY",
9607
+ "helpGroup": "GLOBAL",
9608
+ "name": "api-key",
9609
+ "hasDynamicHelp": false,
9610
+ "multiple": false,
9611
+ "type": "option"
9612
+ },
9613
+ "account": {
9614
+ "description": "Account ID (needed for account-scoped hub endpoints)",
9615
+ "env": "NORBIX_ACCOUNT_ID",
9616
+ "helpGroup": "GLOBAL",
9617
+ "name": "account",
9618
+ "hasDynamicHelp": false,
9619
+ "multiple": false,
9620
+ "type": "option"
9621
+ },
9622
+ "profile": {
9623
+ "description": "Use this profile from ~/.norbix/config (ignores the login session)",
9624
+ "env": "NORBIX_PROFILE",
9625
+ "helpGroup": "GLOBAL",
9626
+ "name": "profile",
9555
9627
  "hasDynamicHelp": false,
9556
9628
  "multiple": false,
9557
9629
  "type": "option"
@@ -9559,7 +9631,7 @@
9559
9631
  },
9560
9632
  "hasDynamicHelp": false,
9561
9633
  "hiddenAliases": [],
9562
- "id": "users:list",
9634
+ "id": "users:block",
9563
9635
  "pluginAlias": "@norbix.ai/cli",
9564
9636
  "pluginName": "@norbix.ai/cli",
9565
9637
  "pluginType": "core",
@@ -9570,15 +9642,21 @@
9570
9642
  "dist",
9571
9643
  "commands",
9572
9644
  "users",
9573
- "list.js"
9645
+ "block.js"
9574
9646
  ]
9575
9647
  },
9576
- "users:policies": {
9648
+ "users:delete": {
9577
9649
  "aliases": [],
9578
- "args": {},
9579
- "description": "List membership policies",
9650
+ "args": {
9651
+ "id": {
9652
+ "description": "User ID",
9653
+ "name": "id",
9654
+ "required": true
9655
+ }
9656
+ },
9657
+ "description": "Delete a user",
9580
9658
  "examples": [
9581
- "<%= config.bin %> users policies"
9659
+ "<%= config.bin %> users delete 66b2f0a1... --yes"
9582
9660
  ],
9583
9661
  "flags": {
9584
9662
  "json": {
@@ -9641,11 +9719,18 @@
9641
9719
  "hasDynamicHelp": false,
9642
9720
  "multiple": false,
9643
9721
  "type": "option"
9722
+ },
9723
+ "yes": {
9724
+ "char": "y",
9725
+ "description": "Skip the confirmation prompt",
9726
+ "name": "yes",
9727
+ "allowNo": false,
9728
+ "type": "boolean"
9644
9729
  }
9645
9730
  },
9646
9731
  "hasDynamicHelp": false,
9647
9732
  "hiddenAliases": [],
9648
- "id": "users:policies",
9733
+ "id": "users:delete",
9649
9734
  "pluginAlias": "@norbix.ai/cli",
9650
9735
  "pluginName": "@norbix.ai/cli",
9651
9736
  "pluginType": "core",
@@ -9656,15 +9741,21 @@
9656
9741
  "dist",
9657
9742
  "commands",
9658
9743
  "users",
9659
- "policies.js"
9744
+ "delete.js"
9660
9745
  ]
9661
9746
  },
9662
- "users:roles": {
9747
+ "users:get": {
9663
9748
  "aliases": [],
9664
- "args": {},
9665
- "description": "List membership roles",
9749
+ "args": {
9750
+ "id": {
9751
+ "description": "User ID",
9752
+ "name": "id",
9753
+ "required": true
9754
+ }
9755
+ },
9756
+ "description": "Show one user",
9666
9757
  "examples": [
9667
- "<%= config.bin %> users roles"
9758
+ "<%= config.bin %> users get 66b2f0a1..."
9668
9759
  ],
9669
9760
  "flags": {
9670
9761
  "json": {
@@ -9731,7 +9822,7 @@
9731
9822
  },
9732
9823
  "hasDynamicHelp": false,
9733
9824
  "hiddenAliases": [],
9734
- "id": "users:roles",
9825
+ "id": "users:get",
9735
9826
  "pluginAlias": "@norbix.ai/cli",
9736
9827
  "pluginName": "@norbix.ai/cli",
9737
9828
  "pluginType": "core",
@@ -9742,21 +9833,21 @@
9742
9833
  "dist",
9743
9834
  "commands",
9744
9835
  "users",
9745
- "roles.js"
9836
+ "get.js"
9746
9837
  ]
9747
9838
  },
9748
- "users:unblock": {
9839
+ "users:invite": {
9749
9840
  "aliases": [],
9750
9841
  "args": {
9751
- "id": {
9752
- "description": "User ID",
9753
- "name": "id",
9842
+ "email": {
9843
+ "description": "Email address to invite",
9844
+ "name": "email",
9754
9845
  "required": true
9755
9846
  }
9756
9847
  },
9757
- "description": "Unblock a user",
9848
+ "description": "Invite a user by email",
9758
9849
  "examples": [
9759
- "<%= config.bin %> users unblock 66b2f0a1..."
9850
+ "<%= config.bin %> users invite alice@example.com"
9760
9851
  ],
9761
9852
  "flags": {
9762
9853
  "json": {
@@ -9823,7 +9914,7 @@
9823
9914
  },
9824
9915
  "hasDynamicHelp": false,
9825
9916
  "hiddenAliases": [],
9826
- "id": "users:unblock",
9917
+ "id": "users:invite",
9827
9918
  "pluginAlias": "@norbix.ai/cli",
9828
9919
  "pluginName": "@norbix.ai/cli",
9829
9920
  "pluginType": "core",
@@ -9834,21 +9925,16 @@
9834
9925
  "dist",
9835
9926
  "commands",
9836
9927
  "users",
9837
- "unblock.js"
9928
+ "invite.js"
9838
9929
  ]
9839
9930
  },
9840
- "webhooks:disable": {
9931
+ "users:list": {
9841
9932
  "aliases": [],
9842
- "args": {
9843
- "destinationId": {
9844
- "description": "Destination ID",
9845
- "name": "destinationId",
9846
- "required": true
9847
- }
9848
- },
9849
- "description": "Disable a webhook destination",
9933
+ "args": {},
9934
+ "description": "List project users",
9850
9935
  "examples": [
9851
- "<%= config.bin %> webhooks disable 66b2f0a1..."
9936
+ "<%= config.bin %> users list",
9937
+ "<%= config.bin %> users list --role admin --page-size 100"
9852
9938
  ],
9853
9939
  "flags": {
9854
9940
  "json": {
@@ -9911,11 +9997,38 @@
9911
9997
  "hasDynamicHelp": false,
9912
9998
  "multiple": false,
9913
9999
  "type": "option"
10000
+ },
10001
+ "role": {
10002
+ "description": "Only users with this role (repeatable)",
10003
+ "name": "role",
10004
+ "hasDynamicHelp": false,
10005
+ "multiple": true,
10006
+ "type": "option"
10007
+ },
10008
+ "permissions": {
10009
+ "description": "Include permissions in the output",
10010
+ "name": "permissions",
10011
+ "allowNo": false,
10012
+ "type": "boolean"
10013
+ },
10014
+ "page-size": {
10015
+ "description": "Records per page",
10016
+ "name": "page-size",
10017
+ "hasDynamicHelp": false,
10018
+ "multiple": false,
10019
+ "type": "option"
10020
+ },
10021
+ "after": {
10022
+ "description": "Cursor: fetch the page after this item",
10023
+ "name": "after",
10024
+ "hasDynamicHelp": false,
10025
+ "multiple": false,
10026
+ "type": "option"
9914
10027
  }
9915
10028
  },
9916
10029
  "hasDynamicHelp": false,
9917
10030
  "hiddenAliases": [],
9918
- "id": "webhooks:disable",
10031
+ "id": "users:list",
9919
10032
  "pluginAlias": "@norbix.ai/cli",
9920
10033
  "pluginName": "@norbix.ai/cli",
9921
10034
  "pluginType": "core",
@@ -9925,22 +10038,16 @@
9925
10038
  "relativePath": [
9926
10039
  "dist",
9927
10040
  "commands",
9928
- "webhooks",
9929
- "disable.js"
10041
+ "users",
10042
+ "list.js"
9930
10043
  ]
9931
10044
  },
9932
- "webhooks:enable": {
10045
+ "users:policies": {
9933
10046
  "aliases": [],
9934
- "args": {
9935
- "destinationId": {
9936
- "description": "Destination ID",
9937
- "name": "destinationId",
9938
- "required": true
9939
- }
9940
- },
9941
- "description": "Enable a webhook destination",
10047
+ "args": {},
10048
+ "description": "List membership policies",
9942
10049
  "examples": [
9943
- "<%= config.bin %> webhooks enable 66b2f0a1..."
10050
+ "<%= config.bin %> users policies"
9944
10051
  ],
9945
10052
  "flags": {
9946
10053
  "json": {
@@ -10007,7 +10114,7 @@
10007
10114
  },
10008
10115
  "hasDynamicHelp": false,
10009
10116
  "hiddenAliases": [],
10010
- "id": "webhooks:enable",
10117
+ "id": "users:policies",
10011
10118
  "pluginAlias": "@norbix.ai/cli",
10012
10119
  "pluginName": "@norbix.ai/cli",
10013
10120
  "pluginType": "core",
@@ -10017,22 +10124,16 @@
10017
10124
  "relativePath": [
10018
10125
  "dist",
10019
10126
  "commands",
10020
- "webhooks",
10021
- "enable.js"
10127
+ "users",
10128
+ "policies.js"
10022
10129
  ]
10023
10130
  },
10024
- "webhooks:remove": {
10131
+ "users:roles": {
10025
10132
  "aliases": [],
10026
- "args": {
10027
- "destinationId": {
10028
- "description": "Destination ID",
10029
- "name": "destinationId",
10030
- "required": true
10031
- }
10032
- },
10033
- "description": "Remove a webhook destination",
10133
+ "args": {},
10134
+ "description": "List membership roles",
10034
10135
  "examples": [
10035
- "<%= config.bin %> webhooks remove 66b2f0a1... --yes"
10136
+ "<%= config.bin %> users roles"
10036
10137
  ],
10037
10138
  "flags": {
10038
10139
  "json": {
@@ -10095,18 +10196,11 @@
10095
10196
  "hasDynamicHelp": false,
10096
10197
  "multiple": false,
10097
10198
  "type": "option"
10098
- },
10099
- "yes": {
10100
- "char": "y",
10101
- "description": "Skip the confirmation prompt",
10102
- "name": "yes",
10103
- "allowNo": false,
10104
- "type": "boolean"
10105
10199
  }
10106
10200
  },
10107
10201
  "hasDynamicHelp": false,
10108
10202
  "hiddenAliases": [],
10109
- "id": "webhooks:remove",
10203
+ "id": "users:roles",
10110
10204
  "pluginAlias": "@norbix.ai/cli",
10111
10205
  "pluginName": "@norbix.ai/cli",
10112
10206
  "pluginType": "core",
@@ -10116,17 +10210,22 @@
10116
10210
  "relativePath": [
10117
10211
  "dist",
10118
10212
  "commands",
10119
- "webhooks",
10120
- "remove.js"
10213
+ "users",
10214
+ "roles.js"
10121
10215
  ]
10122
10216
  },
10123
- "webhooks:secret": {
10217
+ "users:unblock": {
10124
10218
  "aliases": [],
10125
- "args": {},
10126
- "description": "Reveal the webhook signing secret, or rotate it with --rotate",
10219
+ "args": {
10220
+ "id": {
10221
+ "description": "User ID",
10222
+ "name": "id",
10223
+ "required": true
10224
+ }
10225
+ },
10226
+ "description": "Unblock a user",
10127
10227
  "examples": [
10128
- "<%= config.bin %> webhooks secret",
10129
- "<%= config.bin %> webhooks secret --rotate --yes"
10228
+ "<%= config.bin %> users unblock 66b2f0a1..."
10130
10229
  ],
10131
10230
  "flags": {
10132
10231
  "json": {
@@ -10189,24 +10288,11 @@
10189
10288
  "hasDynamicHelp": false,
10190
10289
  "multiple": false,
10191
10290
  "type": "option"
10192
- },
10193
- "rotate": {
10194
- "description": "Rotate the secret (old secret stops working)",
10195
- "name": "rotate",
10196
- "allowNo": false,
10197
- "type": "boolean"
10198
- },
10199
- "yes": {
10200
- "char": "y",
10201
- "description": "Skip the confirmation prompt",
10202
- "name": "yes",
10203
- "allowNo": false,
10204
- "type": "boolean"
10205
10291
  }
10206
10292
  },
10207
10293
  "hasDynamicHelp": false,
10208
10294
  "hiddenAliases": [],
10209
- "id": "webhooks:secret",
10295
+ "id": "users:unblock",
10210
10296
  "pluginAlias": "@norbix.ai/cli",
10211
10297
  "pluginName": "@norbix.ai/cli",
10212
10298
  "pluginType": "core",
@@ -10216,16 +10302,23 @@
10216
10302
  "relativePath": [
10217
10303
  "dist",
10218
10304
  "commands",
10219
- "webhooks",
10220
- "secret.js"
10305
+ "users",
10306
+ "unblock.js"
10221
10307
  ]
10222
10308
  },
10223
- "webhooks:show": {
10309
+ "files:integrations:test": {
10224
10310
  "aliases": [],
10225
- "args": {},
10226
- "description": "Show the project's webhook integration (destinations, settings)",
10311
+ "args": {
10312
+ "id": {
10313
+ "description": "Files integration ID",
10314
+ "name": "id",
10315
+ "required": true
10316
+ }
10317
+ },
10318
+ "description": "Check that a files integration really works\n\nRuns a live probe against the storage: uploads a small file, reads it, lists\nits folder and deletes it again. Prints one line per step — OK, FAILED or\nNOT_TESTED (skipped because an earlier step failed) — with the errors of a\nfailed step. Exits non-zero unless every step is OK.\n\nNeeds the files:create permission, because the probe writes to the storage.",
10227
10319
  "examples": [
10228
- "<%= config.bin %> webhooks show"
10320
+ "<%= config.bin %> files integrations test 55555555-5555-5555-5555-555555555555",
10321
+ "<%= config.bin %> files integrations test 55555555-5555-5555-5555-555555555555 --json"
10229
10322
  ],
10230
10323
  "flags": {
10231
10324
  "json": {
@@ -10292,7 +10385,7 @@
10292
10385
  },
10293
10386
  "hasDynamicHelp": false,
10294
10387
  "hiddenAliases": [],
10295
- "id": "webhooks:show",
10388
+ "id": "files:integrations:test",
10296
10389
  "pluginAlias": "@norbix.ai/cli",
10297
10390
  "pluginName": "@norbix.ai/cli",
10298
10391
  "pluginType": "core",
@@ -10302,8 +10395,9 @@
10302
10395
  "relativePath": [
10303
10396
  "dist",
10304
10397
  "commands",
10305
- "webhooks",
10306
- "show.js"
10398
+ "files",
10399
+ "integrations",
10400
+ "test.js"
10307
10401
  ]
10308
10402
  },
10309
10403
  "push:device:register": {
@@ -10934,5 +11028,5 @@
10934
11028
  ]
10935
11029
  }
10936
11030
  },
10937
- "version": "0.2.0"
11031
+ "version": "1.0.0"
10938
11032
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norbix.ai/cli",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Norbix CLI — manage your Norbix projects from the terminal. Built on the official TypeScript SDK.",
5
5
  "author": "UAB Isidos",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@inquirer/prompts": "^8.7.2",
50
- "@norbix.ai/ts": "^1.3.0",
50
+ "@norbix.ai/ts": "^2.0.0",
51
51
  "@oclif/core": "^5.0.0",
52
52
  "@oclif/plugin-autocomplete": "^4.0.0",
53
53
  "@oclif/plugin-help": "^7.0.0"
@@ -92,7 +92,12 @@
92
92
  "description": "Work with database collections (find, insert, update, delete)"
93
93
  },
94
94
  "files": {
95
- "description": "Upload, download and manage files"
95
+ "description": "Upload, download and manage files",
96
+ "subtopics": {
97
+ "integrations": {
98
+ "description": "Check the storage behind a files integration"
99
+ }
100
+ }
96
101
  },
97
102
  "env": {
98
103
  "description": "Manage project environments (PROD, TEST, ...)"