@xano/cli 1.2.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 (192) hide show
  1. package/README.md +381 -4
  2. package/dist/base-command.d.ts +54 -36
  3. package/dist/base-command.js +246 -169
  4. package/dist/commands/auth/index.d.ts +1 -1
  5. package/dist/commands/auth/index.js +7 -7
  6. package/dist/commands/branch/create/index.d.ts +1 -1
  7. package/dist/commands/branch/create/index.js +1 -1
  8. package/dist/commands/branch/set_live/index.d.ts +22 -0
  9. package/dist/commands/branch/set_live/index.js +60 -1
  10. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  11. package/dist/commands/ephemeral/create/index.js +103 -0
  12. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  13. package/dist/commands/ephemeral/delete/index.js +99 -0
  14. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  15. package/dist/commands/ephemeral/edit/index.js +109 -0
  16. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  17. package/dist/commands/ephemeral/get/index.js +125 -0
  18. package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
  19. package/dist/commands/ephemeral/impersonate/index.js +120 -0
  20. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  21. package/dist/commands/ephemeral/list/index.js +123 -0
  22. package/dist/commands/ephemeral/pull/index.d.ts +19 -0
  23. package/dist/commands/ephemeral/pull/index.js +163 -0
  24. package/dist/commands/ephemeral/push/index.d.ts +35 -0
  25. package/dist/commands/ephemeral/push/index.js +228 -0
  26. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  27. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  28. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  29. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  30. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  31. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  32. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  33. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  34. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  35. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  36. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  37. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  38. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  39. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  40. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  41. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  42. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  43. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  44. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  45. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  46. package/dist/commands/flatten/index.d.ts +34 -0
  47. package/dist/commands/flatten/index.js +140 -0
  48. package/dist/commands/function/create/index.d.ts +15 -0
  49. package/dist/commands/function/create/index.js +41 -1
  50. package/dist/commands/function/edit/index.d.ts +15 -0
  51. package/dist/commands/function/edit/index.js +66 -32
  52. package/dist/commands/function/get/index.js +2 -3
  53. package/dist/commands/function/run/index.d.ts +1 -0
  54. package/dist/commands/function/run/index.js +19 -6
  55. package/dist/commands/policy/catalogue/index.d.ts +15 -0
  56. package/dist/commands/policy/catalogue/index.js +29 -0
  57. package/dist/commands/policy/create/index.d.ts +18 -0
  58. package/dist/commands/policy/create/index.js +39 -0
  59. package/dist/commands/policy/delete/index.d.ts +19 -0
  60. package/dist/commands/policy/delete/index.js +61 -0
  61. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  62. package/dist/commands/policy/evaluate/index.js +71 -0
  63. package/dist/commands/policy/list/index.d.ts +14 -0
  64. package/dist/commands/policy/list/index.js +21 -0
  65. package/dist/commands/policy/parse/index.d.ts +19 -0
  66. package/dist/commands/policy/parse/index.js +18 -0
  67. package/dist/commands/policy/publish/index.d.ts +20 -0
  68. package/dist/commands/policy/publish/index.js +38 -0
  69. package/dist/commands/policy/runs/index.d.ts +54 -0
  70. package/dist/commands/policy/runs/index.js +273 -0
  71. package/dist/commands/policy/status/index.d.ts +21 -0
  72. package/dist/commands/policy/status/index.js +54 -0
  73. package/dist/commands/profile/create/index.d.ts +1 -1
  74. package/dist/commands/profile/create/index.js +7 -7
  75. package/dist/commands/profile/me/index.js +1 -1
  76. package/dist/commands/profile/use/index.js +0 -4
  77. package/dist/commands/profile/wizard/index.d.ts +1 -1
  78. package/dist/commands/profile/wizard/index.js +12 -10
  79. package/dist/commands/profile/workspace/set/index.js +1 -1
  80. package/dist/commands/release/create/index.js +7 -0
  81. package/dist/commands/release/deploy/index.d.ts +28 -0
  82. package/dist/commands/release/deploy/index.js +90 -6
  83. package/dist/commands/release/edit/index.js +2 -2
  84. package/dist/commands/release/export/index.js +1 -2
  85. package/dist/commands/release/import/index.js +6 -2
  86. package/dist/commands/release/list/index.d.ts +7 -0
  87. package/dist/commands/release/list/index.js +54 -10
  88. package/dist/commands/release/pull/index.d.ts +0 -6
  89. package/dist/commands/release/pull/index.js +13 -99
  90. package/dist/commands/release/push/index.js +21 -3
  91. package/dist/commands/sandbox/env/get_all/index.js +1 -1
  92. package/dist/commands/sandbox/env/set_all/index.js +1 -1
  93. package/dist/commands/sandbox/get/index.js +1 -1
  94. package/dist/commands/sandbox/license/get/index.js +1 -1
  95. package/dist/commands/sandbox/license/set/index.js +1 -1
  96. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  97. package/dist/commands/sandbox/pull/index.js +13 -81
  98. package/dist/commands/sandbox/push/index.d.ts +1 -1
  99. package/dist/commands/sandbox/push/index.js +11 -6
  100. package/dist/commands/skills/pull/index.d.ts +30 -0
  101. package/dist/commands/skills/pull/index.js +118 -0
  102. package/dist/commands/static_host/build/create/index.d.ts +1 -1
  103. package/dist/commands/static_host/build/create/index.js +2 -2
  104. package/dist/commands/static_host/build/pull/index.js +1 -1
  105. package/dist/commands/static_host/build/push/index.js +1 -1
  106. package/dist/commands/static_host/edit/index.js +1 -1
  107. package/dist/commands/tenant/backup/export/index.js +1 -2
  108. package/dist/commands/tenant/backup/import/index.js +1 -1
  109. package/dist/commands/tenant/cluster/create/index.js +1 -1
  110. package/dist/commands/tenant/cluster/license/get/index.js +1 -1
  111. package/dist/commands/tenant/cluster/license/set/index.js +1 -1
  112. package/dist/commands/tenant/create/index.d.ts +4 -1
  113. package/dist/commands/tenant/create/index.js +26 -5
  114. package/dist/commands/tenant/deploy_platform/index.js +1 -1
  115. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  116. package/dist/commands/tenant/deploy_release/index.js +186 -4
  117. package/dist/commands/tenant/edit/index.d.ts +2 -0
  118. package/dist/commands/tenant/edit/index.js +46 -11
  119. package/dist/commands/tenant/env/get_all/index.js +1 -1
  120. package/dist/commands/tenant/env/set_all/index.js +1 -1
  121. package/dist/commands/tenant/impersonate/index.d.ts +1 -1
  122. package/dist/commands/tenant/impersonate/index.js +14 -14
  123. package/dist/commands/tenant/license/get/index.js +1 -1
  124. package/dist/commands/tenant/license/set/index.js +1 -1
  125. package/dist/commands/tenant/pull/index.d.ts +0 -6
  126. package/dist/commands/tenant/pull/index.js +14 -100
  127. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  128. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  129. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  130. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  131. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  132. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  133. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  134. package/dist/commands/tenant_deploy_request/get/index.js +116 -0
  135. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  136. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  137. package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
  138. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  139. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  140. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  141. package/dist/commands/workspace/git/pull/index.d.ts +4 -4
  142. package/dist/commands/workspace/git/pull/index.js +158 -97
  143. package/dist/commands/workspace/pull/index.d.ts +9 -5
  144. package/dist/commands/workspace/pull/index.js +83 -106
  145. package/dist/commands/workspace/push/index.d.ts +19 -0
  146. package/dist/commands/workspace/push/index.js +124 -5
  147. package/dist/help.js +4 -2
  148. package/dist/policy-command.d.ts +90 -0
  149. package/dist/policy-command.js +94 -0
  150. package/dist/update-check.d.ts +1 -1
  151. package/dist/update-check.js +1 -1
  152. package/dist/utils/document-parser.d.ts +126 -0
  153. package/dist/utils/document-parser.js +332 -4
  154. package/dist/utils/flatten.d.ts +36 -0
  155. package/dist/utils/flatten.js +68 -0
  156. package/dist/utils/knowledge-sync.d.ts +1 -0
  157. package/dist/utils/knowledge-sync.js +11 -6
  158. package/dist/utils/microservice-wait.d.ts +53 -0
  159. package/dist/utils/microservice-wait.js +84 -0
  160. package/dist/utils/multidoc-push.d.ts +143 -2
  161. package/dist/utils/multidoc-push.js +479 -40
  162. package/dist/utils/paging.d.ts +2 -1
  163. package/dist/utils/paging.js +0 -1
  164. package/dist/utils/policy/catalogue.d.ts +7 -0
  165. package/dist/utils/policy/catalogue.js +72 -0
  166. package/dist/utils/policy/errors.d.ts +12 -0
  167. package/dist/utils/policy/errors.js +61 -0
  168. package/dist/utils/policy/feedback.d.ts +68 -0
  169. package/dist/utils/policy/feedback.js +165 -0
  170. package/dist/utils/policy/findings.d.ts +23 -0
  171. package/dist/utils/policy/findings.js +65 -0
  172. package/dist/utils/policy/gate.d.ts +161 -0
  173. package/dist/utils/policy/gate.js +201 -0
  174. package/dist/utils/policy/goal.d.ts +46 -0
  175. package/dist/utils/policy/goal.js +95 -0
  176. package/dist/utils/policy/permission.d.ts +23 -0
  177. package/dist/utils/policy/permission.js +84 -0
  178. package/dist/utils/policy/push-gate.d.ts +5 -0
  179. package/dist/utils/policy/push-gate.js +29 -0
  180. package/dist/utils/policy/release.d.ts +85 -0
  181. package/dist/utils/policy/release.js +115 -0
  182. package/dist/utils/policy/request.d.ts +55 -0
  183. package/dist/utils/policy/request.js +115 -0
  184. package/dist/utils/policy/runs.d.ts +30 -0
  185. package/dist/utils/policy/runs.js +101 -0
  186. package/dist/utils/policy/status.d.ts +47 -0
  187. package/dist/utils/policy/status.js +117 -0
  188. package/dist/utils/policy/types.d.ts +185 -0
  189. package/dist/utils/policy/types.js +1 -0
  190. package/dist/utils/reference-checker.js +14 -3
  191. package/oclif.manifest.json +8538 -3445
  192. package/package.json +16 -1
@@ -0,0 +1,201 @@
1
+ import { describePolicyError } from './errors.js';
2
+ import { findingLine } from './findings.js';
3
+ import { WEAKENING_REFUSAL } from './permission.js';
4
+ /** The refusal code a policy gate's 403 carries in `payload.code`. */
5
+ export const GATE_REFUSAL = 'policy_gate';
6
+ /** 2 when the gate refuses this deploy (blocked on a gated tenant); every other verdict is 0. */
7
+ export function gateExitCode(answer) {
8
+ return answer?.status === 'blocked' && answer.gated !== false ? 2 : 0;
9
+ }
10
+ function releaseLine(release, status) {
11
+ if (!release)
12
+ return ' Release: unknown';
13
+ const name = release.name?.trim() || (release.id ? `#${release.id}` : 'unknown');
14
+ const detail = status === 'not_carried' || release.status === 'not_carried'
15
+ ? 'cut without its policies'
16
+ : [release.run_id ? `policy run ${release.run_id}` : '', release.status ?? ''].filter(Boolean).join(', ');
17
+ return ` Release: ${name}${detail ? ` (${detail})` : ''}`;
18
+ }
19
+ /** The first of the release's blocking findings, and where the rest of its findings are. */
20
+ function findingLines(answer) {
21
+ const findings = answer.findings ?? [];
22
+ if (findings.length === 0) {
23
+ const withheld = answer.findings === undefined && (answer.total ?? 0) > 0;
24
+ return withheld ? [' The findings are listed only for a credential that reads policies (workspace:policy read).'] : [];
25
+ }
26
+ const total = answer.total ?? findings.length;
27
+ const releaseName = answer.release?.name?.trim();
28
+ return [
29
+ ` Blocking findings (${total > findings.length ? `first ${findings.length} of ${total}` : findings.length}):`,
30
+ ...findings.map(finding => ` ${findingLine(finding, new Map())}`),
31
+ ...(releaseName ? [` Every finding of the release: xano policy runs --release ${quoted(releaseName)}`] : []),
32
+ ];
33
+ }
34
+ /**
35
+ * The gate's verdict on stdout: a headline with its sentence, the release it judged, how many of its
36
+ * findings block, and the first of them. Every blocking finding of the release counts, whatever the
37
+ * tenant runs today.
38
+ */
39
+ export function gateLines(answer) {
40
+ const status = answer.status?.trim() || 'unknown';
41
+ const lines = [`Policy gate: ${status}`];
42
+ if (answer.message?.trim())
43
+ lines.push(` ${answer.message.trim()}`);
44
+ if (answer.release !== undefined)
45
+ lines.push(releaseLine(answer.release, status));
46
+ // Listed findings carry their own count; otherwise say how many there are.
47
+ if (['blocked', 'overridden', 'pass'].includes(status) && typeof answer.total === 'number' && (answer.findings ?? []).length === 0) {
48
+ lines.push(` Blocking findings in this release: ${answer.total}`);
49
+ }
50
+ lines.push(...findingLines(answer));
51
+ if (answer.gated === false)
52
+ lines.push(' Not gated: this tenant is ephemeral or a sandbox, so the verdict never refuses a deploy to it.');
53
+ return lines;
54
+ }
55
+ /**
56
+ * The line a completed deploy prints about its gate. A blocked verdict on an ephemeral or sandbox
57
+ * tenant refused nothing (the deploy went ahead), so it says so rather than reading as a failure.
58
+ */
59
+ export function deployedGateLine(answer) {
60
+ const status = answer.status?.trim() || 'unknown';
61
+ return status === 'blocked' && answer.gated === false
62
+ ? 'Policy gate: blocked (not gated: this tenant is ephemeral or a sandbox, so the deploy went ahead)'
63
+ : `Policy gate: ${status}`;
64
+ }
65
+ /**
66
+ * The verdict of a gate that measures a change against the live branch (set-live, publish): its
67
+ * status, what blocks, and the first blocking findings. `subject` is what changes: a branch set live,
68
+ * the branch a release was deployed as (`release deploy --set_live`), or an object saved to live.
69
+ */
70
+ export function liveGateLines(answer, subject) {
71
+ const lines = [`Policy gate: ${answer.status?.trim() || 'unknown'}`];
72
+ if (typeof answer.total === 'number') {
73
+ lines.push(` Blocking findings: ${answer.total} (${answer.introduced ?? 0} introduced, ${answer.changed ?? 0} on objects the ${subject} changes); ${answer.existing ?? 0} already on the live branch never block`);
74
+ }
75
+ const findings = answer.findings ?? [];
76
+ if (findings.length > 0) {
77
+ lines.push(...findings.map(finding => ` ${findingLine(finding, new Map())}`));
78
+ if (answer.truncated)
79
+ lines.push(' Only the first findings are listed.');
80
+ }
81
+ else if (answer.findings === undefined && (answer.total ?? 0) > 0) {
82
+ lines.push(' The findings are listed only for a credential that reads policies (workspace:policy read).');
83
+ }
84
+ return lines;
85
+ }
86
+ /**
87
+ * A save the live-branch publish gate refused (`function create`, `function edit`): what goes on
88
+ * stdout (the verdict, or under `-o json` `{created|updated: false, message, policy_gate}`) and the
89
+ * error the command exits 2 with, saying nothing was saved and how to proceed (`rerun` is the same
90
+ * command with `--policy-override`).
91
+ */
92
+ export function publishRefusal(refused, options) {
93
+ const lines = options.json
94
+ ? [JSON.stringify({ message: refused.message, [options.outcome]: false, policy_gate: refused.answer }, null, 2)]
95
+ : liveGateLines(refused.answer, 'save');
96
+ return { lines, message: `Nothing was saved. ${refused.message}${gateOverrideHint(refused.answer, options.rerun)}` };
97
+ }
98
+ /** The refusal of a `--policy-override` given without a reason, before any request is made. */
99
+ export const BLANK_POLICY_OVERRIDE = '--policy-override needs a reason: say why this change may proceed past the policy gate.';
100
+ /** True when `--policy-override` was given but holds only whitespace. */
101
+ export function blankPolicyOverride(reason) {
102
+ return reason !== undefined && !reason.trim();
103
+ }
104
+ /** A value quoted for the shell when it needs it. */
105
+ export function quoted(value) {
106
+ return /^[\w.@%+=:,/-]+$/.test(value) ? value : `"${value.replaceAll(/(["\\$`])/g, String.raw `\$1`)}"`;
107
+ }
108
+ /**
109
+ * What to do about a refusal: override it (the credential may, and `command` is how), or ask for
110
+ * the permission. A refusal whose override was denied already says so in its message.
111
+ */
112
+ export function gateOverrideHint(answer, command) {
113
+ if (answer.override_denied)
114
+ return '';
115
+ if (answer.can_override)
116
+ return `\nTo proceed past the policy gate with a reason (audited): ${command}`;
117
+ return '\nOverriding the policy gate needs the `workspace:policy` update permission; ask someone who has it.';
118
+ }
119
+ /**
120
+ * What a command the gate refused prints on stdout: the verdict, or under `-o json` the refusal as
121
+ * `{deployed: false, message, policy_gate}`. The command then exits 2 with the message.
122
+ */
123
+ export function gateRefusalOutput(refused, json) {
124
+ return json
125
+ ? [JSON.stringify({ deployed: false, message: refused.message, policy_gate: refused.answer }, null, 2)]
126
+ : gateLines(refused.answer);
127
+ }
128
+ /** A 403 whose `payload.code` is `code`: its trimmed message (`''` for none) and payload, or `null` for any other failure. The body is left unread. */
129
+ async function coded403(response, code) {
130
+ if (response.status !== 403)
131
+ return null;
132
+ let body;
133
+ try {
134
+ body = JSON.parse(await response.clone().text());
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ const payload = body && typeof body === 'object' ? body.payload : undefined;
140
+ if (!payload || typeof payload !== 'object' || payload.code !== code)
141
+ return null;
142
+ const { message } = body;
143
+ return { message: typeof message === 'string' ? message.trim() : '', payload: payload };
144
+ }
145
+ /** A policy gate's refusal read from a failed response: its message and answer, or `null` for any other failure. */
146
+ export async function gateRefusal(response) {
147
+ const refused = await coded403(response, GATE_REFUSAL);
148
+ if (!refused)
149
+ return null;
150
+ const answer = refused.payload;
151
+ return { answer, message: refused.message || (answer.message ?? 'Refused by the policy gate.') };
152
+ }
153
+ /** The weakening refusal read from a failed response, or `null` for any other failure. */
154
+ export async function weakeningRefusal(response) {
155
+ const refused = await coded403(response, WEAKENING_REFUSAL);
156
+ if (!refused)
157
+ return null;
158
+ return { message: refused.message || 'Refused: the change weakens a mandatory policy.', payload: refused.payload };
159
+ }
160
+ /**
161
+ * `GET workspace/{ws}/tenant/{tenant}/policy_gate?release_name=`: what the gate would decide for the
162
+ * release on the tenant, without deploying. A refusal (the Policies feature is off, the credential
163
+ * cannot read policies, no such tenant or release) is answered as `unavailable` rather than thrown.
164
+ */
165
+ export async function fetchPolicyGate(host, target) {
166
+ const { profile, release, tenant, verbose, workspace } = target;
167
+ const query = new URLSearchParams({ release_name: release });
168
+ const url = `${profile.instance_origin}/api:meta/workspace/${workspace}/tenant/${encodeURIComponent(tenant)}/policy_gate?${query}`;
169
+ const response = await host.verboseFetch(url, { headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` }, method: 'GET' }, verbose, profile.access_token);
170
+ const text = (await response.text()).replaceAll(profile.access_token, '[REDACTED]');
171
+ if (!response.ok) {
172
+ const { message, payload } = describePolicyError(text, response.status, url);
173
+ return { unavailable: { message, payload, status: response.status } };
174
+ }
175
+ try {
176
+ return { answer: JSON.parse(text) };
177
+ }
178
+ catch {
179
+ return { unavailable: { message: `The policy gate answered a ${response.status} that is not JSON.`, status: response.status } };
180
+ }
181
+ }
182
+ /** Why a preview is not available, in a few words for a line that must not fail its command. */
183
+ export function unavailableReason(unavailable) {
184
+ const code = unavailable.payload && typeof unavailable.payload === 'object' ? unavailable.payload.code : undefined;
185
+ switch (code) {
186
+ case 'policy_feature_disabled': {
187
+ return 'the Policies feature is off on this instance';
188
+ }
189
+ case 'policy_permission_required': {
190
+ return 'your role lacks the `workspace:policy` read permission';
191
+ }
192
+ case 'policy_scope_required': {
193
+ return 'this Metadata API token lacks the `workspace:policy` scope';
194
+ }
195
+ default: {
196
+ if (unavailable.status === 401)
197
+ return 'the Metadata API token was not accepted';
198
+ return unavailable.message.replace(/^[a-z_]+: /i, '') || `the server answered ${unavailable.status}`;
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,46 @@
1
+ /** Goals and sparse parameter seeds come from the instance's catalogue. */
2
+ export interface PolicyGoal {
3
+ goal: string;
4
+ id: string;
5
+ key: string;
6
+ rules: Array<{
7
+ check: string;
8
+ needs: string[];
9
+ params: Record<string, unknown>;
10
+ title: string;
11
+ }>;
12
+ severity: string;
13
+ summary: string;
14
+ }
15
+ /** The catalogue's `document.key`: the platform's key pattern, and its refusal with `%s` for the key. */
16
+ export interface PolicyKeyRule {
17
+ message?: string;
18
+ pattern?: string;
19
+ }
20
+ /** How a goal becomes a policy: the key, the catalogue's key rule and the `--param` assignments. */
21
+ export interface GoalPolicyOptions {
22
+ /** `--key`: the new policy's key; refused when a policy already holds it. */
23
+ key?: string;
24
+ /** The fetched catalogue's `document.key`; the offline pattern stands in for an instance that serves none. */
25
+ keyRule?: null | PolicyKeyRule;
26
+ /** `--param N.path=JSON` assignments. */
27
+ overrides?: string[];
28
+ }
29
+ /** The policy document a goal becomes, for the parse route's `data`. */
30
+ export interface GoalPolicyDocument {
31
+ key: string;
32
+ rules: Array<{
33
+ check: string;
34
+ params: Record<string, unknown>;
35
+ title: string;
36
+ }>;
37
+ severity: string;
38
+ statement: string;
39
+ title: string;
40
+ }
41
+ /**
42
+ * The policy document for a goal: its sparse settings with the `--param` assignments applied, every
43
+ * `needs` path filled, and a key no policy on the branch holds (compared case-insensitively). A taken
44
+ * goal key gets a free numeric suffix; a taken or malformed `--key` is refused.
45
+ */
46
+ export declare function policyFromGoal(goal: PolicyGoal, takenKeys: string[], options?: GoalPolicyOptions): GoalPolicyDocument;
@@ -0,0 +1,95 @@
1
+ import { POLICY_KEY_PATTERN } from '../document-parser.js';
2
+ /**
3
+ * The policy document for a goal: its sparse settings with the `--param` assignments applied, every
4
+ * `needs` path filled, and a key no policy on the branch holds (compared case-insensitively). A taken
5
+ * goal key gets a free numeric suffix; a taken or malformed `--key` is refused.
6
+ */
7
+ export function policyFromGoal(goal, takenKeys, options = {}) {
8
+ const { key, keyRule, overrides = [] } = options;
9
+ const taken = new Set(takenKeys.map(value => value.toLowerCase()));
10
+ let chosen = key ?? goal.key;
11
+ if (!keyPattern(keyRule).test(chosen))
12
+ throw new Error(keyRefusal(keyRule, chosen));
13
+ if (key !== undefined && taken.has(chosen.toLowerCase()))
14
+ throw new Error(`Policy ${chosen} already exists. Choose a new --key or use policy publish to update it.`);
15
+ for (let suffix = 2; taken.has(chosen.toLowerCase()); suffix++)
16
+ chosen = `${goal.key.slice(0, 63 - String(suffix).length)}-${suffix}`;
17
+ const rules = goal.rules.map(rule => ({ check: rule.check, params: Array.isArray(rule.params) ? {} : structuredClone(rule.params), title: rule.title }));
18
+ for (const assignment of overrides)
19
+ applyOverride(rules, assignment);
20
+ const missing = goal.rules.flatMap((rule, index) => (rule.needs ?? []).filter(path => !filled(readPath(rules[index].params, path))).map(path => `${index + 1}.${path}`));
21
+ if (missing.length > 0)
22
+ throw new Error(`Fill the goal's required settings with --param 'N.path=JSON': ${missing.join(', ')}.`);
23
+ return { key: chosen, rules, severity: goal.severity, statement: goal.summary, title: goal.goal };
24
+ }
25
+ /** The catalogue's key pattern, or the offline copy when the catalogue serves none (or one that does not compile). */
26
+ function keyPattern(rule) {
27
+ if (typeof rule?.pattern !== 'string' || rule.pattern === '')
28
+ return POLICY_KEY_PATTERN;
29
+ try {
30
+ return new RegExp(rule.pattern);
31
+ }
32
+ catch {
33
+ return POLICY_KEY_PATTERN;
34
+ }
35
+ }
36
+ /** The catalogue's refusal for a malformed key, naming it, or the offline sentence. */
37
+ function keyRefusal(rule, key) {
38
+ return typeof rule?.message === 'string' && rule.message.trim()
39
+ ? rule.message.replaceAll('%s', key)
40
+ : 'A policy key must be 1–64 letters, digits, dots, underscores or hyphens, starting with a letter or digit.';
41
+ }
42
+ /** The value at a dotted path of a rule's params, or undefined when any step is missing. */
43
+ function readPath(params, path) {
44
+ let value = params;
45
+ for (const part of path.split('.')) {
46
+ if (!value || typeof value !== 'object' || !Object.hasOwn(value, part))
47
+ return undefined;
48
+ value = value[part];
49
+ }
50
+ return value;
51
+ }
52
+ /** Whether a `needs` value is set: not empty, null or blank; `false` and `0` count as set. */
53
+ function filled(value) {
54
+ if (Array.isArray(value))
55
+ return value.some(item => filled(item));
56
+ if (value && typeof value === 'object')
57
+ return Object.keys(value).length > 0;
58
+ return value !== undefined && value !== null && (typeof value !== 'string' || value.trim() !== '');
59
+ }
60
+ /** Apply one `--param [N.]path=JSON` assignment to its rule's params, refusing unsafe paths and non-JSON values. */
61
+ function applyOverride(rules, assignment) {
62
+ const equals = assignment.indexOf('=');
63
+ if (equals < 1)
64
+ throw new Error('Use --param path=JSON or --param N.path=JSON (rule numbers start at 1).');
65
+ const parts = assignment.slice(0, equals).split('.');
66
+ let index = 0;
67
+ if (/^\d+$/.test(parts[0]))
68
+ index = Number(parts.shift()) - 1;
69
+ else if (rules.length !== 1)
70
+ throw new Error('Include the rule number in --param N.path=JSON for a goal with multiple rules.');
71
+ if (!Number.isSafeInteger(index) || !rules[index])
72
+ throw new Error(`Rule ${index + 1} does not exist in this goal.`);
73
+ if (parts.length === 0 || parts.some(part => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(part) || ['__proto__', 'constructor', 'prototype'].includes(part)))
74
+ throw new Error('Invalid parameter path. Use parameter names separated by dots.');
75
+ let value;
76
+ try {
77
+ value = JSON.parse(assignment.slice(equals + 1), (_key, parsed) => {
78
+ if (typeof parsed === 'number' && !Number.isFinite(parsed))
79
+ throw new Error('Numbers must be finite.');
80
+ return parsed;
81
+ });
82
+ }
83
+ catch {
84
+ throw new Error(`The value for ${assignment.slice(0, equals)} must be JSON: a quoted string, finite number, boolean, array or object.`);
85
+ }
86
+ let target = rules[index].params;
87
+ for (const part of parts.slice(0, -1)) {
88
+ if (target[part] === undefined)
89
+ target[part] = {};
90
+ if (!target[part] || typeof target[part] !== 'object' || Array.isArray(target[part]))
91
+ throw new Error(`Cannot set a nested parameter inside ${part}.`);
92
+ target = target[part];
93
+ }
94
+ target[parts.at(-1)] = value;
95
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The refusal of a change that weakens a policy active and mandatory on the branch (demotes,
3
+ * deactivates or deletes it, or loosens one of its rules; for a set-live, a live policy the branch
4
+ * lacks) from a caller without the `workspace:policy` update permission. It is a permission refusal,
5
+ * not a blocking finding, so a command exits 1 on it. `payload.policies` names the policies.
6
+ */
7
+ export declare const WEAKENING_REFUSAL = "policy_weakening_permission_required";
8
+ /** Guidance appended to a refused policy request, given the HTTP status and the refusal's payload. */
9
+ export declare function policyPermissionGuidance(status: number, payload: unknown): string;
10
+ /**
11
+ * Guidance for a `workspace push` (or its preview) refused because it would change policies, or
12
+ * because its policy files weaken a mandatory policy of the live branch, or `undefined` for any
13
+ * other refusal.
14
+ */
15
+ export declare function policyFilePushGuidance(payload: unknown): string | undefined;
16
+ /**
17
+ * Guidance for a change refused with `policy_weakening_permission_required`. The platform's sentence
18
+ * already names the policies and what was left alone; this says why (a delete of a mandatory policy,
19
+ * or a check that could not read the policies and so fails closed), that `--policy-override` does
20
+ * not stand in for the permission when the command has that flag (`overrideFlag`), and how to go on:
21
+ * `ways` are the command's own, before the fallback of asking someone who holds the permission.
22
+ */
23
+ export declare function policyWeakeningGuidance(payload: unknown, ways?: string[], overrideFlag?: boolean): string;
@@ -0,0 +1,84 @@
1
+ const TOKEN_SETTINGS = 'Instance settings → Metadata API & MCP Server → Manage Access Tokens';
2
+ /**
3
+ * The refusal of a change that weakens a policy active and mandatory on the branch (demotes,
4
+ * deactivates or deletes it, or loosens one of its rules; for a set-live, a live policy the branch
5
+ * lacks) from a caller without the `workspace:policy` update permission. It is a permission refusal,
6
+ * not a blocking finding, so a command exits 1 on it. `payload.policies` names the policies.
7
+ */
8
+ export const WEAKENING_REFUSAL = 'policy_weakening_permission_required';
9
+ function refusalOf(payload) {
10
+ return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
11
+ }
12
+ /** `workspace:policy create`, or just `workspace:policy` when the refusal names no level. */
13
+ function permissionAt(refusal) {
14
+ return `\`workspace:policy\`${typeof refusal.level === 'string' && refusal.level ? ` ${refusal.level}` : ''}`;
15
+ }
16
+ /** Guidance appended to a refused policy request, given the HTTP status and the refusal's payload. */
17
+ export function policyPermissionGuidance(status, payload) {
18
+ if (status === 401) {
19
+ return `\nThe Metadata API token was not accepted: it is missing, expired or revoked. Check the active profile (xano profile list) or create a new token: ${TOKEN_SETTINGS}.`;
20
+ }
21
+ const refusal = refusalOf(payload);
22
+ switch (refusal.code) {
23
+ case 'policy_feature_disabled': {
24
+ return '\nThe Policies feature is turned off for this instance, so no token or permission can reach it. Ask your Xano contact to enable Policies.';
25
+ }
26
+ case 'policy_permission_required': {
27
+ return `\nYour role on this workspace lacks the ${permissionAt(refusal)} permission. Your token is fine — reissuing it will not help. Ask an instance admin to grant it.${refusal.level === 'read' ? '' : ' Reading policies and running checks need only read.'}`;
28
+ }
29
+ case 'policy_scope_required': {
30
+ return `\nThis Metadata API token was created without the ${permissionAt(refusal)} scope. Create a token that has it: ${TOKEN_SETTINGS}.`;
31
+ }
32
+ case WEAKENING_REFUSAL: {
33
+ return policyWeakeningGuidance(payload);
34
+ }
35
+ default: {
36
+ return '';
37
+ }
38
+ }
39
+ }
40
+ /**
41
+ * Guidance for a `workspace push` (or its preview) refused because it would change policies, or
42
+ * because its policy files weaken a mandatory policy of the live branch, or `undefined` for any
43
+ * other refusal.
44
+ */
45
+ export function policyFilePushGuidance(payload) {
46
+ const refusal = refusalOf(payload);
47
+ if (refusal.code === WEAKENING_REFUSAL) {
48
+ return policyWeakeningGuidance(payload, [
49
+ 'To push everything else, leave the policy files out: xano workspace push -e "policies/*"',
50
+ 'To discard your local policy edits, pull again: xano workspace pull',
51
+ ], true);
52
+ }
53
+ if (!Array.isArray(refusal.policies) || refusal.policies.length === 0)
54
+ return undefined;
55
+ const remedy = {
56
+ policy_permission_required: '\n - To change a policy, ask someone with that permission to push it or to run xano policy publish.',
57
+ policy_scope_required: `\n - To change a policy yourself, create a token with that scope: ${TOKEN_SETTINGS}.`,
58
+ }[String(refusal.code)];
59
+ if (!remedy)
60
+ return undefined;
61
+ return (`\nThis push contains policy files that differ from the policies on the branch, and changing them needs the ${permissionAt(refusal)} ${refusal.code === 'policy_scope_required' ? 'scope on your token' : 'permission'}.` +
62
+ '\nNothing was imported. Policy files that are unchanged never cause this.' +
63
+ '\n - To push everything else, leave the policy files out: xano workspace push -e "policies/*"' +
64
+ '\n - To discard your local policy edits, pull again: xano workspace pull' +
65
+ remedy);
66
+ }
67
+ /**
68
+ * Guidance for a change refused with `policy_weakening_permission_required`. The platform's sentence
69
+ * already names the policies and what was left alone; this says why (a delete of a mandatory policy,
70
+ * or a check that could not read the policies and so fails closed), that `--policy-override` does
71
+ * not stand in for the permission when the command has that flag (`overrideFlag`), and how to go on:
72
+ * `ways` are the command's own, before the fallback of asking someone who holds the permission.
73
+ */
74
+ export function policyWeakeningGuidance(payload, ways = [], overrideFlag = false) {
75
+ const refusal = refusalOf(payload);
76
+ const permission = `${permissionAt({ level: refusal.level ?? 'update' })} permission`;
77
+ const why = refusal.unavailable === true
78
+ ? `The policies could not be read to check this, so only someone with the ${permission} may proceed.`
79
+ : refusal.gate === 'delete'
80
+ ? `Deleting a policy that is active and mandatory weakens it, so it needs the ${permission}, not only delete.`
81
+ : `Weakening means demoting, deactivating or deleting a mandatory policy, or loosening one of its rules, and it needs the ${permission}.`;
82
+ return (`\n${why}${overrideFlag ? ' --policy-override does not stand in for it.' : ''}` +
83
+ [...ways, `${ways.length > 0 ? 'Or ask' : 'Ask'} someone who holds that permission to make this change, or an instance admin to grant it to your role.`].map(way => `\n - ${way}`).join(''));
84
+ }
@@ -0,0 +1,5 @@
1
+ /** Preserve a gate refusal through shared push and operational-error wrappers. */
2
+ export declare class PushPolicyGateError extends Error {
3
+ readonly payload: Record<string, unknown>;
4
+ constructor(payload: Record<string, unknown>);
5
+ }
@@ -0,0 +1,29 @@
1
+ import { findingLine } from './findings.js';
2
+ /**
3
+ * One blocking finding of a refused push: its rule, policy, object and message (a finding's message
4
+ * rarely names its object), and whether the push introduced it or changed its object.
5
+ */
6
+ function refusedFindingLine(finding) {
7
+ const reason = typeof finding.gate_reason === 'string' && finding.gate_reason.trim() ? ` (${finding.gate_reason.trim()})` : '';
8
+ return `${findingLine(finding, new Map())}${reason}`;
9
+ }
10
+ /** Preserve a gate refusal through shared push and operational-error wrappers. */
11
+ export class PushPolicyGateError extends Error {
12
+ payload;
13
+ constructor(payload) {
14
+ const count = (key) => typeof payload[key] === 'number' ? payload[key] : 0;
15
+ const findings = Array.isArray(payload.findings) ? payload.findings.slice(0, 100) : [];
16
+ const lines = findings.flatMap(finding => finding && typeof finding === 'object' ? [refusedFindingLine(finding)] : []);
17
+ const guidance = payload.can_override === true && payload.override_denied !== true
18
+ ? 'To proceed with an audited reason, retry with --policy-override "reason".'
19
+ : 'Fix the findings or ask someone with workspace:policy update permission to override with a reason.';
20
+ const total = count('total');
21
+ super([
22
+ `Push refused: ${total} blocking policy finding${total === 1 ? '' : 's'} (${count('introduced')} introduced, ${count('changed')} on changed objects). Nothing was imported.`,
23
+ ...lines,
24
+ ...(payload.truncated ? ['Only the first findings are listed.'] : []),
25
+ guidance,
26
+ ].join('\n'));
27
+ this.payload = payload;
28
+ }
29
+ }
@@ -0,0 +1,85 @@
1
+ import type { PolicyRunHead } from './types.js';
2
+ import { type PolicyRequestHost, type PolicyRequestRoute } from './request.js';
3
+ /**
4
+ * One release's stored check as `GET release/policy_check` lists it, the read behind Studio's
5
+ * Policies column: the run, its status, when it finished, its finding counts and how many policies
6
+ * the release ships. A release without a stored run is not listed at all.
7
+ */
8
+ export interface ReleasePolicyCheck {
9
+ counts?: {
10
+ advisory?: number;
11
+ blocking?: number;
12
+ errors?: number;
13
+ findings?: number;
14
+ };
15
+ finished_at?: string;
16
+ /** How many policies the release ships, inactive ones included. */
17
+ policies?: number;
18
+ release_id?: number;
19
+ run_id?: number;
20
+ /** `pass`, `fail` (a rule found something, advisory or not) or `error` (a rule could not run). */
21
+ status?: string;
22
+ }
23
+ /**
24
+ * The stored checks of these releases, by release id, or `null` when they could not be read: the
25
+ * Policies feature is off, the credential lacks `workspace:policy` read, the platform has no such
26
+ * route, or the request failed. A release the answer leaves out has no stored check. The read
27
+ * stores nothing and never checks a release on demand.
28
+ */
29
+ export declare function readReleasePolicyChecks(host: Omit<PolicyRequestHost, 'error'>, route: Omit<PolicyRequestRoute, 'label' | 'path'>, releaseIds: number[]): Promise<Map<number, ReleasePolicyCheck> | null>;
30
+ /**
31
+ * A release's stored check as a tag on its list line, worded as Studio's Policies column: the
32
+ * findings it holds (`2 blocking, 1 advisory`, or `11 advisory`), `passed` when it holds none,
33
+ * `none shipped` when the release carries no policies, `could not check` when a rule could not
34
+ * run, and `not checked` for a release with no stored check. The tag is the release's own check,
35
+ * its code against the policies it ships; whether a deploy is held is each tenant's question
36
+ * (`tenant deploy_release --check`), so it never says blocked.
37
+ */
38
+ export declare function releasePolicyCheckTag(check: null | ReleasePolicyCheck | undefined): string;
39
+ /**
40
+ * The policy check a release cut stored (`policy_run` on the create, multidoc push and import
41
+ * answers): whether one was recorded (`recorded`, or `disabled`, `skipped` or `error` with a
42
+ * message), the run, its verdict and how many of its findings block. `null` for a credential that
43
+ * cannot read policies; absent from a platform that does not check releases.
44
+ */
45
+ export interface ReleasePolicyRun {
46
+ advisory?: number;
47
+ blocking?: number;
48
+ message?: string;
49
+ run_id?: number;
50
+ /** `pass`, `fail` or `error`; empty when nothing was recorded. */
51
+ run_status?: string;
52
+ status?: string;
53
+ }
54
+ /** A release's stored run, as `GET release/{id}/policy_run` serves it: a run summary plus the release's own keys. */
55
+ export interface ReleaseRunHead extends PolicyRunHead {
56
+ release?: {
57
+ branch?: {
58
+ id?: number;
59
+ label?: string;
60
+ };
61
+ id?: number;
62
+ };
63
+ /** Every policy the release carries, inactive ones included. */
64
+ shipped?: Array<{
65
+ active?: boolean;
66
+ enforcement?: string;
67
+ key?: string;
68
+ title?: string;
69
+ }>;
70
+ }
71
+ /**
72
+ * The cut's policy check as summary lines under a release's own (indented), or none when the answer
73
+ * carries none. A cut is never refused: blocking findings are judged per tenant at deploy, so the
74
+ * lines point at the deploy preview and the release's findings.
75
+ */
76
+ export declare function releasePolicyRunLines(policyRun: unknown, releaseName?: string, releaseId?: number): string[];
77
+ /** The release's own facts about its check: where it was cut from and the policies it ships. */
78
+ export declare function releaseRunLines(releaseName: string, releaseId: number, run: ReleaseRunHead): string[];
79
+ /**
80
+ * A `release import` the platform refused for the policies the archive carries, as one message with
81
+ * its remedy, or `null` for any other failure. An archive that says it carries policies needs the
82
+ * `workspace:policy` create and update permission to import (`policy_permission_required` for the
83
+ * role, `policy_scope_required` for the token), and nothing is stored without it.
84
+ */
85
+ export declare function releaseImportRefusal(text: string, status: number, url: string): null | string;