saturndocs 0.1.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/LICENSE +21 -0
- package/dist/auth-FRCFGNYT.js +14 -0
- package/dist/brokenLinks-TTOBP4P7.js +48 -0
- package/dist/build-F5PWGP57.js +39 -0
- package/dist/chunk-3P2OFTXR.js +71 -0
- package/dist/chunk-3V753MRI.js +100 -0
- package/dist/chunk-5KX4ZK5E.js +104 -0
- package/dist/chunk-CMNX5DLQ.js +119 -0
- package/dist/chunk-GYRVGWDM.js +3499 -0
- package/dist/chunk-I6U4V7B4.js +1552 -0
- package/dist/chunk-J6P3VGGE.js +21 -0
- package/dist/chunk-KGJESHJC.js +37 -0
- package/dist/chunk-M2BZEDAS.js +57 -0
- package/dist/chunk-N7QPZMLP.js +82 -0
- package/dist/chunk-OEAEX5YW.js +51 -0
- package/dist/chunk-OZWTXMO3.js +584 -0
- package/dist/chunk-QLFODLVN.js +46 -0
- package/dist/chunk-R3X2DPFF.js +568 -0
- package/dist/chunk-S45UWCML.js +588 -0
- package/dist/chunk-SABK3R2P.js +2452 -0
- package/dist/chunk-STCCGFKC.js +246 -0
- package/dist/chunk-TKISMN3P.js +39 -0
- package/dist/chunk-U7PKMSB3.js +465 -0
- package/dist/chunk-VNYDYHXM.js +22 -0
- package/dist/chunk-VXEUNRVA.js +26 -0
- package/dist/chunk-XO4CUC7V.js +99 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +95 -0
- package/dist/deploy-QU7DCKWU.js +11 -0
- package/dist/dev-VDSDCBO2.js +43 -0
- package/dist/index.d.ts +382 -0
- package/dist/index.js +191 -0
- package/dist/init-7T5TEPBH.js +14 -0
- package/dist/manage-MKH27U3B.js +12 -0
- package/dist/openapiCheck-KGBQJTIQ.js +66 -0
- package/dist/pages-HIPXRLSY.js +15 -0
- package/dist/read-DMSJUEDA.js +160 -0
- package/dist/read-ZEVUZNNB.js +146 -0
- package/dist/requests-H5MIGBIS.js +13 -0
- package/dist/schema-XJUEZSIW.js +13 -0
- package/dist/sites-I4WI2MPG.js +14 -0
- package/dist/status-7R3NOI5H.js +14 -0
- package/dist/suggestions-4WFQ3APX.js +739 -0
- package/dist/validate-VXL4PL42.js +30 -0
- package/package.json +68 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyCommandHelp
|
|
3
|
+
} from "./chunk-M2BZEDAS.js";
|
|
4
|
+
import {
|
|
5
|
+
CliError,
|
|
6
|
+
commandMetadata,
|
|
7
|
+
createClient,
|
|
8
|
+
createResultWriter,
|
|
9
|
+
failureResult,
|
|
10
|
+
invalidInput,
|
|
11
|
+
isManagementError,
|
|
12
|
+
requireCommitSha,
|
|
13
|
+
resolveGlobalOptions,
|
|
14
|
+
resolveTarget,
|
|
15
|
+
successResult
|
|
16
|
+
} from "./chunk-GYRVGWDM.js";
|
|
17
|
+
|
|
18
|
+
// src/commands/manage.ts
|
|
19
|
+
import { readFile } from "fs/promises";
|
|
20
|
+
var METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
21
|
+
var PUBLIC_READS = /* @__PURE__ */ new Set(["", "openapi.json"]);
|
|
22
|
+
var SITELESS_READS = /* @__PURE__ */ new Set(["", "openapi.json", "sites"]);
|
|
23
|
+
var SIDE_EFFECTING_GETS = [
|
|
24
|
+
/^suggestions\/[^/]+\/session\/ws$/u,
|
|
25
|
+
/^credentials$/u,
|
|
26
|
+
/^github\/install$/u,
|
|
27
|
+
/^slack\/install$/u
|
|
28
|
+
];
|
|
29
|
+
var EXACT_REVIEW_ROUTES = /^suggestions\/[^/]+\/(approve|dismiss)$/u;
|
|
30
|
+
function routeOf(path) {
|
|
31
|
+
const withoutQuery = path.split("?")[0] ?? "";
|
|
32
|
+
return withoutQuery.replace(/^\//u, "").replace(/\/+$/u, "");
|
|
33
|
+
}
|
|
34
|
+
function validatePath(path) {
|
|
35
|
+
if (/[\u0000-\u001f\u007f]/u.test(path) || path.startsWith(" ") || path.endsWith(" ")) {
|
|
36
|
+
throw invalidInput("A management operation path may not contain control characters or surrounding spaces. Nothing was sent.");
|
|
37
|
+
}
|
|
38
|
+
const pathname = path.split("?")[0] ?? "";
|
|
39
|
+
for (const segment of pathname.split("/")) {
|
|
40
|
+
let decoded;
|
|
41
|
+
try {
|
|
42
|
+
decoded = decodeURIComponent(segment);
|
|
43
|
+
} catch {
|
|
44
|
+
throw invalidInput("A management operation path contains an invalid escape. Nothing was sent.");
|
|
45
|
+
}
|
|
46
|
+
if (decoded === "." || decoded === "..") {
|
|
47
|
+
throw invalidInput("A management operation path may not contain relative segments. Nothing was sent.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function plan(method, path, options, token) {
|
|
52
|
+
const verb = method.toUpperCase();
|
|
53
|
+
if (!METHODS.includes(verb)) throw invalidInput(`${method} is not a management method. Use GET, POST, PUT, PATCH, or DELETE.`);
|
|
54
|
+
if (path.startsWith("//") || path.includes("\\") || path.includes("#")) {
|
|
55
|
+
throw invalidInput("Use a relative management operation path, with no host, fragment, or backslash.");
|
|
56
|
+
}
|
|
57
|
+
validatePath(path);
|
|
58
|
+
if (options.site !== void 0 && options.site.trim() === "") throw invalidInput("--site needs a site id.");
|
|
59
|
+
const route = routeOf(path);
|
|
60
|
+
const siteListing = verb === "GET" && route === "sites" && options.site === void 0;
|
|
61
|
+
if (route.split("/").includes("sites") && !siteListing) {
|
|
62
|
+
throw invalidInput("A management operation path may not name a site. Pass the site as --site, which is the target this command verifies.");
|
|
63
|
+
}
|
|
64
|
+
const mutation = verb !== "GET" || SIDE_EFFECTING_GETS.some((pattern) => pattern.test(route));
|
|
65
|
+
if (mutation && (options.site === void 0 || options.site.trim() === "")) {
|
|
66
|
+
throw invalidInput(`${verb} ${route === "" ? "/v1" : route} acts on a site, so it needs an explicit --site. Nothing was sent.`);
|
|
67
|
+
}
|
|
68
|
+
if (!mutation && options.site === void 0 && !SITELESS_READS.has(route)) {
|
|
69
|
+
throw invalidInput(`Reading ${route} needs an explicit --site. Only the discovery reads answer without one.`);
|
|
70
|
+
}
|
|
71
|
+
const suffix = path === "" ? "" : `/${path.replace(/^\//u, "")}`;
|
|
72
|
+
const target = options.site === void 0 ? `/v1${suffix}` : `/v1/sites/${encodeURIComponent(options.site)}${suffix}`;
|
|
73
|
+
return {
|
|
74
|
+
verb,
|
|
75
|
+
path: target,
|
|
76
|
+
route,
|
|
77
|
+
mutation,
|
|
78
|
+
exactReview: EXACT_REVIEW_ROUTES.test(route),
|
|
79
|
+
anonymous: token === void 0 && verb === "GET" && options.site === void 0 && PUBLIC_READS.has(route)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function readData(options) {
|
|
83
|
+
if (options.data === void 0) return null;
|
|
84
|
+
let raw;
|
|
85
|
+
if (options.data.startsWith("@")) {
|
|
86
|
+
const file = options.data.slice(1);
|
|
87
|
+
try {
|
|
88
|
+
raw = await readFile(file, "utf8");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw invalidInput(`manage could not read ${file}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
raw = options.data;
|
|
94
|
+
}
|
|
95
|
+
let value;
|
|
96
|
+
try {
|
|
97
|
+
value = JSON.parse(raw);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw invalidInput("--data must be JSON, or @path to a file holding JSON. Nothing was sent.", { cause: error });
|
|
100
|
+
}
|
|
101
|
+
const object = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
102
|
+
return { value, fields: object ? value : null };
|
|
103
|
+
}
|
|
104
|
+
function withReviewedHashes(fields, options, route) {
|
|
105
|
+
const base = options.base ?? fields.expected_base_sha;
|
|
106
|
+
const head = options.head ?? fields.expected_head_sha;
|
|
107
|
+
if (base === void 0 || head === void 0) {
|
|
108
|
+
throw invalidInput(`${route} publishes or closes the exact revision that was reviewed, so it needs --base and --head (or expected_base_sha and expected_head_sha in --data). Nothing was sent.`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
...fields,
|
|
112
|
+
expected_base_sha: requireCommitSha(base, "--base"),
|
|
113
|
+
expected_head_sha: requireCommitSha(head, "--head")
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function buildBody(current, options) {
|
|
117
|
+
const data = await readData(options);
|
|
118
|
+
if (data !== null && data.fields === null && current.exactReview) {
|
|
119
|
+
throw invalidInput(`${current.route} needs its reviewed hashes in a JSON object, not ${Array.isArray(data.value) ? "an array" : "a bare value"}.`);
|
|
120
|
+
}
|
|
121
|
+
const fields = current.exactReview ? withReviewedHashes(data?.fields ?? {}, options, current.route) : data?.fields ?? null;
|
|
122
|
+
if (options.file !== void 0 && options.file.length > 0) {
|
|
123
|
+
if (current.verb === "GET") throw invalidInput("A GET operation carries no file parts.");
|
|
124
|
+
if (data !== null && data.fields === null) throw invalidInput("File uploads need --data to hold a JSON object of fields.");
|
|
125
|
+
const form = new FormData();
|
|
126
|
+
for (const [key, value] of Object.entries(fields ?? {})) {
|
|
127
|
+
form.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
128
|
+
}
|
|
129
|
+
for (const part of options.file) {
|
|
130
|
+
const separator = part.indexOf("=");
|
|
131
|
+
if (separator < 1) throw invalidInput(`Use --file field=path; ${part} names no field.`);
|
|
132
|
+
const filePath = part.slice(separator + 1);
|
|
133
|
+
let contents;
|
|
134
|
+
try {
|
|
135
|
+
contents = Uint8Array.from(await readFile(filePath));
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw invalidInput(`manage could not read ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
138
|
+
}
|
|
139
|
+
form.set(part.slice(0, separator), new Blob([contents]), filePath.split(/[\\/]/u).pop());
|
|
140
|
+
}
|
|
141
|
+
return { rawBody: form };
|
|
142
|
+
}
|
|
143
|
+
if (fields !== null) return { body: fields };
|
|
144
|
+
if (data === null) return {};
|
|
145
|
+
return { body: data.value };
|
|
146
|
+
}
|
|
147
|
+
function requireJsonMedia(headers) {
|
|
148
|
+
const media = (headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
|
|
149
|
+
if (media === "" || media === "application/json" || media.endsWith("+json")) return;
|
|
150
|
+
throw new CliError("transport_failure", `The management API answered ${media}, which this command does not read. Only JSON operations are supported here.`);
|
|
151
|
+
}
|
|
152
|
+
function classify(error) {
|
|
153
|
+
if (isManagementError(error)) {
|
|
154
|
+
return new CliError(error.detail.code, error.detail.message, { server: error.detail.server, cause: error });
|
|
155
|
+
}
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
async function callManagement(method, path, options, token, fetchImpl = fetch) {
|
|
159
|
+
try {
|
|
160
|
+
const current = plan(method, path, options, token);
|
|
161
|
+
const body = await buildBody(current, options);
|
|
162
|
+
if (current.verb === "GET" && (body.body !== void 0 || body.rawBody !== void 0)) {
|
|
163
|
+
throw invalidInput("A GET operation carries no body. Nothing was sent.");
|
|
164
|
+
}
|
|
165
|
+
const client = createClient({
|
|
166
|
+
origin: options.api,
|
|
167
|
+
token,
|
|
168
|
+
fetchImpl,
|
|
169
|
+
...options.timeout === void 0 ? {} : { deadlineMs: options.timeout * 1e3 }
|
|
170
|
+
});
|
|
171
|
+
if (current.mutation) await resolveTarget(client, { site: options.site, mutation: true });
|
|
172
|
+
const response = await client.request({
|
|
173
|
+
method: current.verb,
|
|
174
|
+
path: current.path,
|
|
175
|
+
...body,
|
|
176
|
+
...options.revision === void 0 ? {} : { revision: options.revision },
|
|
177
|
+
exactReview: current.exactReview,
|
|
178
|
+
mutation: current.mutation,
|
|
179
|
+
anonymous: current.anonymous
|
|
180
|
+
});
|
|
181
|
+
requireJsonMedia(response.headers);
|
|
182
|
+
return response.data;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw classify(error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
var MANAGE_METADATA = Object.freeze([
|
|
188
|
+
commandMetadata("manage", {
|
|
189
|
+
description: "Send one management operation to the dashboard API and print what came back. The path, the method, and the body are the caller's; the target, precondition, and credential rules are the same as for every named command. Anything that acts \u2014 every non-GET, and the GET routes the permission table gives a write or access permission \u2014 needs an explicit --site that the credential is checked against before the call.",
|
|
190
|
+
target: {
|
|
191
|
+
arguments: [
|
|
192
|
+
{ name: "method", required: true, description: "GET, POST, PUT, PATCH, or DELETE." },
|
|
193
|
+
{ name: "path", required: false, description: "The operation path under the site, such as requests. It may not name a site." }
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
effect: "Whatever the chosen operation does. A GET on a read route reads; anything else may write.",
|
|
197
|
+
idempotency: "expected_review",
|
|
198
|
+
text_sources: ["--data"],
|
|
199
|
+
output: { fields: [], ids: [], limitations: [] },
|
|
200
|
+
errors: ["invalid_input", "unauthorized", "forbidden", "not_found", "stale_review", "conflict", "rate_limited", "service_unavailable", "transport_failure", "interrupted"],
|
|
201
|
+
examples: [
|
|
202
|
+
{ command: "saturndocs manage GET requests --site acme", description: "Read one site's requests through the raw operation." },
|
|
203
|
+
{ command: "saturndocs manage POST requests --site acme --data @request.json --json", description: "Send a body read from a file and take the shared result envelope." },
|
|
204
|
+
{ command: "saturndocs manage POST suggestions/sug-1/approve --site acme --base <sha> --head <sha>", description: "Publish exactly the reviewed revision; without both hashes nothing is sent." }
|
|
205
|
+
]
|
|
206
|
+
})
|
|
207
|
+
]);
|
|
208
|
+
function registerManage(program) {
|
|
209
|
+
const metadata = MANAGE_METADATA[0];
|
|
210
|
+
const command = program.command("manage").argument("<method>", "GET, POST, PUT, PATCH, or DELETE.").argument("[path]", "The operation path under the site, such as requests.").option("--api <origin>", "Management API origin", "https://app.saturndocs.com").option("--revision <revision>", "Revision from the settings read or proposal review; required for settings writes").option("--data <json-or-@file>", "JSON request body").option("--file <field=path>", "Upload a file; repeat for multiple fields", (value, prior) => [...prior, value], []).option("--base <sha>", "The reviewed base SHA, for a raw approval or dismissal").option("--head <sha>", "The reviewed head SHA, for a raw approval or dismissal").action(async (method, path, options, self) => {
|
|
211
|
+
const globals = resolveGlobalOptions(self);
|
|
212
|
+
const writer = createResultWriter({ json: globals.json });
|
|
213
|
+
const site = globals.site ?? options.site;
|
|
214
|
+
const context = { command: "manage", siteId: site ?? null };
|
|
215
|
+
try {
|
|
216
|
+
const data = await callManagement(method, path ?? "", {
|
|
217
|
+
...options,
|
|
218
|
+
site,
|
|
219
|
+
...globals.api === null ? {} : { api: globals.api },
|
|
220
|
+
...globals.timeout === null ? {} : { timeout: globals.timeout }
|
|
221
|
+
}, process.env.SATURNDOCS_MANAGEMENT_TOKEN);
|
|
222
|
+
process.exitCode = writer.write(successResult(context, data), { text: JSON.stringify(data, null, 2) });
|
|
223
|
+
} catch (error) {
|
|
224
|
+
process.exitCode = writer.write(failureResult(error, context));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
applyCommandHelp(command, metadata);
|
|
228
|
+
command.addHelpText(
|
|
229
|
+
"after",
|
|
230
|
+
[
|
|
231
|
+
"Mutation rules, stricter than the pre-M7 command:",
|
|
232
|
+
" A path may not name a site; --site is the only target, and it is verified against the credential before any write.",
|
|
233
|
+
" Every non-GET, and GET on credentials, github/install, slack/install, or a suggestion session stream, needs --site.",
|
|
234
|
+
" A raw approve or dismiss needs --base and --head, or expected_base_sha and expected_head_sha in --data.",
|
|
235
|
+
" Without --site only GET, GET openapi.json, and GET sites answer; the first two need no credential.",
|
|
236
|
+
" Authentication: SATURNDOCS_MANAGEMENT_TOKEN. Provider installation returns an authorization URL.",
|
|
237
|
+
""
|
|
238
|
+
].join("\n")
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export {
|
|
243
|
+
callManagement,
|
|
244
|
+
MANAGE_METADATA,
|
|
245
|
+
registerManage
|
|
246
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
interruptSignal
|
|
3
|
+
} from "./chunk-KGJESHJC.js";
|
|
4
|
+
import {
|
|
5
|
+
CliError,
|
|
6
|
+
createClient,
|
|
7
|
+
isManagementError
|
|
8
|
+
} from "./chunk-GYRVGWDM.js";
|
|
9
|
+
|
|
10
|
+
// src/adapter/client.ts
|
|
11
|
+
var TOKEN_VARIABLE = "SATURNDOCS_MANAGEMENT_TOKEN";
|
|
12
|
+
function clientFor(globals, environment = {}) {
|
|
13
|
+
const env = environment.env ?? process.env;
|
|
14
|
+
const signal = environment.signal ?? interruptSignal() ?? void 0;
|
|
15
|
+
const config = {
|
|
16
|
+
token: env[TOKEN_VARIABLE],
|
|
17
|
+
...globals.api === null ? {} : { origin: globals.api },
|
|
18
|
+
...environment.fetchImpl === void 0 ? {} : { fetchImpl: environment.fetchImpl },
|
|
19
|
+
...signal === void 0 ? {} : { signal },
|
|
20
|
+
...globals.timeout === null ? {} : { deadlineMs: globals.timeout * 1e3 }
|
|
21
|
+
};
|
|
22
|
+
return createClient(config);
|
|
23
|
+
}
|
|
24
|
+
function asCliError(error) {
|
|
25
|
+
if (!isManagementError(error)) return error;
|
|
26
|
+
return new CliError(error.detail.code, error.detail.message, {
|
|
27
|
+
server: error.detail.server,
|
|
28
|
+
retryable: error.detail.retryable,
|
|
29
|
+
mutation: error.detail.mutation,
|
|
30
|
+
exit: error.detail.exit,
|
|
31
|
+
cause: error
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
TOKEN_VARIABLE,
|
|
37
|
+
clientFor,
|
|
38
|
+
asCliError
|
|
39
|
+
};
|