@xano/cli 1.3.0 → 1.3.1-beta.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 (96) hide show
  1. package/README.md +269 -1
  2. package/dist/base-command.d.ts +12 -1
  3. package/dist/base-command.js +36 -22
  4. package/dist/commands/branch/set_live/index.d.ts +22 -0
  5. package/dist/commands/branch/set_live/index.js +60 -1
  6. package/dist/commands/ephemeral/push/index.js +6 -1
  7. package/dist/commands/function/create/index.d.ts +15 -0
  8. package/dist/commands/function/create/index.js +40 -0
  9. package/dist/commands/function/edit/index.d.ts +15 -0
  10. package/dist/commands/function/edit/index.js +40 -0
  11. package/dist/commands/policy/catalogue/index.d.ts +15 -0
  12. package/dist/commands/policy/catalogue/index.js +29 -0
  13. package/dist/commands/policy/create/index.d.ts +18 -0
  14. package/dist/commands/policy/create/index.js +39 -0
  15. package/dist/commands/policy/delete/index.d.ts +19 -0
  16. package/dist/commands/policy/delete/index.js +61 -0
  17. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  18. package/dist/commands/policy/evaluate/index.js +71 -0
  19. package/dist/commands/policy/list/index.d.ts +14 -0
  20. package/dist/commands/policy/list/index.js +21 -0
  21. package/dist/commands/policy/parse/index.d.ts +19 -0
  22. package/dist/commands/policy/parse/index.js +18 -0
  23. package/dist/commands/policy/publish/index.d.ts +20 -0
  24. package/dist/commands/policy/publish/index.js +38 -0
  25. package/dist/commands/policy/runs/index.d.ts +54 -0
  26. package/dist/commands/policy/runs/index.js +273 -0
  27. package/dist/commands/policy/status/index.d.ts +21 -0
  28. package/dist/commands/policy/status/index.js +54 -0
  29. package/dist/commands/release/create/index.js +7 -0
  30. package/dist/commands/release/deploy/index.d.ts +28 -0
  31. package/dist/commands/release/deploy/index.js +90 -6
  32. package/dist/commands/release/import/index.js +5 -1
  33. package/dist/commands/release/list/index.d.ts +7 -0
  34. package/dist/commands/release/list/index.js +54 -10
  35. package/dist/commands/release/push/index.js +19 -1
  36. package/dist/commands/sandbox/push/index.js +6 -1
  37. package/dist/commands/skills/pull/index.d.ts +30 -0
  38. package/dist/commands/skills/pull/index.js +118 -0
  39. package/dist/commands/tenant/create/index.d.ts +3 -0
  40. package/dist/commands/tenant/create/index.js +21 -0
  41. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  42. package/dist/commands/tenant/deploy_release/index.js +186 -4
  43. package/dist/commands/tenant/edit/index.d.ts +2 -0
  44. package/dist/commands/tenant/edit/index.js +39 -4
  45. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  46. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  47. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  48. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  49. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  50. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  51. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  52. package/dist/commands/tenant_deploy_request/get/index.js +116 -0
  53. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  54. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  55. package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
  56. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  57. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  58. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  59. package/dist/commands/workspace/pull/index.d.ts +10 -0
  60. package/dist/commands/workspace/pull/index.js +70 -7
  61. package/dist/commands/workspace/push/index.d.ts +19 -0
  62. package/dist/commands/workspace/push/index.js +124 -5
  63. package/dist/policy-command.d.ts +90 -0
  64. package/dist/policy-command.js +94 -0
  65. package/dist/utils/document-parser.d.ts +10 -0
  66. package/dist/utils/document-parser.js +21 -0
  67. package/dist/utils/multidoc-push.d.ts +55 -2
  68. package/dist/utils/multidoc-push.js +103 -27
  69. package/dist/utils/policy/catalogue.d.ts +7 -0
  70. package/dist/utils/policy/catalogue.js +72 -0
  71. package/dist/utils/policy/errors.d.ts +12 -0
  72. package/dist/utils/policy/errors.js +61 -0
  73. package/dist/utils/policy/feedback.d.ts +68 -0
  74. package/dist/utils/policy/feedback.js +165 -0
  75. package/dist/utils/policy/findings.d.ts +23 -0
  76. package/dist/utils/policy/findings.js +65 -0
  77. package/dist/utils/policy/gate.d.ts +161 -0
  78. package/dist/utils/policy/gate.js +201 -0
  79. package/dist/utils/policy/goal.d.ts +46 -0
  80. package/dist/utils/policy/goal.js +95 -0
  81. package/dist/utils/policy/permission.d.ts +23 -0
  82. package/dist/utils/policy/permission.js +84 -0
  83. package/dist/utils/policy/push-gate.d.ts +5 -0
  84. package/dist/utils/policy/push-gate.js +29 -0
  85. package/dist/utils/policy/release.d.ts +85 -0
  86. package/dist/utils/policy/release.js +115 -0
  87. package/dist/utils/policy/request.d.ts +55 -0
  88. package/dist/utils/policy/request.js +115 -0
  89. package/dist/utils/policy/runs.d.ts +30 -0
  90. package/dist/utils/policy/runs.js +101 -0
  91. package/dist/utils/policy/status.d.ts +47 -0
  92. package/dist/utils/policy/status.js +117 -0
  93. package/dist/utils/policy/types.d.ts +185 -0
  94. package/dist/utils/policy/types.js +1 -0
  95. package/oclif.manifest.json +6132 -3133
  96. package/package.json +7 -1
@@ -1,5 +1,7 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import BaseCommand from '../../../base-command.js';
3
+ import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateOverrideHint, gateRefusal, liveGateLines, quoted, weakeningRefusal, } from '../../../utils/policy/gate.js';
4
+ import { policyWeakeningGuidance } from '../../../utils/policy/permission.js';
3
5
  export default class BranchSetLive extends BaseCommand {
4
6
  static args = {
5
7
  branch_label: Args.string({
@@ -7,7 +9,7 @@ export default class BranchSetLive extends BaseCommand {
7
9
  required: true,
8
10
  }),
9
11
  };
10
- static description = '[IMPORTANT] ALWAYS confirm with the user before changing the live branch. Sets a branch as the live (active) branch for API requests.';
12
+ static description = '[IMPORTANT] ALWAYS confirm with the user before changing the live branch. Sets a branch as the live (active) branch for API requests. The set-live policy gate checks the branch against its own policies, which live will have once it is set live: when blocking findings refuse it, the command exits 2 unless --policy-override gives a reason. A branch that weakens a mandatory policy of the live branch needs workspace:policy update; without it the command exits 1.';
11
13
  static examples = [
12
14
  `$ xano branch set-live staging
13
15
  Are you sure you want to set 'staging' as the live branch? (y/N) y
@@ -24,6 +26,7 @@ Branch 'v1' is now live
24
26
  "live": true
25
27
  }
26
28
  `,
29
+ `$ xano branch set_live rollback --force --policy-override "Rollback approved; finding tracked in JIRA-12"`,
27
30
  ];
28
31
  static flags = {
29
32
  ...BaseCommand.baseFlags,
@@ -40,14 +43,27 @@ Branch 'v1' is now live
40
43
  options: ['summary', 'json'],
41
44
  required: false,
42
45
  }),
46
+ 'policy-override': Flags.string({ description: 'Set the branch live past a blocking set-live policy gate with an audited reason (requires workspace:policy update)' }),
43
47
  workspace: Flags.integer({
44
48
  char: 'w',
45
49
  description: 'Workspace ID (uses profile workspace if not provided)',
46
50
  required: false,
47
51
  }),
48
52
  };
53
+ /** Set once the platform refuses for a weakened mandatory policy: already explained, it exits 1 as it is. */
54
+ refusedByPermission = false;
55
+ /** Set once the set-live policy gate refuses, the one failure that exits 2. */
56
+ refusedByPolicyGate = false;
57
+ /** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
58
+ async catch(error) {
59
+ if (this.refusedByPolicyGate)
60
+ return super.catch(error);
61
+ return this.catchAsOperational(error);
62
+ }
49
63
  async run() {
50
64
  const { args, flags } = await this.parse(BranchSetLive);
65
+ if (blankPolicyOverride(flags['policy-override']))
66
+ this.error(BLANK_POLICY_OVERRIDE);
51
67
  const { profile } = this.resolveProfile(flags);
52
68
  // Get workspace ID from flag or profile
53
69
  const workspaceId = flags.workspace || profile.workspace;
@@ -66,16 +82,26 @@ Branch 'v1' is now live
66
82
  }
67
83
  // Construct the API URL
68
84
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/branch/${encodeURIComponent(branchLabel)}/live`;
85
+ // A reason from someone with workspace:policy update sets the branch live past a blocking gate.
86
+ const overrideReason = flags['policy-override']?.trim();
69
87
  // Set branch as live via the API
70
88
  try {
71
89
  const response = await this.verboseFetch(apiUrl, {
90
+ ...(overrideReason ? { body: JSON.stringify({ override_reason: overrideReason }) } : {}),
72
91
  headers: {
73
92
  'accept': 'application/json',
74
93
  'Authorization': `Bearer ${profile.access_token}`,
94
+ ...(overrideReason ? { 'Content-Type': 'application/json' } : {}),
75
95
  },
76
96
  method: 'POST',
77
97
  }, flags.verbose, profile.access_token);
78
98
  if (!response.ok) {
99
+ const refused = await gateRefusal(response);
100
+ if (refused)
101
+ this.refuseSetLive(refused, flags, branchLabel);
102
+ const weakening = await weakeningRefusal(response);
103
+ if (weakening)
104
+ this.refuseWeakening(weakening, flags, branchLabel);
79
105
  const errorText = await response.text();
80
106
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
81
107
  }
@@ -89,6 +115,8 @@ Branch 'v1' is now live
89
115
  }
90
116
  }
91
117
  catch (error) {
118
+ if (this.refusedByPolicyGate || this.refusedByPermission)
119
+ throw error;
92
120
  if (error instanceof Error) {
93
121
  this.error(`Failed to set branch as live: ${error.message}`);
94
122
  }
@@ -111,4 +139,35 @@ Branch 'v1' is now live
111
139
  });
112
140
  });
113
141
  }
142
+ /**
143
+ * The set-live gate refused: the verdict on stdout (under `-o json`, `{set_live: false, message,
144
+ * policy_gate}`), then the platform's message and how to proceed, exiting 2.
145
+ */
146
+ refuseSetLive(refused, flags, branchLabel) {
147
+ if (flags.output === 'json') {
148
+ this.log(JSON.stringify({ message: refused.message, policy_gate: refused.answer, set_live: false }, null, 2));
149
+ }
150
+ else {
151
+ for (const line of liveGateLines(refused.answer, 'branch'))
152
+ this.log(line);
153
+ }
154
+ const workspace = flags.workspace ? ` -w ${flags.workspace}` : '';
155
+ const hint = gateOverrideHint(refused.answer, `xano branch set_live ${quoted(branchLabel)}${workspace} --policy-override "<why>"`);
156
+ this.refusedByPolicyGate = true;
157
+ this.error(`${refused.message}${hint}`, { exit: 2 });
158
+ }
159
+ /**
160
+ * The branch weakens a mandatory policy of the live branch (or lacks one), and the credential lacks
161
+ * `workspace:policy` update. That is a permission refusal, not a blocking finding: it exits 1 (under
162
+ * `-o json` as `{error}`) with the platform's sentence and how to go on. The live branch is unchanged.
163
+ */
164
+ refuseWeakening(refused, flags, branchLabel) {
165
+ const workspace = flags.workspace ? ` -w ${flags.workspace}` : '';
166
+ this.refusedByPermission = true;
167
+ const again = `xano branch set_live ${quoted(branchLabel)}${workspace}`;
168
+ const way = refused.payload.unavailable === true
169
+ ? `The check may read them next time: ${again}`
170
+ : `To set it live yourself, give ${quoted(branchLabel)} the live branch's version of those policies, then run: ${again}`;
171
+ this.error(`${refused.message}${policyWeakeningGuidance(refused.payload, [way], true)}`);
172
+ }
114
173
  }
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
4
4
  import BaseCommand from '../../../base-command.js';
5
5
  import { isAwaited, waitForMicroservices, } from '../../../utils/microservice-wait.js';
6
6
  import { executePush } from '../../../utils/multidoc-push.js';
7
+ import { policiesSkippedNotice } from '../../../utils/policy/feedback.js';
7
8
  /** Status glyph for one microservice row in the `--wait` progress block. */
8
9
  const icon = (e) => {
9
10
  if (!isAwaited(e))
@@ -168,13 +169,17 @@ Skip preview and push immediately
168
169
  truncate: flags.truncate,
169
170
  verbose: flags.verbose,
170
171
  };
171
- await executePush({
172
+ const result = await executePush({
172
173
  accessToken: profile.access_token,
173
174
  branch: '',
174
175
  command: this,
175
176
  inputDir,
176
177
  verboseFetch: this.verboseFetch.bind(this),
177
178
  }, target, pushFlags);
179
+ // When the platform leaves policy files out, it says so once.
180
+ const skipped = policiesSkippedNotice(result.response ?? result.preview);
181
+ if (skipped)
182
+ this.warn(skipped);
178
183
  // --wait: after a real push, poll the tenant's live microservice status until
179
184
  // every auto-deployed microservice (tenant_deploy="auto") is ready or the
180
185
  // wait times out. Skipped on --dry-run (nothing was deployed).
@@ -7,13 +7,28 @@ export default class FunctionCreate extends BaseCommand {
7
7
  edit: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
8
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
9
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
12
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
16
  };
17
+ /** Set once the publish policy gate refuses, the one failure that exits 2. */
18
+ private refusedByPolicyGate;
19
+ /** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
20
+ protected catch(error: Error & {
21
+ oclif?: {
22
+ exit?: number;
23
+ };
24
+ }): Promise<void>;
16
25
  run(): Promise<void>;
17
26
  private editFile;
18
27
  private readStdin;
28
+ /**
29
+ * The live-branch publish gate refused the new function: the verdict on stdout (under `-o json`,
30
+ * `{created: false, message, policy_gate}`), then the platform's message and how to proceed,
31
+ * exiting 2.
32
+ */
33
+ private refusePublish;
19
34
  }
@@ -4,6 +4,7 @@ import * as fs from 'node:fs';
4
4
  import * as os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import BaseCommand from '../../../base-command.js';
7
+ import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateRefusal, publishRefusal, quoted } from '../../../utils/policy/gate.js';
7
8
  export default class FunctionCreate extends BaseCommand {
8
9
  static args = {};
9
10
  static description = 'Create a new function in a workspace';
@@ -36,6 +37,7 @@ Name: my_function
36
37
  ...
37
38
  }
38
39
  `,
40
+ `$ xano function:create -f function.xs --policy-override "Exception approved; finding tracked in JIRA-12"`,
39
41
  ];
40
42
  static flags = {
41
43
  ...BaseCommand.baseFlags,
@@ -59,6 +61,7 @@ Name: my_function
59
61
  options: ['summary', 'json'],
60
62
  required: false,
61
63
  }),
64
+ 'policy-override': Flags.string({ description: 'Create the function past a blocking live-branch publish policy gate with an audited reason (requires workspace:policy update)' }),
62
65
  stdin: Flags.boolean({
63
66
  char: 's',
64
67
  default: false,
@@ -72,8 +75,18 @@ Name: my_function
72
75
  required: false,
73
76
  }),
74
77
  };
78
+ /** Set once the publish policy gate refuses, the one failure that exits 2. */
79
+ refusedByPolicyGate = false;
80
+ /** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
81
+ async catch(error) {
82
+ if (this.refusedByPolicyGate)
83
+ return super.catch(error);
84
+ return this.catchAsOperational(error);
85
+ }
75
86
  async run() {
76
87
  const { flags } = await this.parse(FunctionCreate);
88
+ if (blankPolicyOverride(flags['policy-override']))
89
+ this.error(BLANK_POLICY_OVERRIDE);
77
90
  const { profile, profileName } = this.resolveProfile(flags);
78
91
  // Determine workspace_id from flag or profile
79
92
  let workspaceId;
@@ -133,6 +146,10 @@ Name: my_function
133
146
  const queryParams = new URLSearchParams({
134
147
  include_xanoscript: 'false',
135
148
  });
149
+ // A reason from someone with workspace:policy update saves past a blocking publish gate.
150
+ const overrideReason = flags['policy-override']?.trim();
151
+ if (overrideReason)
152
+ queryParams.set('override_reason', overrideReason);
136
153
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
137
154
  // Create function via API
138
155
  try {
@@ -146,6 +163,9 @@ Name: my_function
146
163
  method: 'POST',
147
164
  }, flags.verbose, profile.access_token);
148
165
  if (!response.ok) {
166
+ const refused = await gateRefusal(response);
167
+ if (refused)
168
+ this.refusePublish(refused, flags);
149
169
  const errorText = await response.text();
150
170
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
151
171
  }
@@ -166,6 +186,8 @@ Name: my_function
166
186
  }
167
187
  }
168
188
  catch (error) {
189
+ if (this.refusedByPolicyGate)
190
+ throw error;
169
191
  if (error instanceof Error) {
170
192
  this.error(`Failed to create function: ${error.message}`);
171
193
  }
@@ -239,4 +261,22 @@ Name: my_function
239
261
  process.stdin.resume();
240
262
  });
241
263
  }
264
+ /**
265
+ * The live-branch publish gate refused the new function: the verdict on stdout (under `-o json`,
266
+ * `{created: false, message, policy_gate}`), then the platform's message and how to proceed,
267
+ * exiting 2.
268
+ */
269
+ refusePublish(refused, flags) {
270
+ const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
271
+ const source = flags.file ? ` -f ${quoted(flags.file)}` : ' --stdin';
272
+ const { lines, message } = publishRefusal(refused, {
273
+ json: flags.output === 'json',
274
+ outcome: 'created',
275
+ rerun: `xano function create${workspace}${source} --policy-override "<why>"`,
276
+ });
277
+ for (const line of lines)
278
+ this.log(line);
279
+ this.refusedByPolicyGate = true;
280
+ this.error(message, { exit: 2 });
281
+ }
242
282
  }
@@ -9,6 +9,7 @@ export default class FunctionEdit extends BaseCommand {
9
9
  edit: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ 'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  publish: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -16,10 +17,24 @@ export default class FunctionEdit extends BaseCommand {
16
17
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
18
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
19
  };
20
+ /** Set once the publish policy gate refuses, the one failure that exits 2. */
21
+ private refusedByPolicyGate;
22
+ /** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
23
+ protected catch(error: Error & {
24
+ oclif?: {
25
+ exit?: number;
26
+ };
27
+ }): Promise<void>;
19
28
  run(): Promise<void>;
20
29
  private editFile;
21
30
  private editFunctionContent;
22
31
  private fetchFunctionCode;
23
32
  private promptForFunctionId;
24
33
  private readStdin;
34
+ /**
35
+ * The live-branch publish gate refused the update: the verdict on stdout (under `-o json`,
36
+ * `{updated: false, message, policy_gate}`), then the platform's message and how to proceed,
37
+ * exiting 2.
38
+ */
39
+ private refusePublish;
25
40
  }
@@ -5,6 +5,7 @@ import * as fs from 'node:fs';
5
5
  import * as os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import BaseCommand, { buildUserAgent } from '../../../base-command.js';
8
+ import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateRefusal, publishRefusal, quoted } from '../../../utils/policy/gate.js';
8
9
  export default class FunctionEdit extends BaseCommand {
9
10
  static args = {
10
11
  function_id: Args.string({
@@ -59,6 +60,7 @@ Name: my_function
59
60
  ...
60
61
  }
61
62
  `,
63
+ `$ xano function:edit 163 -f function.xs --policy-override "Exception approved; finding tracked in JIRA-12"`,
62
64
  ];
63
65
  static flags = {
64
66
  ...BaseCommand.baseFlags,
@@ -81,6 +83,7 @@ Name: my_function
81
83
  options: ['summary', 'json'],
82
84
  required: false,
83
85
  }),
86
+ 'policy-override': Flags.string({ description: 'Publish past a blocking live-branch publish policy gate with an audited reason (requires workspace:policy update)' }),
84
87
  publish: Flags.boolean({
85
88
  default: true,
86
89
  description: 'Publish the function after editing',
@@ -99,8 +102,18 @@ Name: my_function
99
102
  required: false,
100
103
  }),
101
104
  };
105
+ /** Set once the publish policy gate refuses, the one failure that exits 2. */
106
+ refusedByPolicyGate = false;
107
+ /** Exit 2 is the publish gate's refusal; every other failure, flag errors included, exits 1. */
108
+ async catch(error) {
109
+ if (this.refusedByPolicyGate)
110
+ return super.catch(error);
111
+ return this.catchAsOperational(error);
112
+ }
102
113
  async run() {
103
114
  const { args, flags } = await this.parse(FunctionEdit);
115
+ if (blankPolicyOverride(flags['policy-override']))
116
+ this.error(BLANK_POLICY_OVERRIDE);
104
117
  const { profile, profileName } = this.resolveProfile(flags);
105
118
  // Validate flag combinations
106
119
  if (flags.edit && !flags.file) {
@@ -175,6 +188,10 @@ Name: my_function
175
188
  include_xanoscript: 'false',
176
189
  publish: flags.publish ? 'true' : 'false',
177
190
  });
191
+ // A reason from someone with workspace:policy update publishes past a blocking publish gate.
192
+ const overrideReason = flags['policy-override']?.trim();
193
+ if (overrideReason)
194
+ queryParams.set('override_reason', overrideReason);
178
195
  const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/${functionId}?${queryParams.toString()}`;
179
196
  // Update function via API
180
197
  try {
@@ -188,6 +205,9 @@ Name: my_function
188
205
  method: 'PUT',
189
206
  }, flags.verbose, profile.access_token);
190
207
  if (!response.ok) {
208
+ const refused = await gateRefusal(response);
209
+ if (refused)
210
+ this.refusePublish(refused, flags, functionId);
191
211
  const errorText = await response.text();
192
212
  this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
193
213
  }
@@ -208,6 +228,8 @@ Name: my_function
208
228
  }
209
229
  }
210
230
  catch (error) {
231
+ if (this.refusedByPolicyGate)
232
+ throw error;
211
233
  if (error instanceof Error) {
212
234
  this.error(`Failed to update function: ${error.message}`);
213
235
  }
@@ -428,4 +450,22 @@ Name: my_function
428
450
  process.stdin.resume();
429
451
  });
430
452
  }
453
+ /**
454
+ * The live-branch publish gate refused the update: the verdict on stdout (under `-o json`,
455
+ * `{updated: false, message, policy_gate}`), then the platform's message and how to proceed,
456
+ * exiting 2.
457
+ */
458
+ refusePublish(refused, flags, functionId) {
459
+ const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
460
+ const source = flags.file ? ` -f ${quoted(flags.file)}` : flags.stdin ? ' --stdin' : '';
461
+ const { lines, message } = publishRefusal(refused, {
462
+ json: flags.output === 'json',
463
+ outcome: 'updated',
464
+ rerun: `xano function edit ${quoted(functionId)}${workspace}${source} --policy-override "<why>"`,
465
+ });
466
+ for (const line of lines)
467
+ this.log(line);
468
+ this.refusedByPolicyGate = true;
469
+ this.error(message, { exit: 2 });
470
+ }
431
471
  }
@@ -0,0 +1,15 @@
1
+ import PolicyCommand from '../../../policy-command.js';
2
+ export default class PolicyCatalogue extends PolicyCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ check: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,29 @@
1
+ import { Flags } from '@oclif/core';
2
+ import PolicyCommand from '../../../policy-command.js';
3
+ import { policyCatalogueSummary, selectCatalogueCheck } from '../../../utils/policy/catalogue.js';
4
+ import { listItems } from '../../../utils/policy/request.js';
5
+ export default class PolicyCatalogue extends PolicyCommand {
6
+ static description = 'List built-in policy checks and their parameter schemas';
7
+ static examples = ['$ xano policy catalogue -o json', '$ xano policy catalogue --check object.auth_required'];
8
+ static flags = {
9
+ ...PolicyCommand.policyFlags,
10
+ check: Flags.string({ description: 'Show only this check id (an unknown id names the closest matches)' }),
11
+ };
12
+ async run() {
13
+ const { flags } = await this.parse(PolicyCatalogue);
14
+ const { request } = this.policyTarget(flags);
15
+ const result = await request('/check');
16
+ if (flags.check) {
17
+ // `--check` narrows both output modes, so `-o json` stays pipeable for one check too.
18
+ const selected = selectCatalogueCheck(listItems(result), flags.check);
19
+ this.log(flags.output === 'json' ? JSON.stringify(selected, null, 2) : policyCatalogueSummary(selected).join('\n'));
20
+ }
21
+ else if (flags.output === 'json') {
22
+ this.log(JSON.stringify(result, null, 2));
23
+ }
24
+ else {
25
+ for (const line of policyCatalogueSummary(listItems(result)))
26
+ this.log(line);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,18 @@
1
+ import PolicyCommand from '../../../policy-command.js';
2
+ export default class PolicyCreate extends PolicyCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ goal: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ key: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ param: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
@@ -0,0 +1,39 @@
1
+ import { Flags } from '@oclif/core';
2
+ import PolicyCommand from '../../../policy-command.js';
3
+ import { policyFromGoal } from '../../../utils/policy/goal.js';
4
+ import { listAllPolicies } from '../../../utils/policy/request.js';
5
+ export default class PolicyCreate extends PolicyCommand {
6
+ static description = 'Create an independent policy from a platform goal';
7
+ static examples = [
8
+ '$ xano policy create --goal endpoints_need_login -b dev',
9
+ `$ xano policy create --goal no_pii_in_responses --key DATA-020 --param 'fields=["email","ssn"]'`,
10
+ ];
11
+ static flags = {
12
+ ...PolicyCommand.policyFlags,
13
+ ...PolicyCommand.publishFlags,
14
+ goal: Flags.string({ description: 'Goal id from policy catalogue -o json', required: true }),
15
+ key: Flags.string({ description: 'New policy key (default: the goal key with a free numeric suffix if taken)' }),
16
+ param: Flags.string({ description: 'Parameter override: N.path=JSON, 1-based rule number; path=JSON for a single-rule goal', multiple: true }),
17
+ };
18
+ async run() {
19
+ const { flags } = await this.parse(PolicyCreate);
20
+ const target = this.policyTarget(flags);
21
+ const { request } = target;
22
+ const catalogue = await request('/check');
23
+ const goal = catalogue.goals?.find(item => item.id === flags.goal);
24
+ if (!goal)
25
+ this.error(`Unknown goal "${flags.goal}". Run xano policy catalogue -o json to see this instance's goals.`);
26
+ const keys = (await listAllPolicies(request)).map(policy => policy.key);
27
+ // The key is checked against the pattern this instance's catalogue serves.
28
+ const document = policyFromGoal(goal, keys, { key: flags.key, keyRule: catalogue.document?.key, overrides: flags.param });
29
+ const parsed = await this.parseSource(request, { data: document });
30
+ const saved = await request('', 'POST', { data: { source: parsed.source }, ...(flags.message ? { message: flags.message } : {}) });
31
+ if (typeof saved?.id !== 'number')
32
+ this.error('The platform did not return the saved policy. Check xano policy list before retrying.');
33
+ this.logRuleWarnings(parsed);
34
+ if (flags.output === 'json')
35
+ this.log(JSON.stringify(saved, null, 2));
36
+ else
37
+ this.log(`Created ${parsed.policy.key} (Version ${saved.version}) in ${this.where(target)}.`);
38
+ }
39
+ }
@@ -0,0 +1,19 @@
1
+ import PolicyCommand from '../../../policy-command.js';
2
+ export default class PolicyDelete extends PolicyCommand {
3
+ static args: {
4
+ policy: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ /** `workspace push` is additive, so deleting a policy file leaves the policy in place; this removes it. */
18
+ run(): Promise<void>;
19
+ }
@@ -0,0 +1,61 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import PolicyCommand from '../../../policy-command.js';
3
+ import { confirm } from '../../../utils/multidoc-push.js';
4
+ import { listAllPolicies, sameKey } from '../../../utils/policy/request.js';
5
+ export default class PolicyDelete extends PolicyCommand {
6
+ static args = {
7
+ policy: Args.string({
8
+ description: 'Policy key (AUTH-001) or ID to delete',
9
+ required: true,
10
+ }),
11
+ };
12
+ static description = 'Delete a policy from a branch; its Version History is kept. Deleting an active, mandatory policy also needs the workspace:policy update permission.';
13
+ static examples = [
14
+ `$ xano policy delete TMP-DX-001
15
+ Delete policy TMP-DX-001 (ID: 922, Version 2) from workspace 3? Its Version History is kept. (y/N) y
16
+ Deleted policy TMP-DX-001 (ID: 922) from workspace 3.
17
+ `,
18
+ '$ xano policy delete 922 --force',
19
+ '$ xano policy delete TMP-DX-001 -w 3 --force -o json',
20
+ ];
21
+ static flags = {
22
+ ...PolicyCommand.policyFlags,
23
+ force: Flags.boolean({
24
+ char: 'f',
25
+ default: false,
26
+ description: '[IMPORTANT] NEVER run without explicit user confirmation. Skips the confirmation prompt.',
27
+ }),
28
+ };
29
+ /** `workspace push` is additive, so deleting a policy file leaves the policy in place; this removes it. */
30
+ async run() {
31
+ const { args, flags } = await this.parse(PolicyDelete);
32
+ const target = this.policyTarget(flags);
33
+ const wanted = args.policy.trim();
34
+ if (!wanted)
35
+ this.error('Provide the policy key or ID to delete.');
36
+ const policies = await listAllPolicies(target.request);
37
+ const matched = policies.find((policy) => sameKey(policy.key, wanted))
38
+ ?? (/^\d+$/.test(wanted) ? policies.find((policy) => policy.id === Number(wanted)) : undefined);
39
+ if (!matched) {
40
+ const known = policies.map((policy) => policy.key).sort();
41
+ this.error(`No policy "${wanted}" on ${this.where(target)}.${known.length > 0 ? ` This branch has: ${known.join(', ')}.` : ' This branch has no policies.'}`);
42
+ }
43
+ if (!flags.force) {
44
+ // Nobody can answer the prompt: refuse rather than report a cancel as success.
45
+ if (!process.stdin.isTTY)
46
+ this.error('Non-interactive environment detected. Use --force to skip confirmation.');
47
+ const confirmed = await confirm(`Delete policy ${matched.key} (ID: ${matched.id}, Version ${matched.version}) from ${this.where(target)}? Its Version History is kept.`);
48
+ if (!confirmed) {
49
+ this.log('Deletion cancelled.');
50
+ return;
51
+ }
52
+ }
53
+ // The policy as listed is what the prompt named: a delete of a policy changed since is refused (`policy_stale`).
54
+ const staleCheck = matched.updated_at ? { last_updated_at: String(matched.updated_at) } : {};
55
+ await target.request(`/${matched.id}`, 'DELETE', undefined, staleCheck);
56
+ if (flags.output === 'json')
57
+ this.log(JSON.stringify({ deleted: true, id: matched.id, key: matched.key }, null, 2));
58
+ else
59
+ this.log(`Deleted policy ${matched.key} (ID: ${matched.id}) from ${this.where(target)}.`);
60
+ }
61
+ }
@@ -0,0 +1,32 @@
1
+ import PolicyCommand from '../../../policy-command.js';
2
+ /** One rule's clear objects in a trial's answer: the first ones and how many there are. */
3
+ type TrialClear = {
4
+ items?: Array<{
5
+ name?: string;
6
+ }>;
7
+ policy_key?: string;
8
+ rule_id?: string;
9
+ total?: number;
10
+ };
11
+ /**
12
+ * What a trial says it is, then each rule's clear objects: the objects it examined and found
13
+ * nothing on. A trial is never stored, so its answer is all there is to show.
14
+ */
15
+ export declare function trialLines(key: string, clear: TrialClear[] | undefined): string[];
16
+ export default class PolicyEvaluate extends PolicyCommand {
17
+ static description: string;
18
+ static examples: string[];
19
+ static flags: {
20
+ policy: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'run-detail': import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ summary: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
25
+ workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
26
+ config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
27
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
28
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
+ };
30
+ run(): Promise<void>;
31
+ }
32
+ export {};