@limecloud/cli 0.3.0 → 0.8.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.
@@ -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("Sensitive JSON fields require --data @file, stdin, or --param-env");
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(`${label} does not match its public schema`);
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(`${label} has an unsupported value`);
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(`${label} has the wrong type`);
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(`${label} has an invalid string value`);
32
- if (typeof value === "number" && (schema.minimum !== undefined && value < schema.minimum || schema.maximum !== undefined && value > schema.maximum)) fail(`${label} is outside the allowed range`);
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(`${label} has an invalid number of items`);
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(`${label}.${required} is required`);
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(`${label}.${name} is not a declared field`);
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("Expected a numeric parameter");
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("Expected true or false");
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("Structured typed fields require a JSON value"); }
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("This public method does not accept --file");
89
- if (parsed.get("data")) fail("Use --file alone for a binary request body");
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("Choose a cropped 256 by 256 PNG no larger than 320 KiB");
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("This public method does not accept --data");
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("Sensitive fields require --param-env or private JSON input");
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(`Unknown parameter ${name}`);
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(`--${kind} requires name=value`);
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("Only contract-declared HTTP headers can be set");
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(`--param-env ${name} needs a populated environment variable`);
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(`Unknown path/query/header parameter ${key}`);
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(`Path parameter ${parameter.name} conflicts with the raw HTTP path`);
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(`Required ${parameter.in} parameter: ${parameter.name}`); continue; }
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(`Invalid path parameter ${parameter.name}`);
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("Unsafe or reserved request header");
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(`Required path parameter: ${key}`);
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 ? "This public method requires --file PNG_PATH (or --file - for stdin)" : "This public method requires --data JSON");
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", "--format must be json, ndjson, table or csv");
8
- if (parsed.get("json") && parsed.get("format") && parsed.get("format") !== "json") throw new CLIError("invalid_arguments", "--json conflicts with --format");
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", "--fields must be comma-separated field paths");
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 cells = [keys, ...rows.map((row) => keys.map((key) => text(row[key])))];
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 ? "Command cancelled" : known ? error.message : "Command failed; check local configuration and retry",
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", "Issuer must be an absolute HTTPS origin"); }
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", "Issuer must be an HTTPS origin; HTTP is allowed only on loopback");
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", "Invalid device verification URI"); }
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", "Device verification URI does not match this issuer");
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", "Endpoint must be a contract path on this issuer");
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", "Set LIMECLOUD_CLIENT_ID with LIMECLOUD_CLIENT_SECRET");
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", "Request failed or timed out; a token exchange may have completed. Do not replay an uncertain token exchange");
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", "Endpoint returned a redirect; no follow-up request was sent", response.status);
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", "Endpoint returned a non-JSON response", response.status);
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, `API request failed: ${code} (HTTP ${response.status})`, response.status,
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", "Invalid OAuth token response; start a new login");
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", "Invalid device authorization 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", "The previous device token exchange has an unknown result; start a new auth login");
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", "Invalid pending device authorization");
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", "Device authorization expired; start auth login again");
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", "Local configuration must be a regular file owned by this user with mode 0600");
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", "Unable to read private local configuration; check the file or select another path");
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", "Another command holds this configuration lock; finish it before retrying");
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", "Invalid saved credentials; use a separate LIMECLOUD_CONFIG and sign in again");
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", "Supported config keys: issuer, api_origin, client_id, scope");
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", "client_id must be a registered public client ID");
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", "scope must be a space-separated string");
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;