@bussolabs/closeyourit-cli 0.24.1 → 0.24.2

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
@@ -272,6 +272,9 @@ CLOSEYOURIT_TOKEN=cyi_u_… CLOSEYOURIT_API_URL=https://www.closeyour.it \
272
272
  | `workload promote <action-id> --project <id\|key> [--title] [--description] [--kind] [--status] [--priority]` | Generate a ticket from the action and link it back. |
273
273
 
274
274
  Every command accepts `--json` for machine-readable output and `--help` for usage details.
275
+ Every command also accepts `--yes`: the server refuses a write that goes through a dangerous
276
+ permission (deleting a project, importing or deleting secrets, …) with `R422-CONFIRM-001` until you
277
+ confirm it, and `--yes` is that confirmation. Reads never need it.
275
278
  `--project` accepts either a project UUID or its key (matched case-insensitively).
276
279
 
277
280
  `kb publish` needs a backend exposing the atomic Knowledge publication route; an older one fails with
package/dist/base.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command, type Interfaces } from '@oclif/core';
2
+ import type { ArgOutput, FlagOutput, Input, ParserOutput } from '@oclif/core/lib/interfaces/parser';
2
3
  import { type CliConfig } from './lib/config';
3
4
  import { CliApi } from './lib/api';
4
5
  import { type TicketLookup } from './lib/ticket-lookup';
@@ -28,9 +29,20 @@ export declare function paginationError(opts: {
28
29
  }): string | undefined;
29
30
  export declare abstract class BaseCommand extends Command {
30
31
  static enableJsonFlag: boolean;
32
+ /**
33
+ * `--yes` on every command (CYCL-53): the server asks an explicit confirmation for writes that pass
34
+ * through a dangerous permission key (`R422-CONFIRM-001`, CYRA-728). Which keys are dangerous is
35
+ * the server's catalogue, not ours, so the flag is global instead of being sprinkled on the
36
+ * commands we believe are risky today. Harmless elsewhere: the parameter is simply ignored.
37
+ */
38
+ static baseFlags: {
39
+ yes: Interfaces.BooleanFlag<boolean>;
40
+ };
31
41
  protected cfg: CliConfig;
32
42
  protected api: CliApi;
33
43
  init(): Promise<void>;
44
+ /** Parse as oclif does, then hand `--yes` to the api client before any request leaves. */
45
+ protected parse<F extends FlagOutput, B extends FlagOutput, A extends ArgOutput>(options?: Input<F, B, A>, argv?: string[]): Promise<ParserOutput<F, B, A>>;
34
46
  /**
35
47
  * Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive).
36
48
  * Walks every page of `/cli/v1/projects`: a key can live past the first page, so a single-page
package/dist/base.js CHANGED
@@ -38,6 +38,15 @@ function paginationError(opts) {
38
38
  }
39
39
  class BaseCommand extends core_1.Command {
40
40
  static enableJsonFlag = true;
41
+ /**
42
+ * `--yes` on every command (CYCL-53): the server asks an explicit confirmation for writes that pass
43
+ * through a dangerous permission key (`R422-CONFIRM-001`, CYRA-728). Which keys are dangerous is
44
+ * the server's catalogue, not ours, so the flag is global instead of being sprinkled on the
45
+ * commands we believe are risky today. Harmless elsewhere: the parameter is simply ignored.
46
+ */
47
+ static baseFlags = {
48
+ yes: core_1.Flags.boolean({ description: 'Confirm a dangerous action the server would otherwise refuse (R422-CONFIRM-001)', default: false }),
49
+ };
41
50
  cfg;
42
51
  api;
43
52
  async init() {
@@ -45,6 +54,13 @@ class BaseCommand extends core_1.Command {
45
54
  this.cfg = (0, config_1.loadConfig)();
46
55
  this.api = new api_1.CliApi(this.cfg);
47
56
  }
57
+ /** Parse as oclif does, then hand `--yes` to the api client before any request leaves. */
58
+ async parse(options, argv) {
59
+ const parsed = await super.parse(options, argv);
60
+ if (this.api)
61
+ this.api.confirm = parsed.flags.yes === true;
62
+ return parsed;
63
+ }
48
64
  /**
49
65
  * Resolve a project reference (UUID passes through; otherwise matched by key, case-insensitive).
50
66
  * Walks every page of `/cli/v1/projects`: a key can live past the first page, so a single-page
package/dist/lib/api.d.ts CHANGED
@@ -23,6 +23,8 @@ export interface UploadFile {
23
23
  data: Blob;
24
24
  }
25
25
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
26
+ /** Error code the server answers when a dangerous write arrives without the confirm gesture (CYRA-728). */
27
+ export declare const CONFIRMATION_REQUIRED_CODE = "R422-CONFIRM-001";
26
28
  export declare class CliApi {
27
29
  private readonly config;
28
30
  /**
@@ -32,6 +34,14 @@ export declare class CliApi {
32
34
  * commands (CYCL-11). The instance is per-command, so this is that command's last response.
33
35
  */
34
36
  lastMeta?: Record<string, unknown>;
37
+ /**
38
+ * The user confirmed dangerous actions (`--yes`, CYCL-53). The server (CYRA-728) refuses any
39
+ * write through a `dangerous` permission key without the `confirm` parameter and answers
40
+ * `R422-CONFIRM-001`: not a permission problem, a missing gesture. `BaseCommand.parse` sets it;
41
+ * `request()` then adds `confirm=1` to every writing call — in the query string, so DELETE
42
+ * without a body is covered too. Reads never carry it: looking is not executing.
43
+ */
44
+ confirm: boolean;
35
45
  constructor(config: CliConfig);
36
46
  request<T = unknown>(method: HttpMethod, path: string, opts?: RequestOptions): Promise<Envelope<T>>;
37
47
  /**
@@ -39,6 +49,8 @@ export declare class CliApi {
39
49
  * The boundary Content-Type is derived by fetch from the FormData body — never set it by hand.
40
50
  */
41
51
  upload<T = unknown>(method: 'POST' | 'PUT', path: string, files: UploadFile[], opts?: RequestOptions): Promise<Envelope<T>>;
52
+ /** Add `confirm=1` to a writing request when the user passed `--yes`; reads are left alone. */
53
+ private withConfirmation;
42
54
  private resolveUrl;
43
55
  /** Build the shared Accept + Authorization headers; throws (before any fetch) if an authed call has no token. */
44
56
  private authHeaders;
package/dist/lib/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CliApi = exports.ApiRequestError = void 0;
3
+ exports.CliApi = exports.CONFIRMATION_REQUIRED_CODE = exports.ApiRequestError = void 0;
4
4
  const error_codes_1 = require("../errors/error-codes");
5
5
  /** Error carrying the HTTP status and the structured code from the backend envelope (R/G prefix) or a local C-prefix fallback. */
6
6
  class ApiRequestError extends Error {
@@ -16,6 +16,9 @@ class ApiRequestError extends Error {
16
16
  }
17
17
  }
18
18
  exports.ApiRequestError = ApiRequestError;
19
+ /** Error code the server answers when a dangerous write arrives without the confirm gesture (CYRA-728). */
20
+ exports.CONFIRMATION_REQUIRED_CODE = 'R422-CONFIRM-001';
21
+ const READ_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
19
22
  class CliApi {
20
23
  config;
21
24
  /**
@@ -25,13 +28,21 @@ class CliApi {
25
28
  * commands (CYCL-11). The instance is per-command, so this is that command's last response.
26
29
  */
27
30
  lastMeta;
31
+ /**
32
+ * The user confirmed dangerous actions (`--yes`, CYCL-53). The server (CYRA-728) refuses any
33
+ * write through a `dangerous` permission key without the `confirm` parameter and answers
34
+ * `R422-CONFIRM-001`: not a permission problem, a missing gesture. `BaseCommand.parse` sets it;
35
+ * `request()` then adds `confirm=1` to every writing call — in the query string, so DELETE
36
+ * without a body is covered too. Reads never carry it: looking is not executing.
37
+ */
38
+ confirm = false;
28
39
  constructor(config) {
29
40
  this.config = config;
30
41
  }
31
42
  async request(method, path, opts = {}) {
32
43
  const headers = this.authHeaders(opts.auth !== false);
33
44
  headers['Content-Type'] = 'application/json';
34
- const url = this.resolveUrl(path);
45
+ const url = this.resolveUrl(this.withConfirmation(method, path));
35
46
  let res;
36
47
  try {
37
48
  res = await fetch(url, {
@@ -70,6 +81,12 @@ class CliApi {
70
81
  }
71
82
  return this.handleResponse(res);
72
83
  }
84
+ /** Add `confirm=1` to a writing request when the user passed `--yes`; reads are left alone. */
85
+ withConfirmation(method, path) {
86
+ if (!this.confirm || READ_METHODS.has(method))
87
+ return path;
88
+ return `${path}${path.includes('?') ? '&' : '?'}confirm=1`;
89
+ }
73
90
  resolveUrl(path) {
74
91
  return this.config.apiUrl.replace(/\/+$/, '') + path;
75
92
  }
@@ -102,7 +119,12 @@ class CliApi {
102
119
  if (!res.ok) {
103
120
  const envelope = (json ?? {});
104
121
  const error = envelope.error ?? {};
105
- throw new ApiRequestError(res.status, error.code ?? `C${res.status}-API-000`, error.message ?? res.statusText ?? `Request failed (${res.status})`, error.details);
122
+ let message = error.message ?? res.statusText ?? `Request failed (${res.status})`;
123
+ // The server names the gesture it wants but not the flag that makes it: say it here, once.
124
+ if (error.code === exports.CONFIRMATION_REQUIRED_CODE && !message.includes('--yes')) {
125
+ message = `${message} Re-run with --yes to confirm this dangerous action.`;
126
+ }
127
+ throw new ApiRequestError(res.status, error.code ?? `C${res.status}-API-000`, message, error.details);
106
128
  }
107
129
  const envelope = (json ?? {});
108
130
  this.lastMeta = envelope.meta;