@belvador/api 0.2.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 +13 -0
- package/dist/cli.js +119 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4 -2
- package/dist/node-wire.gen.d.ts +3 -0
- package/dist/node-wire.gen.js +11 -0
- package/dist/operations.gen.d.ts +726 -10
- package/dist/operations.gen.js +351 -1
- package/package.json +4 -1
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.
|
|
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.
|
|
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}$/;
|
|
@@ -170,7 +170,9 @@ class Client {
|
|
|
170
170
|
method: meta.method,
|
|
171
171
|
headers,
|
|
172
172
|
...(parts.body !== undefined ? { body: JSON.stringify(parts.body) } : {}),
|
|
173
|
-
},
|
|
173
|
+
},
|
|
174
|
+
// Reads are safe to retry, including a read that posts a JSON body (a backtest).
|
|
175
|
+
meta.method === "GET" || meta.scope.endsWith(":read") || !!opts.idempotencyKey, opts.signal);
|
|
174
176
|
this.#noteScopes(res);
|
|
175
177
|
if (res.status === 401)
|
|
176
178
|
this.#ticket = null;
|
package/dist/node-wire.gen.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export declare const NODE_REFUSAL_STATUS: {
|
|
|
26
26
|
readonly QUERY_WINDOW_REQUIRED: 400;
|
|
27
27
|
readonly QUERY_WINDOW_TOO_LARGE: 400;
|
|
28
28
|
readonly INVALID_INPUT: 400;
|
|
29
|
+
readonly OCCURRENCE_LIMIT: 409;
|
|
30
|
+
readonly OCCURRENCE_DUE_INVALID: 400;
|
|
31
|
+
readonly MONITOR_SELECTOR_INVALID: 400;
|
|
29
32
|
readonly UNAVAILABLE: 503;
|
|
30
33
|
};
|
|
31
34
|
export type NodeRefusalCode = keyof typeof NODE_REFUSAL_STATUS;
|
package/dist/node-wire.gen.js
CHANGED
|
@@ -66,6 +66,9 @@ export const NODE_REFUSAL_STATUS = {
|
|
|
66
66
|
QUERY_WINDOW_REQUIRED: 400,
|
|
67
67
|
QUERY_WINDOW_TOO_LARGE: 400,
|
|
68
68
|
INVALID_INPUT: 400,
|
|
69
|
+
OCCURRENCE_LIMIT: 409,
|
|
70
|
+
OCCURRENCE_DUE_INVALID: 400,
|
|
71
|
+
MONITOR_SELECTOR_INVALID: 400,
|
|
69
72
|
UNAVAILABLE: 503,
|
|
70
73
|
};
|
|
71
74
|
const refuse = (code, extra = {}) => ({
|
|
@@ -85,6 +88,14 @@ export function nodeRefusal(status, error) {
|
|
|
85
88
|
return refuse("NODE_BUSY", { retryAfter: 2 });
|
|
86
89
|
if (error === "query_timeout")
|
|
87
90
|
return refuse("QUERY_TIMEOUT");
|
|
91
|
+
if (error === "occurrence_limit")
|
|
92
|
+
return refuse("OCCURRENCE_LIMIT");
|
|
93
|
+
if (error === "due_out_of_range")
|
|
94
|
+
return refuse("OCCURRENCE_DUE_INVALID");
|
|
95
|
+
if (error === "selector_invalid")
|
|
96
|
+
return refuse("MONITOR_SELECTOR_INVALID");
|
|
97
|
+
if (error === "unknown_monitor")
|
|
98
|
+
return refuse("NOT_FOUND");
|
|
88
99
|
if (status === 503 || status === 504 || status === 502)
|
|
89
100
|
return refuse("NODE_OFFLINE");
|
|
90
101
|
if (status === 400 || status === 422) {
|