@xano/cli 1.3.4 → 1.3.5-beta.1

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 (105) hide show
  1. package/README.md +70 -3
  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 +14 -0
  5. package/dist/commands/branch/set_live/index.js +38 -0
  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 +39 -0
  9. package/dist/commands/function/edit/index.d.ts +15 -0
  10. package/dist/commands/function/edit/index.js +39 -0
  11. package/dist/commands/function/run/index.js +5 -11
  12. package/dist/commands/policy/catalogue/index.d.ts +16 -0
  13. package/dist/commands/policy/catalogue/index.js +60 -0
  14. package/dist/commands/{tenant/snapshot/create → policy/coverage}/index.d.ts +4 -4
  15. package/dist/commands/policy/coverage/index.js +33 -0
  16. package/dist/commands/policy/create/index.d.ts +18 -0
  17. package/dist/commands/policy/create/index.js +44 -0
  18. package/dist/commands/{tenant/snapshot → policy}/delete/index.d.ts +5 -5
  19. package/dist/commands/policy/delete/index.js +61 -0
  20. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  21. package/dist/commands/policy/evaluate/index.js +71 -0
  22. package/dist/commands/{tenant/snapshot → policy}/list/index.d.ts +3 -5
  23. package/dist/commands/policy/list/index.js +21 -0
  24. package/dist/commands/policy/parse/index.d.ts +19 -0
  25. package/dist/commands/policy/parse/index.js +18 -0
  26. package/dist/commands/policy/publish/index.d.ts +20 -0
  27. package/dist/commands/policy/publish/index.js +38 -0
  28. package/dist/commands/policy/runs/index.d.ts +54 -0
  29. package/dist/commands/policy/runs/index.js +273 -0
  30. package/dist/commands/policy/status/index.d.ts +21 -0
  31. package/dist/commands/policy/status/index.js +54 -0
  32. package/dist/commands/release/create/index.js +4 -0
  33. package/dist/commands/release/deploy/index.d.ts +19 -0
  34. package/dist/commands/release/deploy/index.js +68 -6
  35. package/dist/commands/release/import/index.js +5 -1
  36. package/dist/commands/release/list/index.d.ts +7 -0
  37. package/dist/commands/release/list/index.js +46 -9
  38. package/dist/commands/release/push/index.js +19 -1
  39. package/dist/commands/sandbox/push/index.js +6 -1
  40. package/dist/commands/skills/pull/index.d.ts +30 -0
  41. package/dist/commands/skills/pull/index.js +118 -0
  42. package/dist/commands/tenant/create/index.d.ts +3 -0
  43. package/dist/commands/tenant/create/index.js +21 -0
  44. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  45. package/dist/commands/tenant/deploy_release/index.js +182 -4
  46. package/dist/commands/tenant/edit/index.d.ts +2 -0
  47. package/dist/commands/tenant/edit/index.js +39 -4
  48. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  49. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  50. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  51. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  52. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  53. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  54. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  55. package/dist/commands/tenant_deploy_request/get/index.js +113 -0
  56. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  57. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  58. package/dist/commands/{tenant/snapshot/swap → tenant_deploy_request/revision}/index.d.ts +8 -6
  59. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  60. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  61. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  62. package/dist/commands/workspace/pull/index.d.ts +10 -0
  63. package/dist/commands/workspace/pull/index.js +70 -7
  64. package/dist/commands/workspace/push/index.d.ts +19 -0
  65. package/dist/commands/workspace/push/index.js +135 -6
  66. package/dist/policy-command.d.ts +91 -0
  67. package/dist/policy-command.js +95 -0
  68. package/dist/utils/document-parser.d.ts +10 -0
  69. package/dist/utils/document-parser.js +21 -0
  70. package/dist/utils/multidoc-push.d.ts +57 -2
  71. package/dist/utils/multidoc-push.js +102 -33
  72. package/dist/utils/policy/catalogue.d.ts +85 -0
  73. package/dist/utils/policy/catalogue.js +196 -0
  74. package/dist/utils/policy/coverage.d.ts +19 -0
  75. package/dist/utils/policy/coverage.js +93 -0
  76. package/dist/utils/policy/errors.d.ts +20 -0
  77. package/dist/utils/policy/errors.js +93 -0
  78. package/dist/utils/policy/feedback.d.ts +90 -0
  79. package/dist/utils/policy/feedback.js +210 -0
  80. package/dist/utils/policy/findings.d.ts +29 -0
  81. package/dist/utils/policy/findings.js +82 -0
  82. package/dist/utils/policy/gate.d.ts +138 -0
  83. package/dist/utils/policy/gate.js +193 -0
  84. package/dist/utils/policy/permission.d.ts +7 -0
  85. package/dist/utils/policy/permission.js +55 -0
  86. package/dist/utils/policy/push-gate.d.ts +5 -0
  87. package/dist/utils/policy/push-gate.js +43 -0
  88. package/dist/utils/policy/release.d.ts +85 -0
  89. package/dist/utils/policy/release.js +117 -0
  90. package/dist/utils/policy/request.d.ts +55 -0
  91. package/dist/utils/policy/request.js +119 -0
  92. package/dist/utils/policy/runs.d.ts +30 -0
  93. package/dist/utils/policy/runs.js +101 -0
  94. package/dist/utils/policy/status.d.ts +55 -0
  95. package/dist/utils/policy/status.js +127 -0
  96. package/dist/utils/policy/template.d.ts +66 -0
  97. package/dist/utils/policy/template.js +152 -0
  98. package/dist/utils/policy/types.d.ts +295 -0
  99. package/dist/utils/policy/types.js +1 -0
  100. package/oclif.manifest.json +6829 -4006
  101. package/package.json +9 -1
  102. package/dist/commands/tenant/snapshot/create/index.js +0 -78
  103. package/dist/commands/tenant/snapshot/delete/index.js +0 -102
  104. package/dist/commands/tenant/snapshot/list/index.js +0 -100
  105. package/dist/commands/tenant/snapshot/swap/index.js +0 -103
@@ -0,0 +1,138 @@
1
+ import type { ProfileConfig } from '../../base-command.js';
2
+ import type { PolicyFinding } from './types.js';
3
+ /** The release the tenant deploy gate judges. */
4
+ export interface GateRelease {
5
+ /** Whether the release was cut with its policies; one cut without them has no check. */
6
+ carried?: boolean;
7
+ counts?: null | {
8
+ advisory?: number;
9
+ blocking?: number;
10
+ errors?: number;
11
+ findings?: number;
12
+ };
13
+ id?: number;
14
+ name?: string;
15
+ /** The release's stored policy run, 0 when it has none. */
16
+ run_id?: number;
17
+ status?: string;
18
+ }
19
+ /**
20
+ * The tenant deploy gate's answer: what `GET tenant/{name}/policy_gate` previews, and the payload of
21
+ * the 403 a refused deploy answers (with `code: policy_gate`). The gate judges the release alone, by
22
+ * its own policy check, so the same release gets the same verdict on every tenant; only `gated`
23
+ * differs. The findings come only to a credential that reads the workspace's policies.
24
+ */
25
+ export interface PolicyGateAnswer {
26
+ can_override?: boolean;
27
+ code?: string;
28
+ /** The first of the release's blocking findings. */
29
+ findings?: PolicyFinding[];
30
+ /** `tenant_deploy`, or `tenant_approve` when an approval completes the deploy. */
31
+ gate?: string;
32
+ /** False for an ephemeral or sandbox tenant: the verdict is reported but never refuses. */
33
+ gated?: boolean;
34
+ message?: string;
35
+ override_denied?: boolean;
36
+ release?: GateRelease | null;
37
+ run_id?: number;
38
+ /** `pass`, `blocked`, `overridden`, `not_applicable`, `not_carried`, `disabled` or `unavailable`. */
39
+ status?: string;
40
+ /** Every blocking finding the release has, listed or not. */
41
+ total?: number;
42
+ truncated?: boolean;
43
+ }
44
+ /**
45
+ * A gate that measures a change against the live branch (set-live, publish, a live-branch save): its
46
+ * blocking findings split by why they block.
47
+ */
48
+ export interface LiveGateAnswer extends Omit<PolicyGateAnswer, 'gated' | 'release'> {
49
+ changed?: number;
50
+ existing?: number;
51
+ introduced?: number;
52
+ }
53
+ /** The refusal code a policy gate's 403 carries in `payload.code`. */
54
+ export declare const GATE_REFUSAL = "policy_gate";
55
+ /** 2 when the gate refuses this deploy (blocked on a gated tenant); every other verdict is 0. */
56
+ export declare function gateExitCode(answer?: null | PolicyGateAnswer): number;
57
+ /**
58
+ * The gate's verdict on stdout: a headline with its sentence, the release it judged, how many of its
59
+ * findings block, and the first of them. Every blocking finding of the release counts, whatever the
60
+ * tenant runs today.
61
+ */
62
+ export declare function gateLines(answer: PolicyGateAnswer): string[];
63
+ /**
64
+ * The line a completed deploy prints about its gate. A blocked verdict on an ephemeral or sandbox
65
+ * tenant refused nothing (the deploy went ahead), so it says so rather than reading as a failure.
66
+ */
67
+ export declare function deployedGateLine(answer: PolicyGateAnswer): string;
68
+ /**
69
+ * The verdict of a gate that measures a change against the live branch (set-live, publish): its
70
+ * status, what blocks, and the first blocking findings. `subject` is what changes: a branch set live,
71
+ * the branch a release was deployed as (`release deploy --set_live`), or an object saved to live.
72
+ */
73
+ export declare function liveGateLines(answer: LiveGateAnswer, subject: 'branch' | 'release' | 'save'): string[];
74
+ /**
75
+ * A save the live-branch publish gate refused (`function create`, `function edit`): what goes on
76
+ * stdout (the verdict, or under `-o json` `{created|updated: false, message, policy_gate}`) and the
77
+ * error the command exits 2 with, saying nothing was saved and how to proceed (`rerun` is the same
78
+ * command with `--policy-override`).
79
+ */
80
+ export declare function publishRefusal(refused: GateRefused, options: {
81
+ json: boolean;
82
+ outcome: 'created' | 'updated';
83
+ rerun: string;
84
+ }): {
85
+ lines: string[];
86
+ message: string;
87
+ };
88
+ /** The refusal of a `--policy-override` given without a reason, before any request is made. */
89
+ export declare const BLANK_POLICY_OVERRIDE = "--policy-override needs a reason: say why this change may proceed past the policy gate.";
90
+ /** True when `--policy-override` was given but holds only whitespace. */
91
+ export declare function blankPolicyOverride(reason?: string): boolean;
92
+ /** A value quoted for the shell when it needs it. */
93
+ export declare function quoted(value: string): string;
94
+ /**
95
+ * What to do about a refusal: override it (the credential may, and `command` is how), or ask for
96
+ * the permission. A refusal whose override was denied already says so in its message.
97
+ */
98
+ export declare function gateOverrideHint(answer: PolicyGateAnswer, command: string): string;
99
+ /** A deploy the policy gate refused: the platform's sentence and the gate's answer. */
100
+ export interface GateRefused {
101
+ answer: PolicyGateAnswer;
102
+ message: string;
103
+ }
104
+ /**
105
+ * What a command the gate refused prints on stdout: the verdict, or under `-o json` the refusal as
106
+ * `{deployed: false, message, policy_gate}`. The command then exits 2 with the message.
107
+ */
108
+ export declare function gateRefusalOutput(refused: GateRefused, json: boolean): string[];
109
+ /** A policy gate's refusal read from a failed response: its message and answer, or `null` for any other failure. */
110
+ export declare function gateRefusal(response: Response): Promise<GateRefused | null>;
111
+ /** What a gate preview needs from the command that asks for it. */
112
+ export interface GateHost {
113
+ verboseFetch(url: string, options: RequestInit, verbose: boolean, authToken?: string): Promise<Response>;
114
+ }
115
+ /** A preview the platform could not answer: the HTTP status, its message and the refusal's payload. */
116
+ export interface GateUnavailable {
117
+ message: string;
118
+ payload?: unknown;
119
+ status: number;
120
+ }
121
+ /**
122
+ * `GET workspace/{ws}/tenant/{tenant}/policy_gate?release_name=`: what the gate would decide for the
123
+ * release on the tenant, without deploying. A refusal (the Policies feature is off, the credential
124
+ * cannot read policies, no such tenant or release) is answered as `unavailable` rather than thrown.
125
+ */
126
+ export declare function fetchPolicyGate(host: GateHost, target: {
127
+ profile: ProfileConfig;
128
+ release: string;
129
+ tenant: string;
130
+ verbose: boolean;
131
+ workspace: string;
132
+ }): Promise<{
133
+ answer: PolicyGateAnswer;
134
+ } | {
135
+ unavailable: GateUnavailable;
136
+ }>;
137
+ /** Why a preview is not available, in a few words for a line that must not fail its command. */
138
+ export declare function unavailableReason(unavailable: GateUnavailable): string;
@@ -0,0 +1,193 @@
1
+ import { describePolicyError } from './errors.js';
2
+ import { findingLine } from './findings.js';
3
+ /** The refusal code a policy gate's 403 carries in `payload.code`. */
4
+ export const GATE_REFUSAL = 'policy_gate';
5
+ /** 2 when the gate refuses this deploy (blocked on a gated tenant); every other verdict is 0. */
6
+ export function gateExitCode(answer) {
7
+ return answer?.status === 'blocked' && answer.gated !== false ? 2 : 0;
8
+ }
9
+ function releaseLine(release, status) {
10
+ if (!release)
11
+ return ' Release: unknown';
12
+ const name = release.name?.trim() || (release.id ? `#${release.id}` : 'unknown');
13
+ const detail = status === 'not_carried' || release.status === 'not_carried'
14
+ ? 'cut without its policies'
15
+ : [release.run_id ? `policy run ${release.run_id}` : '', release.status ?? ''].filter(Boolean).join(', ');
16
+ return ` Release: ${name}${detail ? ` (${detail})` : ''}`;
17
+ }
18
+ /** The first of the release's blocking findings, and where the rest of its findings are. */
19
+ function findingLines(answer) {
20
+ const findings = answer.findings ?? [];
21
+ if (findings.length === 0) {
22
+ const withheld = answer.findings === undefined && (answer.total ?? 0) > 0;
23
+ return withheld ? [' The findings are listed only for a credential that reads policies (workspace:policy read).'] : [];
24
+ }
25
+ const total = answer.total ?? findings.length;
26
+ const releaseName = answer.release?.name?.trim();
27
+ return [
28
+ ` Blocking findings (${total > findings.length ? `first ${findings.length} of ${total}` : findings.length}):`,
29
+ ...findings.map(finding => ` ${findingLine(finding, new Map())}`),
30
+ ...(releaseName ? [` Every finding of the release: xano policy runs --release ${quoted(releaseName)}`] : []),
31
+ ];
32
+ }
33
+ /**
34
+ * The gate's verdict on stdout: a headline with its sentence, the release it judged, how many of its
35
+ * findings block, and the first of them. Every blocking finding of the release counts, whatever the
36
+ * tenant runs today.
37
+ */
38
+ export function gateLines(answer) {
39
+ const status = answer.status?.trim() || 'unknown';
40
+ const lines = [`Policy gate: ${status}`];
41
+ if (answer.message?.trim())
42
+ lines.push(` ${answer.message.trim()}`);
43
+ if (answer.release !== undefined)
44
+ lines.push(releaseLine(answer.release, status));
45
+ // Listed findings carry their own count; otherwise say how many there are.
46
+ if (['blocked', 'overridden', 'pass'].includes(status) && typeof answer.total === 'number' && (answer.findings ?? []).length === 0) {
47
+ lines.push(` Blocking findings in this release: ${answer.total}`);
48
+ }
49
+ lines.push(...findingLines(answer));
50
+ if (answer.gated === false)
51
+ lines.push(' Not gated: this tenant is ephemeral or a sandbox, so the verdict never refuses a deploy to it.');
52
+ return lines;
53
+ }
54
+ /**
55
+ * The line a completed deploy prints about its gate. A blocked verdict on an ephemeral or sandbox
56
+ * tenant refused nothing (the deploy went ahead), so it says so rather than reading as a failure.
57
+ */
58
+ export function deployedGateLine(answer) {
59
+ const status = answer.status?.trim() || 'unknown';
60
+ return status === 'blocked' && answer.gated === false
61
+ ? 'Policy gate: blocked (not gated: this tenant is ephemeral or a sandbox, so the deploy went ahead)'
62
+ : `Policy gate: ${status}`;
63
+ }
64
+ /**
65
+ * The verdict of a gate that measures a change against the live branch (set-live, publish): its
66
+ * status, what blocks, and the first blocking findings. `subject` is what changes: a branch set live,
67
+ * the branch a release was deployed as (`release deploy --set_live`), or an object saved to live.
68
+ */
69
+ export function liveGateLines(answer, subject) {
70
+ const lines = [`Policy gate: ${answer.status?.trim() || 'unknown'}`];
71
+ if (typeof answer.total === 'number') {
72
+ 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`);
73
+ }
74
+ const findings = answer.findings ?? [];
75
+ if (findings.length > 0) {
76
+ lines.push(...findings.map(finding => ` ${findingLine(finding, new Map())}`));
77
+ if (answer.truncated)
78
+ lines.push(' Only the first findings are listed.');
79
+ }
80
+ else if (answer.findings === undefined && (answer.total ?? 0) > 0) {
81
+ lines.push(' The findings are listed only for a credential that reads policies (workspace:policy read).');
82
+ }
83
+ return lines;
84
+ }
85
+ /**
86
+ * A save the live-branch publish gate refused (`function create`, `function edit`): what goes on
87
+ * stdout (the verdict, or under `-o json` `{created|updated: false, message, policy_gate}`) and the
88
+ * error the command exits 2 with, saying nothing was saved and how to proceed (`rerun` is the same
89
+ * command with `--policy-override`).
90
+ */
91
+ export function publishRefusal(refused, options) {
92
+ const lines = options.json
93
+ ? [JSON.stringify({ message: refused.message, [options.outcome]: false, policy_gate: refused.answer }, null, 2)]
94
+ : liveGateLines(refused.answer, 'save');
95
+ return { lines, message: `Nothing was saved. ${refused.message}${gateOverrideHint(refused.answer, options.rerun)}` };
96
+ }
97
+ /** The refusal of a `--policy-override` given without a reason, before any request is made. */
98
+ export const BLANK_POLICY_OVERRIDE = '--policy-override needs a reason: say why this change may proceed past the policy gate.';
99
+ /** True when `--policy-override` was given but holds only whitespace. */
100
+ export function blankPolicyOverride(reason) {
101
+ return reason !== undefined && !reason.trim();
102
+ }
103
+ /** A value quoted for the shell when it needs it. */
104
+ export function quoted(value) {
105
+ return /^[\w.@%+=:,/-]+$/.test(value) ? value : `"${value.replaceAll(/(["\\$`])/g, String.raw `\$1`)}"`;
106
+ }
107
+ /**
108
+ * What to do about a refusal: override it (the credential may, and `command` is how), or ask for
109
+ * the permission. A refusal whose override was denied already says so in its message.
110
+ */
111
+ export function gateOverrideHint(answer, command) {
112
+ if (answer.override_denied)
113
+ return '';
114
+ if (answer.can_override)
115
+ return `\nTo proceed past the policy gate with a reason (audited): ${command}`;
116
+ return '\nOverriding the policy gate needs the `workspace:policy` update permission; ask someone who has it.';
117
+ }
118
+ /**
119
+ * What a command the gate refused prints on stdout: the verdict, or under `-o json` the refusal as
120
+ * `{deployed: false, message, policy_gate}`. The command then exits 2 with the message.
121
+ */
122
+ export function gateRefusalOutput(refused, json) {
123
+ return json
124
+ ? [JSON.stringify({ deployed: false, message: refused.message, policy_gate: refused.answer }, null, 2)]
125
+ : gateLines(refused.answer);
126
+ }
127
+ /** 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. */
128
+ async function coded403(response, code) {
129
+ if (response.status !== 403)
130
+ return null;
131
+ let body;
132
+ try {
133
+ body = JSON.parse(await response.clone().text());
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ const payload = body && typeof body === 'object' ? body.payload : undefined;
139
+ if (!payload || typeof payload !== 'object' || payload.code !== code)
140
+ return null;
141
+ const { message } = body;
142
+ return { message: typeof message === 'string' ? message.trim() : '', payload: payload };
143
+ }
144
+ /** A policy gate's refusal read from a failed response: its message and answer, or `null` for any other failure. */
145
+ export async function gateRefusal(response) {
146
+ const refused = await coded403(response, GATE_REFUSAL);
147
+ if (!refused)
148
+ return null;
149
+ const answer = refused.payload;
150
+ return { answer, message: refused.message || (answer.message ?? 'Refused by the policy gate.') };
151
+ }
152
+ /**
153
+ * `GET workspace/{ws}/tenant/{tenant}/policy_gate?release_name=`: what the gate would decide for the
154
+ * release on the tenant, without deploying. A refusal (the Policies feature is off, the credential
155
+ * cannot read policies, no such tenant or release) is answered as `unavailable` rather than thrown.
156
+ */
157
+ export async function fetchPolicyGate(host, target) {
158
+ const { profile, release, tenant, verbose, workspace } = target;
159
+ const query = new URLSearchParams({ release_name: release });
160
+ const url = `${profile.instance_origin}/api:meta/workspace/${workspace}/tenant/${encodeURIComponent(tenant)}/policy_gate?${query}`;
161
+ const response = await host.verboseFetch(url, { headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` }, method: 'GET' }, verbose, profile.access_token);
162
+ const text = (await response.text()).replaceAll(profile.access_token, '[REDACTED]');
163
+ if (!response.ok) {
164
+ const { message, payload } = describePolicyError(text, response.status, url);
165
+ return { unavailable: { message, payload, status: response.status } };
166
+ }
167
+ try {
168
+ return { answer: JSON.parse(text) };
169
+ }
170
+ catch {
171
+ return { unavailable: { message: `The policy gate answered a ${response.status} that is not JSON.`, status: response.status } };
172
+ }
173
+ }
174
+ /** Why a preview is not available, in a few words for a line that must not fail its command. */
175
+ export function unavailableReason(unavailable) {
176
+ const code = unavailable.payload && typeof unavailable.payload === 'object' ? unavailable.payload.code : undefined;
177
+ switch (code) {
178
+ case 'policy_feature_disabled': {
179
+ return 'the Policies feature is off on this instance';
180
+ }
181
+ case 'policy_permission_required': {
182
+ return 'your role lacks the `workspace:policy` read permission';
183
+ }
184
+ case 'policy_scope_required': {
185
+ return 'this Metadata API token lacks the `workspace:policy` scope';
186
+ }
187
+ default: {
188
+ if (unavailable.status === 401)
189
+ return 'the Metadata API token was not accepted';
190
+ return unavailable.message.replace(/^[a-z_]+: /i, '') || `the server answered ${unavailable.status}`;
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,7 @@
1
+ /** Guidance appended to a refused policy request, given the HTTP status and the refusal's payload. */
2
+ export declare function policyPermissionGuidance(status: number, payload: unknown): string;
3
+ /**
4
+ * Guidance for a `workspace push` (or its preview) refused because it would change policies, or
5
+ * `undefined` for any other refusal.
6
+ */
7
+ export declare function policyFilePushGuidance(payload: unknown): string | undefined;
@@ -0,0 +1,55 @@
1
+ const TOKEN_SETTINGS = 'Instance settings → Metadata API & MCP Server → Manage Access Tokens';
2
+ function refusalOf(payload) {
3
+ return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
4
+ }
5
+ /** `workspace:policy create`, or just `workspace:policy` when the refusal names no level. */
6
+ function permissionAt(refusal) {
7
+ return `\`workspace:policy\`${typeof refusal.level === 'string' && refusal.level ? ` ${refusal.level}` : ''}`;
8
+ }
9
+ /** Guidance appended to a refused policy request, given the HTTP status and the refusal's payload. */
10
+ export function policyPermissionGuidance(status, payload) {
11
+ if (status === 401) {
12
+ 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}.`;
13
+ }
14
+ const refusal = refusalOf(payload);
15
+ switch (refusal.code) {
16
+ case 'policy_feature_disabled': {
17
+ 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.';
18
+ }
19
+ case 'policy_object_permission_required': {
20
+ const permission = typeof refusal.permission === 'string' && refusal.permission ? `\`${refusal.permission}\` read` : 'read';
21
+ // The platform raises this code for a role (or its per-workspace override) and for a Metadata API
22
+ // token whose scope map lacks the permission alike, so the remedy names both.
23
+ return `\nYour role or this token lacks the ${permission} permission for this object. An instance admin grants it to your role; a Metadata API token needs that scope, so create one that has it: ${TOKEN_SETTINGS}. An object's policy coverage needs only that, not \`workspace:policy\`.`;
24
+ }
25
+ case 'policy_permission_required': {
26
+ 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.'}`;
27
+ }
28
+ case 'policy_scope_required': {
29
+ return `\nThis Metadata API token was created without the ${permissionAt(refusal)} scope. Create a token that has it: ${TOKEN_SETTINGS}.`;
30
+ }
31
+ default: {
32
+ return '';
33
+ }
34
+ }
35
+ }
36
+ /**
37
+ * Guidance for a `workspace push` (or its preview) refused because it would change policies, or
38
+ * `undefined` for any other refusal.
39
+ */
40
+ export function policyFilePushGuidance(payload) {
41
+ const refusal = refusalOf(payload);
42
+ if (!Array.isArray(refusal.policies) || refusal.policies.length === 0)
43
+ return undefined;
44
+ const remedy = {
45
+ policy_permission_required: '\n - To change a policy, ask someone with that permission to push it or to run xano policy publish.',
46
+ policy_scope_required: `\n - To change a policy yourself, create a token with that scope: ${TOKEN_SETTINGS}.`,
47
+ }[String(refusal.code)];
48
+ if (!remedy)
49
+ return undefined;
50
+ 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'}.` +
51
+ '\nNothing was imported. Policy files that are unchanged never cause this.' +
52
+ '\n - To push everything else, leave the policy files out: xano workspace push -e "policies/*"' +
53
+ '\n - To discard your local policy edits, pull again: xano workspace pull' +
54
+ remedy);
55
+ }
@@ -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>, message?: string);
5
+ }
@@ -0,0 +1,43 @@
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
+ /**
11
+ * The refusal's headline. The platform's sentence names the policies the blocking findings come from,
12
+ * to every caller (`Push refused: 2 blocking policy findings from AUTH-010 and PII-001 (1 introduced
13
+ * by this change, 1 on objects it changes).`), so it is printed as it is; without one, the counts.
14
+ */
15
+ function headline(payload, message) {
16
+ const sentence = message?.trim() ?? '';
17
+ if (sentence.startsWith('Push refused:'))
18
+ return sentence;
19
+ const count = (key) => typeof payload[key] === 'number' ? payload[key] : 0;
20
+ const total = count('total');
21
+ return `Push refused: ${total} blocking policy finding${total === 1 ? '' : 's'} (${count('introduced')} introduced, ${count('changed')} on changed objects).`;
22
+ }
23
+ /** Preserve a gate refusal through shared push and operational-error wrappers. */
24
+ export class PushPolicyGateError extends Error {
25
+ payload;
26
+ constructor(payload, message) {
27
+ const findings = Array.isArray(payload.findings) ? payload.findings.slice(0, 100) : [];
28
+ const lines = findings.flatMap(finding => finding && typeof finding === 'object' ? [refusedFindingLine(finding)] : []);
29
+ // The findings come only to a caller who reads the workspace's policies; anyone else gets the counts.
30
+ const withheld = payload.findings === undefined && typeof payload.total === 'number' && payload.total > 0;
31
+ const guidance = payload.can_override === true && payload.override_denied !== true
32
+ ? 'To proceed with an audited reason, retry with --policy-override "reason".'
33
+ : 'Fix the findings or ask someone with workspace:policy update permission to override with a reason.';
34
+ super([
35
+ `${headline(payload, message)} Nothing was imported.`,
36
+ ...lines,
37
+ ...(withheld ? ['The findings are listed only for a credential that reads policies (workspace:policy read).'] : []),
38
+ ...(payload.truncated ? ['Only the first findings are listed.'] : []),
39
+ guidance,
40
+ ].join('\n'));
41
+ this.payload = payload;
42
+ }
43
+ }
@@ -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;
@@ -0,0 +1,117 @@
1
+ import { describePolicyError } from './errors.js';
2
+ import { quoted } from './gate.js';
3
+ import { policyPermissionGuidance } from './permission.js';
4
+ import { policyRequest } from './request.js';
5
+ /** The most releases one `release/policy_check` request may name (the platform refuses more). */
6
+ const CHECKS_PER_REQUEST = 200;
7
+ /** A request host's `error` that throws instead of exiting, for a read that may fail quietly. */
8
+ function fail(message) {
9
+ throw new Error(message);
10
+ }
11
+ /**
12
+ * The stored checks of these releases, by release id, or `null` when they could not be read: the
13
+ * Policies feature is off, the credential lacks `workspace:policy` read, the platform has no such
14
+ * route, or the request failed. A release the answer leaves out has no stored check. The read
15
+ * stores nothing and never checks a release on demand.
16
+ */
17
+ export async function readReleasePolicyChecks(host, route, releaseIds) {
18
+ const ids = [...new Set(releaseIds.filter(id => Number.isSafeInteger(id) && id > 0))];
19
+ const checks = new Map();
20
+ if (ids.length === 0)
21
+ return checks;
22
+ const request = policyRequest({ ...host, error: fail }, { ...route, label: 'Release policy check', path: '' });
23
+ try {
24
+ for (let start = 0; start < ids.length; start += CHECKS_PER_REQUEST) {
25
+ const batch = ids.slice(start, start + CHECKS_PER_REQUEST).map(String);
26
+ // Each batch is one request; the list is far shorter than a batch in practice.
27
+ // eslint-disable-next-line no-await-in-loop
28
+ const answer = await request('/release/policy_check', 'GET', undefined, { release_id: batch });
29
+ const items = answer && typeof answer === 'object' ? answer.items : undefined;
30
+ if (!Array.isArray(items))
31
+ return null;
32
+ for (const item of items) {
33
+ if (item && typeof item === 'object' && typeof item.release_id === 'number')
34
+ checks.set(item.release_id, item);
35
+ }
36
+ }
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ return checks;
42
+ }
43
+ /**
44
+ * A release's stored check as a tag on its list line, worded as Studio's Policies column: the
45
+ * findings it holds (`2 blocking, 1 advisory`, or `11 advisory`), `passed` when it holds none,
46
+ * `none shipped` when the release carries no policies, `could not check` when a rule could not
47
+ * run, and `not checked` for a release with no stored check. The tag is the release's own check,
48
+ * its code against the policies it ships; whether a deploy is held is each tenant's question
49
+ * (`tenant deploy_release --check`), so it never says blocked.
50
+ */
51
+ export function releasePolicyCheckTag(check) {
52
+ return `[policies: ${releasePolicyCheckLabel(check)}]`;
53
+ }
54
+ function releasePolicyCheckLabel(check) {
55
+ if (!check)
56
+ return 'not checked';
57
+ const findings = Math.max(0, check.counts?.findings ?? 0);
58
+ const blocking = Math.max(0, check.counts?.blocking ?? 0);
59
+ const advisory = Math.max(0, check.counts?.advisory ?? findings - blocking);
60
+ const counts = [blocking > 0 ? `${blocking} blocking` : '', advisory > 0 ? `${advisory} advisory` : ''].filter(Boolean);
61
+ if (check.status === 'error')
62
+ return ['could not check', ...counts].join(', ');
63
+ if (counts.length > 0)
64
+ return counts.join(', ');
65
+ return (check.policies ?? 0) > 0 ? 'passed' : 'none shipped';
66
+ }
67
+ /**
68
+ * The cut's policy check as summary lines under a release's own (indented), or none when the answer
69
+ * carries none. A cut is never refused: blocking findings are judged per tenant at deploy, so the
70
+ * lines point at the deploy preview and the release's findings.
71
+ */
72
+ export function releasePolicyRunLines(policyRun, releaseName, releaseId) {
73
+ if (!policyRun || typeof policyRun !== 'object')
74
+ return [];
75
+ const check = policyRun;
76
+ const status = check.status?.trim() || 'unknown';
77
+ if (status === 'disabled')
78
+ return [];
79
+ if (status !== 'recorded') {
80
+ return [` Policy check ${status}: ${check.message?.trim() || 'no message returned.'}`];
81
+ }
82
+ const blocking = check.blocking ?? 0;
83
+ const advisory = check.advisory ?? 0;
84
+ const findings = blocking + advisory === 0 ? 'no findings' : `${blocking} blocking, ${advisory} advisory ${blocking + advisory === 1 ? 'finding' : 'findings'}`;
85
+ const lines = [` Policy check: ${check.run_status || 'recorded'} (run ${check.run_id ?? '?'}), ${findings}`];
86
+ if (check.message?.trim())
87
+ lines.push(` ${check.message.trim()}`);
88
+ const name = releaseName?.trim() ? quoted(releaseName.trim()) : '<release_name>';
89
+ const target = releaseName?.trim() || !releaseId ? `--release ${name}` : `--release-id ${releaseId}`;
90
+ if (blocking + advisory > 0)
91
+ lines.push(` Findings: xano policy runs ${target}`);
92
+ if (blocking > 0 && releaseName?.trim())
93
+ lines.push(` Its blocking findings block a deploy to a standard or run tenant: xano tenant deploy_release <tenant> --release ${name} --check`);
94
+ return lines;
95
+ }
96
+ /** The release's own facts about its check: where it was cut from and the policies it ships. */
97
+ export function releaseRunLines(releaseName, releaseId, run) {
98
+ const shipped = run.shipped ?? [];
99
+ const keys = shipped.map(policy => policy.key ?? '').filter(Boolean);
100
+ const from = run.release?.branch?.label?.trim();
101
+ return [
102
+ `Release ${releaseName} (ID ${releaseId}): ships ${keys.length} ${keys.length === 1 ? 'policy' : 'policies'}${keys.length > 0 ? ` (${keys.join(', ')})` : ''}${from ? `; checked as cut from branch ${from}` : ''}`,
103
+ ];
104
+ }
105
+ /**
106
+ * A `release import` the platform refused for the policies the archive carries, as one message with
107
+ * its remedy, or `null` for any other failure. An archive that says it carries policies needs the
108
+ * `workspace:policy` create and update permission to import (`policy_permission_required` for the
109
+ * role, `policy_scope_required` for the token), and nothing is stored without it.
110
+ */
111
+ export function releaseImportRefusal(text, status, url) {
112
+ const { message, payload } = describePolicyError(text, status, url);
113
+ const code = payload && typeof payload === 'object' ? payload.code : undefined;
114
+ if (typeof code !== 'string' || !code.startsWith('policy_'))
115
+ return null;
116
+ return `${message}${policyPermissionGuidance(status, payload)}`;
117
+ }