@limecloud/cli 0.3.0 → 0.6.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 +66 -4
- package/bin/limecloud-cli.mjs +2 -0
- package/dist/identity.json +1 -1
- package/dist/locales.json +17 -0
- package/dist/management.json +1757 -184
- package/package.json +4 -1
- package/src/cli.mjs +110 -106
- package/src/commands.mjs +5 -4
- package/src/contract.mjs +11 -15
- package/src/i18n.mjs +68 -0
- package/src/input.mjs +20 -19
- package/src/install.mjs +91 -0
- package/src/locales/en.json +153 -0
- package/src/locales/zh-CN.json +153 -0
- package/src/management.mjs +29 -28
- package/src/output.mjs +12 -6
- package/src/protocol.mjs +18 -17
- package/src/resources.mjs +11 -0
- package/src/storage.mjs +8 -7
package/src/management.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { message } from "./i18n.mjs";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { resolveSchema } from "./contract.mjs";
|
|
3
4
|
import { jsonInput, readSource } from "./input.mjs";
|
|
@@ -10,7 +11,7 @@ const sensitive = new Set(["password", "secret", "client_secret", "signing_key",
|
|
|
10
11
|
function noInlineSecrets(value) {
|
|
11
12
|
if (!value || typeof value !== "object") return;
|
|
12
13
|
for (const [key, item] of Object.entries(value)) {
|
|
13
|
-
if (sensitive.has(key) && item != null && item !== "") fail("
|
|
14
|
+
if (sensitive.has(key) && item != null && item !== "") fail(message("arguments.privateJSONRequired"));
|
|
14
15
|
noInlineSecrets(item);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -20,23 +21,23 @@ export function validateJSON(contract, input, value, label = "data", depth = 0)
|
|
|
20
21
|
const schema = resolveSchema(contract, input);
|
|
21
22
|
for (const kind of ["oneOf", "anyOf"]) if (schema[kind]) {
|
|
22
23
|
const accepted = schema[kind].filter((variant) => { try { validateJSON(contract, variant, value, label, depth + 1); return true; } catch { return false; } });
|
|
23
|
-
if (!accepted.length || kind === "oneOf" && accepted.length !== 1) fail(
|
|
24
|
+
if (!accepted.length || kind === "oneOf" && accepted.length !== 1) fail(message("arguments.schemaMismatch", { label }));
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
for (const variant of schema.allOf ?? []) validateJSON(contract, variant, value, label, depth + 1);
|
|
27
|
-
if (schema.const !== undefined && value !== schema.const || schema.enum && !schema.enum.includes(value)) fail(
|
|
28
|
+
if (schema.const !== undefined && value !== schema.const || schema.enum && !schema.enum.includes(value)) fail(message("arguments.unsupportedValue", { label }));
|
|
28
29
|
const types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];
|
|
29
30
|
if (types.length && !types.some((type) => type === "null" ? value === null : type === "array" ? Array.isArray(value) : type === "object" ? value && typeof value === "object" && !Array.isArray(value) :
|
|
30
|
-
type === "integer" ? Number.isInteger(value) : type === "number" ? typeof value === "number" && Number.isFinite(value) : typeof value === type)) fail(
|
|
31
|
-
if (typeof value === "string" && (schema.minLength !== undefined && value.length < schema.minLength || schema.maxLength !== undefined && value.length > schema.maxLength || schema.pattern && !new RegExp(schema.pattern).test(value))) fail(
|
|
32
|
-
if (typeof value === "number" && (schema.minimum !== undefined && value < schema.minimum || schema.maximum !== undefined && value > schema.maximum)) fail(
|
|
31
|
+
type === "integer" ? Number.isInteger(value) : type === "number" ? typeof value === "number" && Number.isFinite(value) : typeof value === type)) fail(message("arguments.wrongType", { label }));
|
|
32
|
+
if (typeof value === "string" && (schema.minLength !== undefined && value.length < schema.minLength || schema.maxLength !== undefined && value.length > schema.maxLength || schema.pattern && !new RegExp(schema.pattern).test(value))) fail(message("arguments.invalidString", { label }));
|
|
33
|
+
if (typeof value === "number" && (schema.minimum !== undefined && value < schema.minimum || schema.maximum !== undefined && value > schema.maximum)) fail(message("arguments.outsideRange", { label }));
|
|
33
34
|
if (Array.isArray(value)) {
|
|
34
|
-
if (schema.minItems !== undefined && value.length < schema.minItems || schema.maxItems !== undefined && value.length > schema.maxItems) fail(
|
|
35
|
+
if (schema.minItems !== undefined && value.length < schema.minItems || schema.maxItems !== undefined && value.length > schema.maxItems) fail(message("arguments.invalidItemCount", { label }));
|
|
35
36
|
value.forEach((item, index) => validateJSON(contract, schema.items, item, `${label}[${index}]`, depth + 1));
|
|
36
37
|
} else if (value && typeof value === "object") {
|
|
37
|
-
for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) fail(
|
|
38
|
+
for (const required of schema.required ?? []) if (!Object.hasOwn(value, required)) fail(message("arguments.requiredField", { label, name: required }));
|
|
38
39
|
for (const [name, item] of Object.entries(value)) {
|
|
39
|
-
if (schema.additionalProperties === false && !Object.hasOwn(schema.properties ?? {}, name)) fail(
|
|
40
|
+
if (schema.additionalProperties === false && !Object.hasOwn(schema.properties ?? {}, name)) fail(message("arguments.undeclaredField", { label, name }));
|
|
40
41
|
validateJSON(contract, schema.properties?.[name], item, `${label}.${name}`, depth + 1);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -64,15 +65,15 @@ export function matchHTTP(catalogue, method, path) {
|
|
|
64
65
|
|
|
65
66
|
function cast(raw, schema) {
|
|
66
67
|
if (schema?.type === "integer" || schema?.type === "number") {
|
|
67
|
-
if (String(raw).trim() === "" || !Number.isFinite(Number(raw))) fail("
|
|
68
|
+
if (String(raw).trim() === "" || !Number.isFinite(Number(raw))) fail(message("arguments.numericRequired"));
|
|
68
69
|
return Number(raw);
|
|
69
70
|
}
|
|
70
71
|
if (schema?.type === "boolean") {
|
|
71
|
-
if (raw !== "true" && raw !== "false" && typeof raw !== "boolean") fail("
|
|
72
|
+
if (raw !== "true" && raw !== "false" && typeof raw !== "boolean") fail(message("arguments.booleanRequired"));
|
|
72
73
|
return raw === true || raw === "true";
|
|
73
74
|
}
|
|
74
75
|
if (["object", "array"].includes(schema?.type) && typeof raw === "string") {
|
|
75
|
-
try { return JSON.parse(raw); } catch { fail("
|
|
76
|
+
try { return JSON.parse(raw); } catch { fail(message("arguments.structuredJSONRequired")); }
|
|
76
77
|
}
|
|
77
78
|
return raw;
|
|
78
79
|
}
|
|
@@ -85,11 +86,11 @@ export async function prepareManagement(contract, operation, parsed, env, readIn
|
|
|
85
86
|
const binaryType = Object.keys(operation.requestBody?.content ?? {}).find((type) => type === "image/png");
|
|
86
87
|
let binaryBody;
|
|
87
88
|
if (parsed.get("file")) {
|
|
88
|
-
if (!binaryType) fail("
|
|
89
|
-
if (parsed.get("data")) fail("
|
|
89
|
+
if (!binaryType) fail(message("arguments.fileUnsupported"));
|
|
90
|
+
if (parsed.get("data")) fail(message("arguments.binaryBodyConflict"));
|
|
90
91
|
binaryBody = Buffer.from(await readBinary(parsed.get("file") === "-" ? 0 : parsed.get("file"), 320 << 10));
|
|
91
92
|
if (binaryBody.length > 320 << 10 || binaryBody.length < 33 || !binaryBody.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ||
|
|
92
|
-
binaryBody.toString("ascii", 12, 16) !== "IHDR" || binaryBody.readUInt32BE(16) !== 256 || binaryBody.readUInt32BE(20) !== 256) fail("
|
|
93
|
+
binaryBody.toString("ascii", 12, 16) !== "IHDR" || binaryBody.readUInt32BE(16) !== 256 || binaryBody.readUInt32BE(20) !== 256) fail(message("arguments.invalidPNG"));
|
|
93
94
|
bodyProvided = true;
|
|
94
95
|
}
|
|
95
96
|
if (parsed.get("params")) {
|
|
@@ -98,27 +99,27 @@ export async function prepareManagement(contract, operation, parsed, env, readIn
|
|
|
98
99
|
Object.assign(values, value);
|
|
99
100
|
}
|
|
100
101
|
if (parsed.get("data")) {
|
|
101
|
-
if (!inputSchema) fail("
|
|
102
|
+
if (!inputSchema) fail(message("arguments.dataUnsupported"));
|
|
102
103
|
const input = await jsonInput(parsed.get("data"), readInput);
|
|
103
104
|
if (!input.indirect) noInlineSecrets(input.value);
|
|
104
105
|
Object.assign(body, input.value); bodyProvided = true;
|
|
105
106
|
}
|
|
106
107
|
const declaration = (name) => declarations.find((item) => item.name === name || item.in === "header" && item.name.toLowerCase() === name.toLowerCase());
|
|
107
108
|
function apply(name, raw, privateInput) {
|
|
108
|
-
if (!privateInput && sensitive.has(name)) fail("
|
|
109
|
+
if (!privateInput && sensitive.has(name)) fail(message("arguments.privateFieldRequired"));
|
|
109
110
|
const parameter = declaration(name);
|
|
110
111
|
const property = inputSchema?.properties?.[name];
|
|
111
|
-
if (!parameter && !property) fail(
|
|
112
|
+
if (!parameter && !property) fail(message("arguments.unknownParameter", { name }));
|
|
112
113
|
if (parameter) values[parameter.name] = cast(raw, parameter.schema);
|
|
113
114
|
else { body[name] = cast(raw, resolveSchema(contract, property)); bodyProvided = true; }
|
|
114
115
|
}
|
|
115
116
|
for (const kind of ["param", "param-env", "header"]) for (const pair of parsed.flags.get(kind) ?? []) {
|
|
116
117
|
const index = pair.indexOf("=");
|
|
117
|
-
if (index < 1) fail(
|
|
118
|
+
if (index < 1) fail(message("arguments.nameValueRequired", { flag: kind }));
|
|
118
119
|
const name = pair.slice(0, index), raw = pair.slice(index + 1);
|
|
119
|
-
if (kind === "header" && !declarations.some((item) => item.in === "header" && item.name.toLowerCase() === name.toLowerCase())) fail("
|
|
120
|
+
if (kind === "header" && !declarations.some((item) => item.in === "header" && item.name.toLowerCase() === name.toLowerCase())) fail(message("arguments.headerUndeclared"));
|
|
120
121
|
if (kind === "param-env") {
|
|
121
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw) || !env[raw]) fail(
|
|
122
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw) || !env[raw]) fail(message("arguments.environmentValueRequired", { name }));
|
|
122
123
|
apply(name, env[raw], true);
|
|
123
124
|
} else apply(name, raw, false);
|
|
124
125
|
}
|
|
@@ -129,30 +130,30 @@ export async function prepareManagement(contract, operation, parsed, env, readIn
|
|
|
129
130
|
apply(name, field?.type === "array" && items.length > 1 ? items : items[0], false);
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
|
-
for (const key of Object.keys(values)) if (!declaration(key)) fail(
|
|
133
|
+
for (const key of Object.keys(values)) if (!declaration(key)) fail(message("arguments.unknownRequestParameter", { name: key }));
|
|
133
134
|
const query = new URLSearchParams(), headers = {}, bindings = { ...rawBindings };
|
|
134
135
|
for (const parameter of declarations) {
|
|
135
136
|
let value = values[parameter.name];
|
|
136
137
|
if (parameter.in === "path" && rawBindings[parameter.name] !== undefined) {
|
|
137
|
-
if (value !== undefined && String(value) !== rawBindings[parameter.name]) fail(
|
|
138
|
+
if (value !== undefined && String(value) !== rawBindings[parameter.name]) fail(message("arguments.pathConflict", { name: parameter.name }));
|
|
138
139
|
value = rawBindings[parameter.name];
|
|
139
140
|
}
|
|
140
|
-
if (value === undefined) { if (parameter.required) fail(
|
|
141
|
+
if (value === undefined) { if (parameter.required) fail(message("arguments.requiredRequestParameter", { location: parameter.in, name: parameter.name })); continue; }
|
|
141
142
|
value = cast(value, parameter.schema);
|
|
142
143
|
validateJSON(contract, parameter.schema, value, parameter.name);
|
|
143
144
|
if (parameter.in === "path") {
|
|
144
|
-
if (typeof value !== "string" || !value || value === "." || value === ".." || /[/\\?#\x00-\x1f]/.test(value)) fail(
|
|
145
|
+
if (typeof value !== "string" || !value || value === "." || value === ".." || /[/\\?#\x00-\x1f]/.test(value)) fail(message("arguments.invalidPathParameter", { name: parameter.name }));
|
|
145
146
|
bindings[parameter.name] = value;
|
|
146
147
|
} else if (parameter.in === "header") {
|
|
147
|
-
if (["authorization", "cookie", "host", "content-type"].includes(parameter.name.toLowerCase()) || /[\r\n\0]/.test(String(value))) fail("
|
|
148
|
+
if (["authorization", "cookie", "host", "content-type"].includes(parameter.name.toLowerCase()) || /[\r\n\0]/.test(String(value))) fail(message("arguments.unsafeHeader"));
|
|
148
149
|
headers[parameter.name] = String(value);
|
|
149
150
|
} else if (parameter.in === "query") for (const item of Array.isArray(value) ? value : [value]) query.append(parameter.name, String(item));
|
|
150
151
|
}
|
|
151
152
|
const path = operation.path.replace(/\{([^}]+)\}/g, (_match, key) => {
|
|
152
|
-
if (bindings[key] === undefined) fail(
|
|
153
|
+
if (bindings[key] === undefined) fail(message("arguments.requiredPathParameter", { name: key }));
|
|
153
154
|
return encodeURIComponent(bindings[key]);
|
|
154
155
|
});
|
|
155
|
-
if (operation.requestBody?.required && !bodyProvided) fail(binaryType ? "
|
|
156
|
+
if (operation.requestBody?.required && !bodyProvided) fail(binaryType ? message("arguments.fileRequired") : message("arguments.dataRequired"));
|
|
156
157
|
if (bodyProvided && !binaryBody) validateJSON(contract, inputSchema, body);
|
|
157
158
|
return { path, query, extraHeaders: headers, ...(binaryBody ? { binaryBody, binaryType,
|
|
158
159
|
binarySummary: { media_type: binaryType, bytes: binaryBody.length, width: 256, height: 256, sha256: createHash("sha256").update(binaryBody).digest("hex") } } :
|
package/src/output.mjs
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { currentLocalizer, message } from "./i18n.mjs";
|
|
1
2
|
import { CLIError, redact } from "./protocol.mjs";
|
|
2
3
|
|
|
3
4
|
export const formats = ["json", "ndjson", "table", "csv"];
|
|
5
|
+
const columnLabels = {
|
|
6
|
+
command: "columns.command", operationId: "columns.operationId", method: "columns.method",
|
|
7
|
+
path: "columns.path", summary: "columns.summary", description: "columns.description", value: "columns.value",
|
|
8
|
+
};
|
|
4
9
|
|
|
5
10
|
export function outputOptions(parsed) {
|
|
6
11
|
const format = parsed.get("json") ? "json" : parsed.get("format") ?? "json";
|
|
7
|
-
if (!formats.includes(format)) throw new CLIError("invalid_arguments", "
|
|
8
|
-
if (parsed.get("json") && parsed.get("format") && parsed.get("format") !== "json") throw new CLIError("invalid_arguments", "
|
|
12
|
+
if (!formats.includes(format)) throw new CLIError("invalid_arguments", message("arguments.invalidFormat"));
|
|
13
|
+
if (parsed.get("json") && parsed.get("format") && parsed.get("format") !== "json") throw new CLIError("invalid_arguments", message("arguments.formatConflict"));
|
|
9
14
|
const fields = parsed.get("fields")?.split(",").map((value) => value.trim());
|
|
10
15
|
if (fields?.some((value) => !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(value) || value.split(".").some((part) => ["__proto__", "prototype", "constructor"].includes(part)))) {
|
|
11
|
-
throw new CLIError("invalid_arguments", "
|
|
16
|
+
throw new CLIError("invalid_arguments", message("arguments.invalidFields"));
|
|
12
17
|
}
|
|
13
18
|
return { format, fields };
|
|
14
19
|
}
|
|
@@ -38,18 +43,19 @@ export function success(io, value, { format = "json", fields, identity = "public
|
|
|
38
43
|
const quote = (item) => `"${String(item ?? "").replaceAll('"', '""')}"`;
|
|
39
44
|
io.stdout.write(`${[keys, ...rows.map((row) => keys.map((key) => typeof row[key] === "object" && row[key] !== null ? JSON.stringify(row[key]) : row[key]))].map((row) => row.map(quote).join(",")).join("\n")}\n`);
|
|
40
45
|
} else {
|
|
41
|
-
const
|
|
46
|
+
const headings = keys.map(key => Object.hasOwn(columnLabels, key) ? currentLocalizer().t(columnLabels[key]) : key);
|
|
47
|
+
const cells = [headings, ...rows.map((row) => keys.map((key) => text(row[key])))];
|
|
42
48
|
const widths = keys.map((_key, index) => Math.min(80, Math.max(...cells.map((row) => row[index].length))));
|
|
43
49
|
io.stdout.write(`${cells.map((row) => row.map((cell, index) => text(cell).slice(0, widths[index]).padEnd(widths[index])).join(" ").trimEnd()).join("\n")}\n`);
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
export function errorEnvelope(error, cancelled) {
|
|
54
|
+
export function errorEnvelope(error, cancelled, localizer = currentLocalizer()) {
|
|
49
55
|
const known = error instanceof CLIError;
|
|
50
56
|
const code = cancelled ? "cancelled" : known ? error.code : "cli_error";
|
|
51
57
|
const type = error?.status ? "api" : ["login_required", "token_expired", "missing_scopes"].includes(code) ? "auth" :
|
|
52
58
|
code === "request_failed" ? "transport" : code === "invalid_arguments" || code === "invalid_issuer" ? "validation" : "cli";
|
|
53
|
-
return { ok: false, error: { type, ...(type === "validation" ? { subtype: "invalid_argument" } : {}), code, message: cancelled ? "
|
|
59
|
+
return { ok: false, error: { type, ...(type === "validation" ? { subtype: "invalid_argument" } : {}), code, message: cancelled ? localizer.t("errors.cancelled") : known ? error.message : localizer.t("errors.failed"),
|
|
54
60
|
...(known && error.status ? { status: error.status } : {}), ...(known && error.retryAfter ? { retry_after: error.retryAfter } : {}) } };
|
|
55
61
|
}
|
package/src/protocol.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { currentLocalizer, message, translate } from "./i18n.mjs";
|
|
1
2
|
export class CLIError extends Error {
|
|
2
3
|
constructor(code, message, status, retryAfter) {
|
|
3
|
-
super(message);
|
|
4
|
+
super(typeof message === "object" && message?.key ? translate(message.key, message.values) : message);
|
|
4
5
|
this.code = code;
|
|
5
6
|
this.status = status;
|
|
6
7
|
this.retryAfter = retryAfter;
|
|
@@ -9,20 +10,20 @@ export class CLIError extends Error {
|
|
|
9
10
|
|
|
10
11
|
export function issuerOrigin(value) {
|
|
11
12
|
let url;
|
|
12
|
-
try { url = new URL(value); } catch { throw new CLIError("invalid_issuer", "
|
|
13
|
+
try { url = new URL(value); } catch { throw new CLIError("invalid_issuer", message("protocol.invalidIssuer")); }
|
|
13
14
|
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
14
15
|
if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) || url.username || url.password ||
|
|
15
16
|
url.pathname !== "/" || url.search || url.hash) {
|
|
16
|
-
throw new CLIError("invalid_issuer", "
|
|
17
|
+
throw new CLIError("invalid_issuer", message("protocol.insecureIssuer"));
|
|
17
18
|
}
|
|
18
19
|
return url.origin;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function verificationURI(value, issuer) {
|
|
22
23
|
let url;
|
|
23
|
-
try { url = new URL(value); } catch { throw new CLIError("invalid_response", "
|
|
24
|
+
try { url = new URL(value); } catch { throw new CLIError("invalid_response", message("protocol.invalidVerificationURI")); }
|
|
24
25
|
if (url.origin !== issuer || url.username || url.password || url.pathname !== "/auth/device") {
|
|
25
|
-
throw new CLIError("invalid_response", "
|
|
26
|
+
throw new CLIError("invalid_response", message("protocol.verificationIssuerMismatch"));
|
|
26
27
|
}
|
|
27
28
|
return url.href;
|
|
28
29
|
}
|
|
@@ -38,12 +39,12 @@ export async function protocolRequest({ issuer, origin = issuer, path, method =
|
|
|
38
39
|
const targetOrigin = issuerOrigin(origin);
|
|
39
40
|
const url = new URL(path, targetOrigin);
|
|
40
41
|
if (url.origin !== targetOrigin || !path.startsWith("/") || path.startsWith("//") || url.pathname !== path || url.hash || url.search) {
|
|
41
|
-
throw new CLIError("invalid_endpoint", "
|
|
42
|
+
throw new CLIError("invalid_endpoint", message("protocol.invalidEndpoint"));
|
|
42
43
|
}
|
|
43
|
-
const headers = { ...extraHeaders, Accept: "application/json" };
|
|
44
|
+
const headers = { ...extraHeaders, Accept: "application/json", "Accept-Language": currentLocalizer().locale };
|
|
44
45
|
const form = new URLSearchParams(parameters);
|
|
45
46
|
if (clientSecret) {
|
|
46
|
-
if (!clientID) throw new CLIError("client_id_required", "
|
|
47
|
+
if (!clientID) throw new CLIError("client_id_required", message("protocol.basicClientRequired"));
|
|
47
48
|
// OAuth Basic credentials use form encoding before Base64 encoding.
|
|
48
49
|
const encode = (value) => new URLSearchParams({ v: value }).toString().slice(2);
|
|
49
50
|
headers.Authorization = `Basic ${Buffer.from(`${encode(clientID)}:${encode(clientSecret)}`).toString("base64")}`;
|
|
@@ -63,17 +64,17 @@ export async function protocolRequest({ issuer, origin = issuer, path, method =
|
|
|
63
64
|
redirect: "manual", signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000),
|
|
64
65
|
});
|
|
65
66
|
} catch {
|
|
66
|
-
throw new CLIError("request_failed", "
|
|
67
|
+
throw new CLIError("request_failed", message("protocol.requestUncertain"));
|
|
67
68
|
}
|
|
68
69
|
if (response.status >= 300 && response.status < 400) {
|
|
69
|
-
throw new CLIError("redirect_refused", "
|
|
70
|
+
throw new CLIError("redirect_refused", message("protocol.redirectRefused"), response.status);
|
|
70
71
|
}
|
|
71
72
|
let data;
|
|
72
73
|
const body = await response.text();
|
|
73
74
|
if (!body) data = null;
|
|
74
75
|
else {
|
|
75
76
|
try { data = JSON.parse(body); } catch {
|
|
76
|
-
throw new CLIError("invalid_response", "
|
|
77
|
+
throw new CLIError("invalid_response", message("protocol.nonJSONResponse"), response.status);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
if (!response.ok) {
|
|
@@ -81,7 +82,7 @@ export async function protocolRequest({ issuer, origin = issuer, path, method =
|
|
|
81
82
|
const code = typeof errorCode === "string" && /^[A-Za-z0-9_.:-]{1,80}$/.test(errorCode) ? errorCode : "http_error";
|
|
82
83
|
// Do not echo error descriptions: a server may include submitted secrets in them.
|
|
83
84
|
const retry = Number(response.headers.get("Retry-After"));
|
|
84
|
-
throw new CLIError(code,
|
|
85
|
+
throw new CLIError(code, message("protocol.apiFailed", { code, status: response.status }), response.status,
|
|
85
86
|
Number.isFinite(retry) && retry > 0 ? retry : undefined);
|
|
86
87
|
}
|
|
87
88
|
return data;
|
|
@@ -92,7 +93,7 @@ export function tokenRecord(tokens, context, now = Date.now()) {
|
|
|
92
93
|
!/^bearer$/i.test(tokens.token_type) || !Number.isFinite(tokens.expires_in) || tokens.expires_in <= 0 || tokens.expires_in > 31_536_000 ||
|
|
93
94
|
(tokens.scope !== undefined && typeof tokens.scope !== "string") ||
|
|
94
95
|
(tokens.refresh_token !== undefined && (typeof tokens.refresh_token !== "string" || !tokens.refresh_token))) {
|
|
95
|
-
throw new CLIError("invalid_response", "
|
|
96
|
+
throw new CLIError("invalid_response", message("protocol.invalidTokenResponse"));
|
|
96
97
|
}
|
|
97
98
|
return { issuer: context.issuer, ...(context.apiOrigin ? { api_origin: context.apiOrigin } : {}), client_id: context.clientID, access_token: tokens.access_token,
|
|
98
99
|
refresh_token: tokens.refresh_token, scope: tokens.scope ?? "", expires_at: new Date(now + tokens.expires_in * 1000).toISOString() };
|
|
@@ -105,7 +106,7 @@ export async function startDevice({ context, request = protocolRequest, now = Da
|
|
|
105
106
|
if (!device || typeof device.device_code !== "string" || !device.device_code || typeof device.user_code !== "string" ||
|
|
106
107
|
!/^[A-Z2-7]{4}-[A-Z2-7]{4}$/.test(device.user_code) || !Number.isInteger(device.interval) || device.interval <= 0 ||
|
|
107
108
|
!Number.isInteger(device.expires_in) || device.expires_in <= 0 || device.expires_in > 3600) {
|
|
108
|
-
throw new CLIError("invalid_response", "
|
|
109
|
+
throw new CLIError("invalid_response", message("protocol.invalidDeviceResponse"));
|
|
109
110
|
}
|
|
110
111
|
return { issuer: context.issuer, ...(context.apiOrigin ? { api_origin: context.apiOrigin } : {}), client_id: context.clientID, device_code: device.device_code, user_code: device.user_code,
|
|
111
112
|
verification_uri: verificationURI(device.verification_uri_complete ?? device.verification_uri, context.issuer),
|
|
@@ -113,11 +114,11 @@ export async function startDevice({ context, request = protocolRequest, now = Da
|
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
export async function pollDevice({ pending, request = protocolRequest, wait, now = Date.now, persist = async () => {} }) {
|
|
116
|
-
if (pending.exchange_pending) throw new CLIError("login_required", "
|
|
117
|
+
if (pending.exchange_pending) throw new CLIError("login_required", message("protocol.deviceExchangeUncertain"));
|
|
117
118
|
const deadline = Date.parse(pending.expires_at);
|
|
118
119
|
const context = { issuer: pending.issuer, apiOrigin: pending.api_origin, clientID: pending.client_id };
|
|
119
120
|
if (!Number.isFinite(deadline) || !Number.isFinite(pending.next_poll_at) || !Number.isInteger(pending.interval) || pending.interval <= 0 ||
|
|
120
|
-
typeof pending.device_code !== "string" || !pending.device_code || !context.clientID) throw new CLIError("invalid_credentials", "
|
|
121
|
+
typeof pending.device_code !== "string" || !pending.device_code || !context.clientID) throw new CLIError("invalid_credentials", message("protocol.invalidPendingDevice"));
|
|
121
122
|
const state = { ...pending };
|
|
122
123
|
const form = new URLSearchParams({ client_id: context.clientID, grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: state.device_code });
|
|
123
124
|
while (now() < deadline) {
|
|
@@ -137,7 +138,7 @@ export async function pollDevice({ pending, request = protocolRequest, wait, now
|
|
|
137
138
|
await persist(state);
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
throw new CLIError("expired_token", "
|
|
141
|
+
throw new CLIError("expired_token", message("protocol.deviceExpired"));
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
export async function deviceLogin({ context, request = protocolRequest, notify, wait, now = Date.now, parameters = new URLSearchParams() }) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
let workspace = false;
|
|
4
|
+
try {
|
|
5
|
+
workspace = JSON.parse(readFileSync(new URL("../../../package.json", import.meta.url), "utf8")).name === "lime-cloud";
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error.code !== "ENOENT") throw error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const contractURL = (name) => new URL(workspace ? `../../../contracts/openapi/${name}.json` : `../dist/${name}.json`, import.meta.url);
|
|
11
|
+
export const localeRegistryURL = new URL(workspace ? "../../../contracts/i18n/locales.json" : "../dist/locales.json", import.meta.url);
|
package/src/storage.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { message } from "./i18n.mjs";
|
|
1
2
|
import { constants } from "node:fs";
|
|
2
3
|
import { mkdir, open, rename, unlink } from "node:fs/promises";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -15,7 +16,7 @@ export function settingsPath(env = process.env) {
|
|
|
15
16
|
|
|
16
17
|
function checkPrivate(stat) {
|
|
17
18
|
if (!stat.isFile() || (process.platform !== "win32" && ((stat.mode & 0o077) || stat.uid !== process.getuid()))) {
|
|
18
|
-
throw new CLIError("unsafe_credentials", "
|
|
19
|
+
throw new CLIError("unsafe_credentials", message("storage.unsafeFile"));
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -32,7 +33,7 @@ export class PrivateJSONStore {
|
|
|
32
33
|
} catch (error) {
|
|
33
34
|
if (error.code === "ENOENT") return null;
|
|
34
35
|
if (error instanceof CLIError) throw error;
|
|
35
|
-
throw new CLIError("invalid_credentials", "
|
|
36
|
+
throw new CLIError("invalid_credentials", message("storage.unreadable"));
|
|
36
37
|
} finally { await file?.close(); }
|
|
37
38
|
}
|
|
38
39
|
async write(record) {
|
|
@@ -49,7 +50,7 @@ export class PrivateJSONStore {
|
|
|
49
50
|
let lock;
|
|
50
51
|
try { lock = await open(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600); }
|
|
51
52
|
catch (error) {
|
|
52
|
-
if (error.code === "EEXIST") throw new CLIError("credentials_busy", "
|
|
53
|
+
if (error.code === "EEXIST") throw new CLIError("credentials_busy", message("storage.busy"));
|
|
53
54
|
throw error;
|
|
54
55
|
}
|
|
55
56
|
try { await lock.writeFile(`${process.pid}\n`); return await callback(); }
|
|
@@ -66,25 +67,25 @@ export class CredentialStore extends PrivateJSONStore {
|
|
|
66
67
|
if (typeof data.client_id !== "string" || !data.client_id || typeof data.access_token !== "string" ||
|
|
67
68
|
!Number.isFinite(Date.parse(data.expires_at)) ||
|
|
68
69
|
(data.refresh_token !== undefined && typeof data.refresh_token !== "string")) {
|
|
69
|
-
throw new CLIError("invalid_credentials", "
|
|
70
|
+
throw new CLIError("invalid_credentials", message("storage.invalidCredentials"));
|
|
70
71
|
}
|
|
71
72
|
return data;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export function validateSettings(value) {
|
|
76
|
-
if (Object.keys(value).some((key) => !["issuer", "api_origin", "client_id", "scope"].includes(key))) throw new CLIError("invalid_arguments", "
|
|
77
|
+
if (Object.keys(value).some((key) => !["issuer", "api_origin", "client_id", "scope"].includes(key))) throw new CLIError("invalid_arguments", message("arguments.configKeys"));
|
|
77
78
|
const result = {};
|
|
78
79
|
if (value.issuer !== undefined) result.issuer = issuerOrigin(value.issuer);
|
|
79
80
|
if (value.api_origin !== undefined) result.api_origin = issuerOrigin(value.api_origin);
|
|
80
81
|
if (value.client_id !== undefined) {
|
|
81
82
|
if (typeof value.client_id !== "string" || !value.client_id || value.client_id.length > 200 || /[\x00-\x20\x7f]/.test(value.client_id)) {
|
|
82
|
-
throw new CLIError("invalid_arguments", "
|
|
83
|
+
throw new CLIError("invalid_arguments", message("arguments.invalidClientID"));
|
|
83
84
|
}
|
|
84
85
|
result.client_id = value.client_id;
|
|
85
86
|
}
|
|
86
87
|
if (value.scope !== undefined) {
|
|
87
|
-
if (typeof value.scope !== "string") throw new CLIError("invalid_arguments", "
|
|
88
|
+
if (typeof value.scope !== "string") throw new CLIError("invalid_arguments", message("arguments.invalidScope"));
|
|
88
89
|
result.scope = scopeList(value.scope).join(" ");
|
|
89
90
|
}
|
|
90
91
|
return result;
|