@xano/cli 1.3.4 → 1.3.5-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 (101) hide show
  1. package/README.md +69 -2
  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/policy/coverage/index.d.ts +17 -0
  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/policy/delete/index.d.ts +19 -0
  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/policy/list/index.d.ts +14 -0
  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_deploy_request/revision/index.d.ts +21 -0
  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 +5782 -2573
  101. package/package.json +9 -1
@@ -0,0 +1,55 @@
1
+ import type { ProfileConfig } from '../../base-command.js';
2
+ import type { Policy } from './types.js';
3
+ /** Query params; a list is sent as PHP reads one (`policy[0]=A&policy[1]=B`). */
4
+ export type PolicyQuery = Record<string, string | string[]>;
5
+ /** One request against a policy-family route: a path under it, a method, a JSON body and query params. */
6
+ export type PolicyRequest = (path?: string, method?: string, body?: unknown, query?: PolicyQuery) => Promise<unknown>;
7
+ /** What a request needs from the command that makes it. */
8
+ export interface PolicyRequestHost {
9
+ error(message: string): never;
10
+ logToStderr(message: string): void;
11
+ verboseFetch(url: string, options: RequestInit, verbose: boolean, authToken?: string): Promise<Response>;
12
+ }
13
+ export interface PolicyRequestRoute {
14
+ branch: string;
15
+ /** How a failure introduces itself: `Policy request failed (403): …`. */
16
+ label: string;
17
+ /** The route under the workspace, e.g. `/policy`. */
18
+ path: string;
19
+ profile: ProfileConfig;
20
+ verbose: boolean;
21
+ workspace: string;
22
+ }
23
+ /** The route the `policy *` commands request. */
24
+ export declare const POLICY_ROUTE: Pick<PolicyRequestRoute, 'label' | 'path'>;
25
+ /** The workspace and branch a command targets: its flags first, then the profile. `-b ''` selects live. */
26
+ export declare function policyScope(flags: {
27
+ branch?: string;
28
+ workspace?: string;
29
+ }, profile: ProfileConfig): {
30
+ branch: string;
31
+ workspace: string;
32
+ };
33
+ /**
34
+ * A request against a route gated by `workspace:policy`: failures are folded to the platform's code,
35
+ * message and position, the token is redacted, and a coded refusal says what to do about it.
36
+ */
37
+ export declare function policyRequest(host: PolicyRequestHost, route: PolicyRequestRoute): PolicyRequest;
38
+ /** What the policy list route answers this credential: it lists, the Policies feature is off, or it may not. */
39
+ export type PolicyListing = 'feature_off' | 'listed' | 'refused';
40
+ /**
41
+ * Whether this credential can list the branch's policies. An export leaves out the policies its
42
+ * credential cannot read, and every policy while the Policies feature is off, without refusing, so
43
+ * this tells an export without policies from one that withheld them, and names the feature when
44
+ * the platform's refusal does (`policy_feature_disabled`). Any other failure answers `refused`.
45
+ */
46
+ export declare function policyListing(host: Omit<PolicyRequestHost, 'error'>, route: Omit<PolicyRequestRoute, 'label' | 'path'>): Promise<PolicyListing>;
47
+ /** The `items` of a list envelope, as every policy list route answers. */
48
+ export declare function listItems<T>(data: unknown): T[];
49
+ /**
50
+ * Every policy on the target branch. The list route pages them (`page`, `nextPage`), so each page is
51
+ * read in turn until the route names no next one.
52
+ */
53
+ export declare function listAllPolicies(request: PolicyRequest): Promise<Policy[]>;
54
+ /** Whether two policy keys name the same policy: keys are unique on a branch without regard to case. */
55
+ export declare function sameKey(key: string | undefined, other: string): boolean;
@@ -0,0 +1,119 @@
1
+ import { describePolicyError, policyCodeGuidance, policyRefusal } from './errors.js';
2
+ import { policyPermissionGuidance } from './permission.js';
3
+ /** The route the `policy *` commands request. */
4
+ export const POLICY_ROUTE = { label: 'Policy', path: '/policy' };
5
+ /** The workspace and branch a command targets: its flags first, then the profile. `-b ''` selects live. */
6
+ export function policyScope(flags, profile) {
7
+ // A workspace from profile.yaml or the credentials file can arrive from YAML as a number.
8
+ return { branch: flags.branch ?? profile.branch ?? '', workspace: String(flags.workspace || profile.workspace || '') };
9
+ }
10
+ /**
11
+ * A request against a route gated by `workspace:policy`: failures are folded to the platform's code,
12
+ * message and position, the token is redacted, and a coded refusal says what to do about it.
13
+ */
14
+ export function policyRequest(host, route) {
15
+ const { branch, label, profile, verbose, workspace } = route;
16
+ const base = `${profile.instance_origin}/api:meta/workspace/${workspace}${route.path}`;
17
+ return async (path = '', method = 'GET', body, query = {}) => {
18
+ const url = `${base}${path}?${queryString({ branch, ...query })}`;
19
+ const response = await host.verboseFetch(url, {
20
+ body: body === undefined ? undefined : JSON.stringify(body),
21
+ headers: {
22
+ accept: 'application/json',
23
+ Authorization: `Bearer ${profile.access_token}`,
24
+ ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
25
+ },
26
+ method,
27
+ }, verbose, profile.access_token);
28
+ if (!response.ok) {
29
+ const text = (await response.text()).replaceAll(profile.access_token, '[REDACTED]');
30
+ if (verbose && text)
31
+ host.logToStderr(text);
32
+ const { message, payload } = describePolicyError(text, response.status, url);
33
+ // A refusal that names policies reads as the CLI's own sentence, not the coded message.
34
+ const plain = policyRefusal(payload);
35
+ if (plain)
36
+ host.error(plain);
37
+ const guidance = `${policyPermissionGuidance(response.status, payload)}${policyCodeGuidance(payload)}`;
38
+ host.error(`${label} request failed (${response.status}): ${message}${guidance}`);
39
+ }
40
+ // The DELETE route can answer with no body at all; every other route sends JSON.
41
+ const text = await response.text();
42
+ if (text.trim() === '')
43
+ return {};
44
+ try {
45
+ return JSON.parse(text);
46
+ }
47
+ catch {
48
+ return host.error(`${label} request to ${path || '/'} returned a ${response.status} that is not JSON.`);
49
+ }
50
+ };
51
+ }
52
+ function queryString(query) {
53
+ const params = new URLSearchParams();
54
+ for (const [name, value] of Object.entries(query)) {
55
+ if (Array.isArray(value))
56
+ for (const [index, item] of value.entries())
57
+ params.append(`${name}[${index}]`, item);
58
+ else
59
+ params.append(name, value);
60
+ }
61
+ return params;
62
+ }
63
+ /** A request host's `error` for a caller that asks rather than fails: it throws instead of exiting. */
64
+ function fail(message) {
65
+ throw new Error(message);
66
+ }
67
+ /**
68
+ * Whether this credential can list the branch's policies. An export leaves out the policies its
69
+ * credential cannot read, and every policy while the Policies feature is off, without refusing, so
70
+ * this tells an export without policies from one that withheld them, and names the feature when
71
+ * the platform's refusal does (`policy_feature_disabled`). Any other failure answers `refused`.
72
+ */
73
+ export async function policyListing(host, route) {
74
+ let refusal;
75
+ const verboseFetch = async (...args) => {
76
+ const response = await host.verboseFetch(...args);
77
+ if (!response.ok)
78
+ refusal = await response.clone().json().catch(() => null);
79
+ return response;
80
+ };
81
+ const list = policyRequest({ ...host, error: fail, verboseFetch }, { ...route, ...POLICY_ROUTE });
82
+ return list().then(() => 'listed', () => refusalCode(refusal) === 'policy_feature_disabled' ? 'feature_off' : 'refused');
83
+ }
84
+ /** The `payload.code` of a refusal body, if it carries one. */
85
+ function refusalCode(body) {
86
+ const payload = body && typeof body === 'object' ? body.payload : undefined;
87
+ return payload && typeof payload === 'object' ? payload.code : undefined;
88
+ }
89
+ /** The `items` of a list envelope, as every policy list route answers. */
90
+ export function listItems(data) {
91
+ if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items))
92
+ return data.items;
93
+ throw new Error('The platform answered a list request without an items array.');
94
+ }
95
+ /**
96
+ * Every policy on the target branch. The list route pages them (`page`, `nextPage`), so each page is
97
+ * read in turn until the route names no next one.
98
+ */
99
+ export async function listAllPolicies(request) {
100
+ const policies = [];
101
+ let page = 1;
102
+ while (true) {
103
+ // Each page number comes from the previous response.
104
+ // eslint-disable-next-line no-await-in-loop
105
+ const listed = await request('', 'GET', undefined, { page: String(page) });
106
+ policies.push(...listItems(listed));
107
+ const { nextPage } = listed;
108
+ if (nextPage === undefined || nextPage === null)
109
+ return policies;
110
+ if (typeof nextPage !== 'number' || !Number.isSafeInteger(nextPage) || nextPage <= page) {
111
+ throw new Error('The platform returned an invalid next policy page.');
112
+ }
113
+ page = nextPage;
114
+ }
115
+ }
116
+ /** Whether two policy keys name the same policy: keys are unique on a branch without regard to case. */
117
+ export function sameKey(key, other) {
118
+ return typeof key === 'string' && key.toLowerCase() === other.toLowerCase();
119
+ }
@@ -0,0 +1,30 @@
1
+ import type { PolicyRun, PolicyRunHead, PolicyRunSummary } from './types.js';
2
+ /** The retained runs, as the run list summarises them, in a table, header first. */
3
+ export declare function policyRunTable(runs: PolicyRunSummary[]): string[];
4
+ /** Where a page of a run's findings sits among the findings that match its filters. */
5
+ export interface PolicyRunPage {
6
+ /** Whether a filter narrowed the findings. */
7
+ filtered: boolean;
8
+ /** The command that reads this run's next page; `xano policy runs <id>` unless another is named. */
9
+ next?: string;
10
+ offset: number;
11
+ /** The findings that match, listed or not. */
12
+ total: number;
13
+ }
14
+ /**
15
+ * One stored run. A run records its findings, results and the enforcement each policy had;
16
+ * `policy_check.blocking` is the gate's verdict at the time and is not stored. With `page`, the
17
+ * run's findings are one page of them (`GET run/{id}/findings`) and its counts are the summary's.
18
+ */
19
+ export declare function policyRunSummary(run: PolicyRunHead, page?: PolicyRunPage): string[];
20
+ /**
21
+ * The resolved settings a check ran with, as `settings: name=value, …`, sorted by name so output
22
+ * is stable. Values that resolved to nothing are already omitted by the platform.
23
+ */
24
+ export declare function policySettings(params: unknown): string;
25
+ /**
26
+ * What a stored run recorded about the policies it evaluated: each policy's description as written
27
+ * then, and each rule's name, settings and how many objects it inspected. Empty for a run that
28
+ * evaluated no policy.
29
+ */
30
+ export declare function policyRunDetail(run?: PolicyRun): string[];
@@ -0,0 +1,101 @@
1
+ import { findingLine, hasNoObjects, policyResultSummary, policyRuleName, ruleKey, snapshotRules } from './findings.js';
2
+ const RUN_COLUMNS = [6, 8, 14, 12, 9, 0];
3
+ const runRow = (cells) => cells.map((cell, index) => cell.padEnd(RUN_COLUMNS[index])).join('').trimEnd();
4
+ /** The retained runs, as the run list summarises them, in a table, header first. */
5
+ export function policyRunTable(runs) {
6
+ return [
7
+ runRow(['Run', 'Status', 'Findings', 'Checked', 'Trigger', 'Started']),
8
+ ...runs.map((run) => runRow([
9
+ String(run.id),
10
+ run.status,
11
+ `${run.counts.findings} findings`,
12
+ `${run.objects_checked} objects`,
13
+ run.trigger ?? '',
14
+ run.started_at ?? '',
15
+ ])),
16
+ ];
17
+ }
18
+ /**
19
+ * One stored run. A run records its findings, results and the enforcement each policy had;
20
+ * `policy_check.blocking` is the gate's verdict at the time and is not stored. With `page`, the
21
+ * run's findings are one page of them (`GET run/{id}/findings`) and its counts are the summary's.
22
+ */
23
+ export function policyRunSummary(run, page) {
24
+ const when = [run.started_at, run.finished_at].filter(Boolean).join(' → ');
25
+ const checked = typeof run.objects_checked === 'number' ? ` ${run.objects_checked} objects checked` : '';
26
+ const lines = [`Run ${run.id ?? '?'} ${run.status ?? 'unknown'}${run.trigger ? ` ${run.trigger}` : ''}${when ? ` ${when}` : ''}${checked}`];
27
+ const rules = snapshotRules(run.policies ?? []);
28
+ const findings = run.findings ?? [];
29
+ if (page)
30
+ lines.push(...pageLines(run, page));
31
+ else if (findings.length > 0)
32
+ lines.push(`Findings (${findings.length}):`, ...findings.map(finding => findingLine(finding, rules)));
33
+ else
34
+ lines.push('No findings.');
35
+ lines.push(...policyResultSummary(run.results));
36
+ return lines;
37
+ }
38
+ /** The run's counts, then one page of its findings and how to read the next. */
39
+ function pageLines(run, page) {
40
+ const rules = snapshotRules(run.policies ?? []);
41
+ const findings = run.findings ?? [];
42
+ const { counts } = run;
43
+ const lines = counts && counts.findings > 0
44
+ ? [`Findings: ${counts.findings} (${counts.blocking} blocking, ${counts.advisory ?? counts.findings - counts.blocking} advisory)`]
45
+ : [];
46
+ if (findings.length === 0) {
47
+ lines.push(page.total > 0 ? `No findings from offset ${page.offset}: ${page.total} ${page.filtered ? 'match' : 'in all'}.`
48
+ : (page.filtered ? 'No findings match.' : 'No findings.'));
49
+ return lines;
50
+ }
51
+ const next = page.offset + findings.length;
52
+ // A page that is not every finding says so, and how to read the rest.
53
+ const partial = page.offset > 0 || next < page.total;
54
+ lines.push(`Findings ${page.offset + 1}-${next} of ${page.total}${page.filtered ? ' matching' : ''}${partial ? ' (one page)' : ''}:`, ...findings.map(finding => findingLine(finding, rules)));
55
+ if (next < page.total) {
56
+ lines.push(`Next page: \`${page.next ?? `xano policy runs ${run.id}`} --offset ${next}\`, or --all for all ${page.total}${page.filtered ? ', with the same filters' : ''}.`);
57
+ }
58
+ return lines;
59
+ }
60
+ function settingValue(value) {
61
+ if (Array.isArray(value))
62
+ return `[${value.map(item => settingValue(item)).join(', ')}]`;
63
+ if (value !== null && typeof value === 'object')
64
+ return JSON.stringify(value);
65
+ return String(value);
66
+ }
67
+ /**
68
+ * The resolved settings a check ran with, as `settings: name=value, …`, sorted by name so output
69
+ * is stable. Values that resolved to nothing are already omitted by the platform.
70
+ */
71
+ export function policySettings(params) {
72
+ if (!params || typeof params !== 'object' || Array.isArray(params))
73
+ return '';
74
+ const entries = Object.entries(params).sort(([a], [b]) => a.localeCompare(b));
75
+ return entries.length === 0 ? '' : `settings: ${entries.map(([name, value]) => `${name}=${settingValue(value)}`).join(', ')}`;
76
+ }
77
+ /**
78
+ * What a stored run recorded about the policies it evaluated: each policy's description as written
79
+ * then, and each rule's name, settings and how many objects it inspected. Empty for a run that
80
+ * evaluated no policy.
81
+ */
82
+ export function policyRunDetail(run) {
83
+ const results = new Map((run?.results ?? []).map(result => [ruleKey(result.policy_key, result.check_id), result]));
84
+ const body = (run?.policies ?? []).flatMap(policy => {
85
+ const statement = policy.statement?.trim();
86
+ const rules = (policy.rules ?? []).map(rule => {
87
+ // The name falls back to the id, which the line already carries.
88
+ const name = policyRuleName(rule);
89
+ const result = results.get(ruleKey(policy.key, rule.id));
90
+ const inspected = result?.checked === undefined ? '' : (hasNoObjects(result) ? 'no objects checked' : `checked ${result.checked}`);
91
+ return ` ${[rule.id, name === rule.id ? '' : name, policySettings(rule.params) || 'settings: none', inspected]
92
+ .filter(Boolean).join(' ')}`;
93
+ });
94
+ return [` ${policy.key ?? 'policy'}${statement ? ` ${statement}` : ''}`, ...rules];
95
+ });
96
+ if (body.length === 0)
97
+ return [];
98
+ const provenance = [run?.trigger, run?.started_at].filter(Boolean).join(', ');
99
+ const heading = run?.id ? `Run ${run.id} as recorded` : 'This evaluation, which was not stored';
100
+ return [`${heading}${provenance ? ` (${provenance})` : ''}:`, ...body];
101
+ }
@@ -0,0 +1,55 @@
1
+ import type { Policy, PolicyRunHead } from './types.js';
2
+ /**
3
+ * Where a policy stands against the latest run. `stale` means the run evaluated a different
4
+ * version; `not_evaluated` that it has no result for every rule.
5
+ */
6
+ export type PolicyStatus = 'error' | 'fail' | 'inactive' | 'no_checks' | 'no_objects_checked' | 'not_evaluated' | 'pass' | 'stale';
7
+ export interface PolicyStatusRow {
8
+ /** Whether the policy is active. */
9
+ active: boolean;
10
+ /**
11
+ * Whether the latest run's findings on this policy block a merge: the run evaluated it, in its
12
+ * current version, as active and blocking (the platform's `latest_run.enforcement`), and found something.
13
+ */
14
+ blocking: boolean;
15
+ checked: number;
16
+ /** Whether `findings` and `checked` come from the latest run: only for an active policy it evaluated in its current version. */
17
+ counted: boolean;
18
+ /** The policy's own enforcement (`blocking` / `advisory`). */
19
+ enforcement: string;
20
+ findings: number;
21
+ key: string;
22
+ /** Completed rules that inspected no objects. */
23
+ rules_unchecked: number;
24
+ /** The platform's answer: the latest run evaluated a different version of this policy. */
25
+ stale: boolean;
26
+ status: PolicyStatus;
27
+ title?: string;
28
+ }
29
+ /** How a row's status reads in the summary table. */
30
+ export declare function statusLabel(row: PolicyStatusRow): string;
31
+ /** The policy's own enforcement, in Studio's words. Whether its findings block is `findingsLabel`'s to say. */
32
+ export declare function enforcementLabel(enforcement: string): string;
33
+ /**
34
+ * The enforcement column of a status row, worded as Studio's list words it: an inactive policy
35
+ * reads `Inactive` whatever its enforcement, so it never reads as blocking.
36
+ */
37
+ export declare function enforcementColumn(row: Pick<PolicyStatusRow, 'active' | 'enforcement'>): string;
38
+ /**
39
+ * `3 findings`, marked blocking when they block. A row the latest run did not count says so: an active
40
+ * policy is `not checked yet` (never evaluated, or changed since), an inactive one `not checked`.
41
+ */
42
+ export declare function findingsLabel(row: PolicyStatusRow): string;
43
+ /**
44
+ * Combine the branch's policies with the latest stored run. Whether the run is still evidence for a
45
+ * policy is the platform's `latest_run` answer; only an active policy the run evaluated in its
46
+ * current version carries counts.
47
+ */
48
+ export declare function computeStatusRows(policies: Policy[], run?: PolicyRunHead): PolicyStatusRow[];
49
+ /**
50
+ * `--fail-on-findings`: 2 for a current blocking finding, whatever else is true, as push and evaluate
51
+ * exit; otherwise 1 for stale, missing or errored evidence.
52
+ */
53
+ export declare function statusExitCode(rows: PolicyStatusRow[]): number;
54
+ /** Why `--fail-on-findings` failed, in one line naming the policies, or `null` when it did not. */
55
+ export declare function statusExitReason(rows: PolicyStatusRow[]): null | string;
@@ -0,0 +1,127 @@
1
+ import { hasNoObjects } from './findings.js';
2
+ /** How a row's status reads in the summary table. */
3
+ export function statusLabel(row) {
4
+ switch (row.status) {
5
+ case 'inactive': {
6
+ return 'inactive; not evaluated';
7
+ }
8
+ case 'pass': {
9
+ return row.rules_unchecked > 0
10
+ ? `pass; ${row.rules_unchecked} rule${row.rules_unchecked === 1 ? '' : 's'} no objects checked`
11
+ : 'pass';
12
+ }
13
+ case 'stale': {
14
+ return 'outdated; evaluate again';
15
+ }
16
+ default: {
17
+ return row.status.replaceAll('_', ' ');
18
+ }
19
+ }
20
+ }
21
+ /** The policy's own enforcement, in Studio's words. Whether its findings block is `findingsLabel`'s to say. */
22
+ export function enforcementLabel(enforcement) {
23
+ if (enforcement === 'blocking')
24
+ return 'Blocking';
25
+ if (enforcement === 'advisory')
26
+ return 'Advisory';
27
+ return enforcement.trim() || '—';
28
+ }
29
+ /**
30
+ * The enforcement column of a status row, worded as Studio's list words it: an inactive policy
31
+ * reads `Inactive` whatever its enforcement, so it never reads as blocking.
32
+ */
33
+ export function enforcementColumn(row) {
34
+ return row.active ? enforcementLabel(row.enforcement) : 'Inactive';
35
+ }
36
+ /**
37
+ * `3 findings`, marked blocking when they block. A row the latest run did not count says so: an active
38
+ * policy is `not checked yet` (never evaluated, or changed since), an inactive one `not checked`.
39
+ */
40
+ export function findingsLabel(row) {
41
+ if (!row.counted)
42
+ return row.status === 'inactive' ? 'not checked' : 'not checked yet';
43
+ return `${row.findings} findings${row.blocking ? ' (blocking)' : ''}`;
44
+ }
45
+ /** Where an active policy stands, given its rule results in a run that is still evidence for it. */
46
+ function ruleStatus(results, ruleCount, checked) {
47
+ if (ruleCount === 0)
48
+ return 'no_checks';
49
+ if (results.length === 0)
50
+ return 'not_evaluated';
51
+ if (results.some((result) => !['fail', 'no_objects', 'pass'].includes(result.status ?? '')))
52
+ return 'error';
53
+ if (results.some((result) => result.status === 'fail'))
54
+ return 'fail';
55
+ if (results.length < ruleCount)
56
+ return 'not_evaluated';
57
+ return checked === 0 ? 'no_objects_checked' : 'pass';
58
+ }
59
+ /**
60
+ * A policy's finding count in a run: its verdict in the run summary, which counts them all without
61
+ * listing them, or, from a whole run, its findings counted.
62
+ */
63
+ function findingCount(run, key) {
64
+ const verdict = (run?.policies ?? []).find((policy) => policy.key === key);
65
+ if (typeof verdict?.findings === 'number')
66
+ return verdict.findings;
67
+ return (run?.findings ?? []).filter((finding) => finding.policy_key === key).length;
68
+ }
69
+ /**
70
+ * Combine the branch's policies with the latest stored run. Whether the run is still evidence for a
71
+ * policy is the platform's `latest_run` answer; only an active policy the run evaluated in its
72
+ * current version carries counts.
73
+ */
74
+ export function computeStatusRows(policies, run) {
75
+ return policies.map((policy) => {
76
+ const ids = new Set((policy.rules ?? []).map((rule) => rule.id));
77
+ const active = policy.active !== false;
78
+ const stale = policy.latest_run?.stale === true;
79
+ const counted = active && policy.latest_run?.included === true && !stale;
80
+ const results = counted
81
+ ? (run?.results ?? []).filter((result) => result.policy_key === policy.key && ids.has(result.check_id ?? ''))
82
+ : [];
83
+ const checked = results.reduce((sum, result) => sum + (result.checked ?? 0), 0);
84
+ const findings = counted ? findingCount(run, policy.key) : 0;
85
+ let status = 'inactive';
86
+ if (active)
87
+ status = stale ? 'stale' : ruleStatus(results, ids.size, checked);
88
+ return {
89
+ active: policy.active,
90
+ blocking: counted && policy.latest_run?.enforcement === 'blocking' && findings > 0,
91
+ checked,
92
+ counted,
93
+ enforcement: policy.enforcement,
94
+ findings,
95
+ key: policy.key,
96
+ rules_unchecked: status === 'pass' ? results.filter((result) => hasNoObjects(result)).length : 0,
97
+ stale,
98
+ status,
99
+ title: policy.title,
100
+ };
101
+ });
102
+ }
103
+ /** The rows whose evidence cannot be relied on: stale, missing or errored. An inactive policy is never evaluated. */
104
+ function unreliableRows(rows) {
105
+ return rows.filter((row) => ['error', 'not_evaluated', 'stale'].includes(row.status));
106
+ }
107
+ /**
108
+ * `--fail-on-findings`: 2 for a current blocking finding, whatever else is true, as push and evaluate
109
+ * exit; otherwise 1 for stale, missing or errored evidence.
110
+ */
111
+ export function statusExitCode(rows) {
112
+ if (rows.some((row) => row.blocking))
113
+ return 2;
114
+ return unreliableRows(rows).length > 0 ? 1 : 0;
115
+ }
116
+ /** Why `--fail-on-findings` failed, in one line naming the policies, or `null` when it did not. */
117
+ export function statusExitReason(rows) {
118
+ const unreliable = unreliableRows(rows);
119
+ const evidence = unreliable.map((row) => `${row.key} ${statusLabel(row)}`).join(', ');
120
+ const blocking = rows.filter((row) => row.blocking);
121
+ if (blocking.length === 0) {
122
+ return unreliable.length > 0 ? `Evaluation evidence is stale, missing or errored (${evidence}); exit 1.` : null;
123
+ }
124
+ const findings = blocking.reduce((sum, row) => sum + row.findings, 0);
125
+ // A prediction, not a verdict: the merge gate evaluates the branch again.
126
+ return `The latest run has ${findings} blocking finding${findings === 1 ? '' : 's'} (${blocking.map((row) => row.key).join(', ')}); the merge gate evaluates the branch again before a merge.${unreliable.length > 0 ? ` Evidence is also stale, missing or errored (${evidence}).` : ''}`;
127
+ }
@@ -0,0 +1,66 @@
1
+ import type { PolicyRecipe } from './catalogue.js';
2
+ import type { PolicyCatalogueEntry } from './types.js';
3
+ /** Templates and sparse parameter seeds come from the instance's catalogue. */
4
+ export interface PolicyTemplate {
5
+ /** The id of the group the catalogue files the template under (`access_control`…; labels in `template_categories`). */
6
+ category?: string;
7
+ /** Example values that fit this template, by `needs` path: a hint for `--param`, never a value it fills. */
8
+ examples?: Record<string, unknown>;
9
+ /** Framework tag ids (`soc2`…; labels in `template_frameworks`): topics the template relates to, never a compliance claim. */
10
+ frameworks?: string[];
11
+ id: string;
12
+ key: string;
13
+ /** `needs`: the param paths the author fills, every one of them unless `needs_any` says one is enough. */
14
+ rules: Array<{
15
+ check: string;
16
+ needs: string[];
17
+ needs_any?: boolean;
18
+ params: Record<string, unknown>;
19
+ title: string;
20
+ }>;
21
+ severity: string;
22
+ summary: string;
23
+ /** Becomes the policy title. */
24
+ title: string;
25
+ }
26
+ /**
27
+ * The refusal of a recipe id given to `--template`: recipes have no create action, so it names the
28
+ * recipe's templates in the order to create them and, when it has rules no template covers, where
29
+ * they are shown.
30
+ */
31
+ export declare function recipeRefusal(recipe: PolicyRecipe): string;
32
+ /** The catalogue's `document.key`: the platform's key pattern, and its refusal with `%s` for the key. */
33
+ export interface PolicyKeyRule {
34
+ message?: string;
35
+ pattern?: string;
36
+ }
37
+ /** How a template becomes a policy: the key, the catalogue's key rule and the `--param` assignments. */
38
+ export interface TemplatePolicyOptions {
39
+ /** The fetched catalogue's checks: a need the template gives no example for shows its check's own `example`. */
40
+ checks?: PolicyCatalogueEntry[];
41
+ /** `--key`: the new policy's key; refused when a policy already holds it. */
42
+ key?: string;
43
+ /** The fetched catalogue's `document.key`; the offline pattern stands in for an instance that serves none. */
44
+ keyRule?: null | PolicyKeyRule;
45
+ /** `--param N.path=JSON` assignments. */
46
+ overrides?: string[];
47
+ }
48
+ /** The policy document a template becomes, for the parse route's `data`. */
49
+ export interface TemplatePolicyDocument {
50
+ key: string;
51
+ rules: Array<{
52
+ check: string;
53
+ params: Record<string, unknown>;
54
+ title: string;
55
+ }>;
56
+ severity: string;
57
+ statement: string;
58
+ title: string;
59
+ }
60
+ /**
61
+ * The policy document for a template: its sparse settings with the `--param` assignments applied, every
62
+ * `needs` path filled (at least one of a rule's when it says `needs_any`), and a key no policy on the
63
+ * branch holds (compared case-insensitively). A taken template key gets a free numeric suffix; a taken or
64
+ * malformed `--key` is refused.
65
+ */
66
+ export declare function policyFromTemplate(template: PolicyTemplate, takenKeys: string[], options?: TemplatePolicyOptions): TemplatePolicyDocument;