@belvador/api 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -56,4 +56,17 @@ try {
56
56
  `edge.belvador.dev` directly, so the data never passes the API gateway; it falls back to the gateway
57
57
  when the token may not hold a ticket.
58
58
 
59
+ ## Command line
60
+
61
+ `belvador-api privacy pull` writes a project's privacy policy as the JSON the Belvador SDKs accept
62
+ (`privacyPolicy()`, SDK 0.6.0+), so a build can ship the same rules your node applies:
63
+
64
+ ```sh
65
+ BELVADOR_TOKEN=blv_svc_… npx -p @belvador/api belvador-api privacy pull --project 42 --out belvador-privacy.json
66
+ ```
67
+
68
+ `--project` is the project's number (the end of its DSN). The token needs `projects:read`; it is read
69
+ only from `BELVADOR_TOKEN` and never printed. Exit codes: 0 written, 1 refused by the API (the code is
70
+ printed), 2 wrong usage.
71
+
59
72
  Node ≥ 20.10, Bun, Deno and modern browsers (any runtime with `fetch` and `AbortSignal.any`). MIT licensed.
package/dist/cli.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/api/bin/belvador-api.ts
4
+ import { writeFileSync } from "node:fs";
5
+
6
+ // packages/api/bin/run.ts
7
+ import { BelvadorApiError, createClient, TOKEN_PATTERN } from "@belvador/api";
8
+ var USAGE = `Usage: belvador-api privacy pull --project <numeric project id> [--out <file>] [--base-url <url>]
9
+
10
+ privacy pull Write the project's privacy policy as JSON (for init({ privacyPolicy })).
11
+ Reads BELVADOR_TOKEN: a token that can read the project (scope projects:read).
12
+ --out <file> Write to a file instead of stdout.
13
+ `;
14
+ function isPolicy(v) {
15
+ if (!v || typeof v !== "object")
16
+ return false;
17
+ const p = v;
18
+ return typeof p.schema === "number" && typeof p.version === "number" && typeof p.mode === "string" && Array.isArray(p.rules) && Array.isArray(p.allowlist);
19
+ }
20
+ function flags(args) {
21
+ const out = new Map;
22
+ for (let i = 0;i < args.length; i++) {
23
+ const a = args[i];
24
+ if (!a.startsWith("--"))
25
+ return `unexpected argument ${a}`;
26
+ const eq = a.indexOf("=");
27
+ const name = eq > 0 ? a.slice(2, eq) : a.slice(2);
28
+ const value = eq > 0 ? a.slice(eq + 1) : args[++i];
29
+ if (!["project", "out", "base-url"].includes(name))
30
+ return `unknown option --${name}`;
31
+ if (value === undefined || value === "" || value.startsWith("--"))
32
+ return `--${name} needs a value`;
33
+ out.set(name, value);
34
+ }
35
+ return out;
36
+ }
37
+ async function run(argv, deps) {
38
+ const [group, command, ...rest] = argv;
39
+ if (group === "--help" || group === "-h" || group === "help") {
40
+ deps.stdout(USAGE);
41
+ return 0;
42
+ }
43
+ if (group !== "privacy" || command !== "pull") {
44
+ deps.stderr(USAGE);
45
+ return 2;
46
+ }
47
+ const opts = flags(rest);
48
+ if (typeof opts === "string") {
49
+ deps.stderr(`belvador-api: ${opts}
50
+
51
+ ${USAGE}`);
52
+ return 2;
53
+ }
54
+ const raw = opts.get("project");
55
+ if (!raw) {
56
+ deps.stderr(`belvador-api: --project is required
57
+
58
+ ${USAGE}`);
59
+ return 2;
60
+ }
61
+ if (!/^[1-9][0-9]{0,15}$/.test(raw)) {
62
+ deps.stderr(`belvador-api: --project is the project's numeric id (the number at the end of the project DSN, or GET /v1/projects)
63
+ `);
64
+ return 2;
65
+ }
66
+ const project = Number(raw);
67
+ const token = deps.env["BELVADOR_TOKEN"]?.trim();
68
+ if (!token) {
69
+ deps.stderr(`belvador-api: BELVADOR_TOKEN is not set
70
+ `);
71
+ return 2;
72
+ }
73
+ if (!TOKEN_PATTERN.test(token)) {
74
+ deps.stderr(`belvador-api: BELVADOR_TOKEN is not a Belvador token (blv_pat_, blv_svc_ or blv_oat_)
75
+ `);
76
+ return 2;
77
+ }
78
+ let policy;
79
+ try {
80
+ const baseUrl = opts.get("base-url");
81
+ const client = (deps.client ?? createClient)({ token, ...baseUrl ? { baseUrl } : {}, userAgent: "belvador-api-cli" });
82
+ const res = await client.call("privacy.get", { project_id: project });
83
+ policy = res.data?.policy;
84
+ } catch (e) {
85
+ if (e instanceof BelvadorApiError) {
86
+ const hint = e.code === "API_SCOPE_INSUFFICIENT" && e.scope ? ` (needs ${e.scope})` : "";
87
+ deps.stderr(`belvador-api: ${e.code}${hint}${e.requestId ? ` · request ${e.requestId}` : ""}
88
+ `);
89
+ return 1;
90
+ }
91
+ deps.stderr(`belvador-api: ${e instanceof TypeError ? e.message : "request failed"}
92
+ `);
93
+ return 1;
94
+ }
95
+ if (!isPolicy(policy)) {
96
+ deps.stderr(`belvador-api: the API answered without a privacy policy
97
+ `);
98
+ return 1;
99
+ }
100
+ const text = `${JSON.stringify({ schema: policy.schema, version: policy.version, mode: policy.mode, rules: policy.rules, allowlist: policy.allowlist }, null, 2)}
101
+ `;
102
+ const out = opts.get("out");
103
+ if (out) {
104
+ deps.writeFile(out, text);
105
+ deps.stderr(`belvador-api: wrote privacy policy v${policy.version} (${policy.rules.length} rules) to ${out}
106
+ `);
107
+ } else {
108
+ deps.stdout(text);
109
+ }
110
+ return 0;
111
+ }
112
+
113
+ // packages/api/bin/belvador-api.ts
114
+ process.exitCode = await run(process.argv.slice(2), {
115
+ env: process.env,
116
+ stdout: (t) => process.stdout.write(t),
117
+ stderr: (t) => process.stderr.write(t),
118
+ writeFile: (path, text) => writeFileSync(path, text, { mode: 420 })
119
+ });
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { OPERATIONS, type OperationId, type OperationMeta, type OperationParams } from "./operations.gen.js";
2
- export declare const CLIENT_VERSION = "0.3.0";
2
+ export declare const CLIENT_VERSION = "0.4.0";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.belvador.dev";
4
4
  /** Personal (`blv_pat_`), service (`blv_svc_`) or OAuth access (`blv_oat_`) token. */
5
5
  export declare const TOKEN_PATTERN: RegExp;
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BelvadorApiError, errorFrom } from "./errors.js";
2
2
  import { nodeErrorWord, nodeReadRequest, nodeRefusal, nodeWindowMissing, NODE_REFUSAL_STATUS, reshapeNodeAnswer, } from "./node-wire.gen.js";
3
3
  import { OPERATIONS } from "./operations.gen.js";
4
- export const CLIENT_VERSION = "0.3.0";
4
+ export const CLIENT_VERSION = "0.4.0";
5
5
  export const DEFAULT_BASE_URL = "https://api.belvador.dev";
6
6
  /** Personal (`blv_pat_`), service (`blv_svc_`) or OAuth access (`blv_oat_`) token. */
7
7
  export const TOKEN_PATTERN = /^blv_(pat|svc|oat)_[A-Za-z0-9_-]{20,200}$/;
@@ -138,6 +138,34 @@ export declare const OPERATIONS: {
138
138
  readonly node: null;
139
139
  readonly defaults: {};
140
140
  };
141
+ readonly "coverage.get": {
142
+ readonly method: "GET";
143
+ readonly path: "/v1/coverage";
144
+ readonly pathParams: readonly [];
145
+ readonly scope: "coverage:read";
146
+ readonly kind: "read";
147
+ readonly sensitivity: "normal";
148
+ readonly idempotency: "none";
149
+ readonly ifMatch: false;
150
+ readonly paged: false;
151
+ readonly status: 200;
152
+ readonly node: null;
153
+ readonly defaults: {};
154
+ };
155
+ readonly "coverage.set": {
156
+ readonly method: "PUT";
157
+ readonly path: "/v1/projects/{project_id}/coverage-expectations";
158
+ readonly pathParams: readonly ["project_id"];
159
+ readonly scope: "coverage:write";
160
+ readonly kind: "write";
161
+ readonly sensitivity: "normal";
162
+ readonly idempotency: "optional";
163
+ readonly ifMatch: false;
164
+ readonly paged: false;
165
+ readonly status: 200;
166
+ readonly node: null;
167
+ readonly defaults: {};
168
+ };
141
169
  readonly "destinations.create": {
142
170
  readonly method: "POST";
143
171
  readonly path: "/v1/destinations";
@@ -1175,6 +1203,34 @@ export declare const OPERATIONS: {
1175
1203
  readonly node: null;
1176
1204
  readonly defaults: {};
1177
1205
  };
1206
+ readonly "privacy.get": {
1207
+ readonly method: "GET";
1208
+ readonly path: "/v1/projects/{project_id}/privacy";
1209
+ readonly pathParams: readonly ["project_id"];
1210
+ readonly scope: "projects:read";
1211
+ readonly kind: "read";
1212
+ readonly sensitivity: "normal";
1213
+ readonly idempotency: "none";
1214
+ readonly ifMatch: false;
1215
+ readonly paged: false;
1216
+ readonly status: 200;
1217
+ readonly node: null;
1218
+ readonly defaults: {};
1219
+ };
1220
+ readonly "privacy.set": {
1221
+ readonly method: "PUT";
1222
+ readonly path: "/v1/projects/{project_id}/privacy";
1223
+ readonly pathParams: readonly ["project_id"];
1224
+ readonly scope: "privacy:write";
1225
+ readonly kind: "write";
1226
+ readonly sensitivity: "approval";
1227
+ readonly idempotency: "optional";
1228
+ readonly ifMatch: false;
1229
+ readonly paged: false;
1230
+ readonly status: 200;
1231
+ readonly node: null;
1232
+ readonly defaults: {};
1233
+ };
1178
1234
  readonly "project_keys.create": {
1179
1235
  readonly method: "POST";
1180
1236
  readonly path: "/v1/projects/{project_id}/keys";
@@ -1335,7 +1391,7 @@ export declare const OPERATIONS: {
1335
1391
  readonly pathParams: readonly ["project_id", "group"];
1336
1392
  readonly scope: "privacy:write";
1337
1393
  readonly kind: "write";
1338
- readonly sensitivity: "normal";
1394
+ readonly sensitivity: "approval";
1339
1395
  readonly idempotency: "none";
1340
1396
  readonly ifMatch: false;
1341
1397
  readonly paged: false;
@@ -1889,6 +1945,23 @@ export interface OperationParams {
1889
1945
  limit?: number;
1890
1946
  cursor?: string;
1891
1947
  };
1948
+ /** What each app is expected to send (surface × signal × environment) against what Belvador saw: `verified`, `stale`, `problem` or `unknown`, the verified rate, and signals seen but not declared. Same numbers as the Coverage page. */
1949
+ "coverage.get": {
1950
+ project_id?: number;
1951
+ environment?: string;
1952
+ };
1953
+ /** Replace a project's coverage expectations (one per environment × surface). `config_source: code` locks them in the app; send `ui` once to hand them back. Members and up. */
1954
+ "coverage.set": {
1955
+ project_id: number;
1956
+ config_source?: "ui" | "code";
1957
+ expectations: Array<{
1958
+ environment: string;
1959
+ surface: "browser" | "server" | "worker" | "job" | "edge_function" | "database" | "uptime";
1960
+ signals: Array<"errors" | "traces" | "logs" | "metrics" | "check_ins" | "web_vitals" | "probes">;
1961
+ stale_after_hours?: number;
1962
+ note?: string;
1963
+ }>;
1964
+ };
1892
1965
  /** Start adding a Telegram, Slack, email, webhook, PagerDuty or incident.io destination; a person enters its secrets. */
1893
1966
  "destinations.create": {
1894
1967
  provider: "telegram" | "slack" | "email" | "webhook" | "pagerduty" | "incident_io";
@@ -2544,6 +2617,27 @@ export interface OperationParams {
2544
2617
  monitor_id: string;
2545
2618
  grace_hours: number;
2546
2619
  };
2620
+ /** A project's privacy policy (mode, rules, strict allowlist, version), the built-in detectors that are off, whether user ids are pseudonymised, the last 20 versions and what the node applies and counted in the last 24 h. */
2621
+ "privacy.get": {
2622
+ project_id: number;
2623
+ };
2624
+ /** Replace a project's privacy policy (rules are applied in order on the node before anything is stored). Always needs an admin's approval; loosening (a rule removed or changed, strict off, allowlist grown) also needs their fresh authenticator code and emails owners. */
2625
+ "privacy.set": {
2626
+ project_id: number;
2627
+ expected_version: number;
2628
+ mode: "standard" | "strict";
2629
+ rules: Array<{
2630
+ id: string;
2631
+ selector: string;
2632
+ type: "pattern" | "anything" | "creditcard" | "ip" | "email" | "phone_e164";
2633
+ method: "remove" | "replace" | "mask" | "hash";
2634
+ pattern?: string;
2635
+ ignore_case?: boolean;
2636
+ replace_with?: string;
2637
+ enabled?: boolean;
2638
+ }>;
2639
+ allowlist: Array<string>;
2640
+ };
2547
2641
  /** Add a DSN key. */
2548
2642
  "project_keys.create": {
2549
2643
  project_id: number;
@@ -114,6 +114,34 @@ export const OPERATIONS = {
114
114
  node: null,
115
115
  defaults: {},
116
116
  },
117
+ "coverage.get": {
118
+ method: "GET",
119
+ path: "/v1/coverage",
120
+ pathParams: [],
121
+ scope: "coverage:read",
122
+ kind: "read",
123
+ sensitivity: "normal",
124
+ idempotency: "none",
125
+ ifMatch: false,
126
+ paged: false,
127
+ status: 200,
128
+ node: null,
129
+ defaults: {},
130
+ },
131
+ "coverage.set": {
132
+ method: "PUT",
133
+ path: "/v1/projects/{project_id}/coverage-expectations",
134
+ pathParams: ["project_id"],
135
+ scope: "coverage:write",
136
+ kind: "write",
137
+ sensitivity: "normal",
138
+ idempotency: "optional",
139
+ ifMatch: false,
140
+ paged: false,
141
+ status: 200,
142
+ node: null,
143
+ defaults: {},
144
+ },
117
145
  "destinations.create": {
118
146
  method: "POST",
119
147
  path: "/v1/destinations",
@@ -1108,6 +1136,34 @@ export const OPERATIONS = {
1108
1136
  node: null,
1109
1137
  defaults: {},
1110
1138
  },
1139
+ "privacy.get": {
1140
+ method: "GET",
1141
+ path: "/v1/projects/{project_id}/privacy",
1142
+ pathParams: ["project_id"],
1143
+ scope: "projects:read",
1144
+ kind: "read",
1145
+ sensitivity: "normal",
1146
+ idempotency: "none",
1147
+ ifMatch: false,
1148
+ paged: false,
1149
+ status: 200,
1150
+ node: null,
1151
+ defaults: {},
1152
+ },
1153
+ "privacy.set": {
1154
+ method: "PUT",
1155
+ path: "/v1/projects/{project_id}/privacy",
1156
+ pathParams: ["project_id"],
1157
+ scope: "privacy:write",
1158
+ kind: "write",
1159
+ sensitivity: "approval",
1160
+ idempotency: "optional",
1161
+ ifMatch: false,
1162
+ paged: false,
1163
+ status: 200,
1164
+ node: null,
1165
+ defaults: {},
1166
+ },
1111
1167
  "project_keys.create": {
1112
1168
  method: "POST",
1113
1169
  path: "/v1/projects/{project_id}/keys",
@@ -1268,7 +1324,7 @@ export const OPERATIONS = {
1268
1324
  pathParams: ["project_id", "group"],
1269
1325
  scope: "privacy:write",
1270
1326
  kind: "write",
1271
- sensitivity: "normal",
1327
+ sensitivity: "approval",
1272
1328
  idempotency: "none",
1273
1329
  ifMatch: false,
1274
1330
  paged: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@belvador/api",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Typed client for the Belvador API (api.belvador.dev, OpenAPI 3.1): every operation by operationId, problem+json errors, retries, and node reads straight from the edge with short-lived tickets.",
5
5
  "license": "MIT",
6
6
  "author": "Belvador (https://belvador.dev)",
@@ -15,6 +15,9 @@
15
15
  ],
16
16
  "type": "module",
17
17
  "sideEffects": false,
18
+ "bin": {
19
+ "belvador-api": "dist/cli.js"
20
+ },
18
21
  "main": "./dist/index.js",
19
22
  "types": "./dist/index.d.ts",
20
23
  "exports": {