@prospera/eprospera-cli 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/bin/eprospera.js +3 -0
- package/cli.ocs.yaml +406 -0
- package/dist/bundle/eprospera.mjs +28349 -0
- package/dist/completions/eprospera.bash +7 -0
- package/dist/completions/eprospera.fish +2 -0
- package/dist/completions/eprospera.ps1 +7 -0
- package/dist/completions/eprospera.zsh +4 -0
- package/dist/src/api/client.d.ts +29 -0
- package/dist/src/api/client.js +131 -0
- package/dist/src/api/errors.d.ts +11 -0
- package/dist/src/api/errors.js +125 -0
- package/dist/src/api/generated.d.ts +1159 -0
- package/dist/src/api/generated.js +6 -0
- package/dist/src/commands/application/create.d.ts +5 -0
- package/dist/src/commands/application/create.js +62 -0
- package/dist/src/commands/application/get.d.ts +2 -0
- package/dist/src/commands/application/get.js +11 -0
- package/dist/src/commands/application/list.d.ts +2 -0
- package/dist/src/commands/application/list.js +8 -0
- package/dist/src/commands/application/pay.d.ts +5 -0
- package/dist/src/commands/application/pay.js +27 -0
- package/dist/src/commands/application/watch.d.ts +7 -0
- package/dist/src/commands/application/watch.js +42 -0
- package/dist/src/commands/auth/login.d.ts +7 -0
- package/dist/src/commands/auth/login.js +60 -0
- package/dist/src/commands/auth/logout.d.ts +2 -0
- package/dist/src/commands/auth/logout.js +14 -0
- package/dist/src/commands/auth/whoami.d.ts +2 -0
- package/dist/src/commands/auth/whoami.js +19 -0
- package/dist/src/commands/completion.d.ts +3 -0
- package/dist/src/commands/completion.js +80 -0
- package/dist/src/commands/config/get.d.ts +2 -0
- package/dist/src/commands/config/get.js +9 -0
- package/dist/src/commands/config/list.d.ts +2 -0
- package/dist/src/commands/config/list.js +7 -0
- package/dist/src/commands/config/set.d.ts +2 -0
- package/dist/src/commands/config/set.js +9 -0
- package/dist/src/commands/config/unset.d.ts +2 -0
- package/dist/src/commands/config/unset.js +9 -0
- package/dist/src/commands/entity/documents.d.ts +2 -0
- package/dist/src/commands/entity/documents.js +11 -0
- package/dist/src/commands/entity/get.d.ts +2 -0
- package/dist/src/commands/entity/get.js +11 -0
- package/dist/src/commands/entity/search.d.ts +2 -0
- package/dist/src/commands/entity/search.js +17 -0
- package/dist/src/commands/entity/verify.d.ts +2 -0
- package/dist/src/commands/entity/verify.js +9 -0
- package/dist/src/commands/files.d.ts +2 -0
- package/dist/src/commands/files.js +24 -0
- package/dist/src/commands/me/id-verification.d.ts +2 -0
- package/dist/src/commands/me/id-verification.js +8 -0
- package/dist/src/commands/me/profile.d.ts +2 -0
- package/dist/src/commands/me/profile.js +8 -0
- package/dist/src/commands/me/residency.d.ts +2 -0
- package/dist/src/commands/me/residency.js +8 -0
- package/dist/src/commands/runtime.d.ts +56 -0
- package/dist/src/commands/runtime.js +101 -0
- package/dist/src/commands/schema.d.ts +2 -0
- package/dist/src/commands/schema.js +13 -0
- package/dist/src/config/store.d.ts +14 -0
- package/dist/src/config/store.js +99 -0
- package/dist/src/credentials/resolve.d.ts +15 -0
- package/dist/src/credentials/resolve.js +63 -0
- package/dist/src/credentials/store.d.ts +22 -0
- package/dist/src/credentials/store.js +184 -0
- package/dist/src/credentials/types.d.ts +15 -0
- package/dist/src/credentials/types.js +5 -0
- package/dist/src/errors.d.ts +39 -0
- package/dist/src/errors.js +38 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +244 -0
- package/dist/src/output/format.d.ts +27 -0
- package/dist/src/output/format.js +126 -0
- package/dist/src/output/table.d.ts +5 -0
- package/dist/src/output/table.js +124 -0
- package/dist/src/output/tty.d.ts +26 -0
- package/dist/src/output/tty.js +41 -0
- package/dist/src/polling/watch.d.ts +19 -0
- package/dist/src/polling/watch.js +86 -0
- package/dist/src/prompts/confirm.d.ts +2 -0
- package/dist/src/prompts/confirm.js +19 -0
- package/dist/src/scopes/check.d.ts +17 -0
- package/dist/src/scopes/check.js +64 -0
- package/dist/src/scopes/map.d.ts +81 -0
- package/dist/src/scopes/map.js +76 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +35 -0
- package/package.json +70 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { type ZodType, z } from "zod";
|
|
3
|
+
import { type EProsperaApiClient, type RetryOptions } from "../api/client.js";
|
|
4
|
+
import { type CliConfig, type ConfigStoreOptions } from "../config/store.js";
|
|
5
|
+
import { type ResolveCredentialOptions } from "../credentials/resolve.js";
|
|
6
|
+
import type { CredentialSource, ResolvedCredential, StoredCredential } from "../credentials/types.js";
|
|
7
|
+
import { type OutputStreams, type PrintOptions } from "../output/format.js";
|
|
8
|
+
export type GlobalOptions = {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
raw?: boolean;
|
|
11
|
+
fields?: string;
|
|
12
|
+
quiet?: boolean;
|
|
13
|
+
yes?: boolean;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
dryRun?: boolean;
|
|
16
|
+
autoJson?: boolean;
|
|
17
|
+
skipScopeCheck?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type RuntimeDependencies = {
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
fetch?: (input: Request) => Promise<Response>;
|
|
22
|
+
retry?: RetryOptions;
|
|
23
|
+
idempotencyKey?: () => string;
|
|
24
|
+
streams?: OutputStreams;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
readFile?: typeof readFile;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
sleep?: (ms: number) => Promise<void>;
|
|
29
|
+
loadStoredCredential?: ResolveCredentialOptions["loadStoredCredential"];
|
|
30
|
+
saveStoredCredential?: (credential: StoredCredential) => Promise<CredentialSource>;
|
|
31
|
+
deleteStoredCredential?: () => Promise<boolean>;
|
|
32
|
+
configStore?: ConfigStoreOptions;
|
|
33
|
+
promptPassword?: (message: string) => Promise<string>;
|
|
34
|
+
promptInput?: (message: string) => Promise<string>;
|
|
35
|
+
promptConfirm?: (message: string) => Promise<boolean>;
|
|
36
|
+
};
|
|
37
|
+
export type AuthenticatedContext = {
|
|
38
|
+
api: EProsperaApiClient;
|
|
39
|
+
credential: ResolvedCredential;
|
|
40
|
+
output: PrintOptions;
|
|
41
|
+
globals: GlobalOptions;
|
|
42
|
+
};
|
|
43
|
+
export declare const uuidSchema: z.ZodString;
|
|
44
|
+
export declare const nonEmptyStringSchema: z.ZodString;
|
|
45
|
+
export declare const rpnSchema: z.ZodString;
|
|
46
|
+
export declare function authenticatedContext(commandId: string, globals: GlobalOptions, deps?: RuntimeDependencies): Promise<AuthenticatedContext>;
|
|
47
|
+
export declare function outputOptions(globals: GlobalOptions, deps?: RuntimeDependencies): PrintOptions;
|
|
48
|
+
export declare function resolveRuntimeBaseUrl(env: NodeJS.ProcessEnv, config: CliConfig): string;
|
|
49
|
+
export declare function configStoreOptions(deps?: RuntimeDependencies): ConfigStoreOptions;
|
|
50
|
+
export declare function parseInput<T>(schema: ZodType<T>, value: unknown): T;
|
|
51
|
+
export declare function readJsonFile<T>(filePath: string, schema: ZodType<T>, deps?: RuntimeDependencies): Promise<T>;
|
|
52
|
+
export declare function printDryRun(request: {
|
|
53
|
+
method: string;
|
|
54
|
+
path: string;
|
|
55
|
+
body?: unknown;
|
|
56
|
+
}, globals: GlobalOptions, deps?: RuntimeDependencies): void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createApiClient, resolveBaseUrl, } from "../api/client.js";
|
|
5
|
+
import { loadConfig } from "../config/store.js";
|
|
6
|
+
import { resolveCredential } from "../credentials/resolve.js";
|
|
7
|
+
import { ExitCodes, ExitError } from "../errors.js";
|
|
8
|
+
import { print } from "../output/format.js";
|
|
9
|
+
import { assertCommandScope } from "../scopes/check.js";
|
|
10
|
+
export const uuidSchema = z.string().uuid();
|
|
11
|
+
export const nonEmptyStringSchema = z.string().trim().min(1);
|
|
12
|
+
export const rpnSchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.regex(/^[89]\d{13}$/, "RPN must be 14 digits starting with 8 or 9.");
|
|
15
|
+
export async function authenticatedContext(commandId, globals, deps = {}) {
|
|
16
|
+
const env = deps.env ?? process.env;
|
|
17
|
+
const config = await loadConfig(configStoreOptions(deps));
|
|
18
|
+
const credential = await resolveCredential({
|
|
19
|
+
apiKey: globals.apiKey,
|
|
20
|
+
env,
|
|
21
|
+
store: configStoreOptions(deps),
|
|
22
|
+
loadStoredCredential: deps.loadStoredCredential,
|
|
23
|
+
});
|
|
24
|
+
assertCommandScope(commandId, credential, { skipScopeCheck: globals.skipScopeCheck });
|
|
25
|
+
return {
|
|
26
|
+
api: createApiClient({
|
|
27
|
+
baseUrl: resolveRuntimeBaseUrl(env, config),
|
|
28
|
+
env,
|
|
29
|
+
token: credential.token,
|
|
30
|
+
fetch: deps.fetch,
|
|
31
|
+
retry: deps.retry,
|
|
32
|
+
idempotencyKey: deps.idempotencyKey,
|
|
33
|
+
}),
|
|
34
|
+
credential,
|
|
35
|
+
output: outputOptions(globals, deps),
|
|
36
|
+
globals,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function outputOptions(globals, deps = {}) {
|
|
40
|
+
return {
|
|
41
|
+
json: globals.json,
|
|
42
|
+
raw: globals.raw,
|
|
43
|
+
fields: globals.fields,
|
|
44
|
+
quiet: globals.quiet,
|
|
45
|
+
noAutoJson: globals.autoJson === false,
|
|
46
|
+
env: deps.env,
|
|
47
|
+
streams: deps.streams,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function resolveRuntimeBaseUrl(env, config) {
|
|
51
|
+
if (env.EPROSPERA_BASE_URL?.trim()) {
|
|
52
|
+
return resolveBaseUrl(env);
|
|
53
|
+
}
|
|
54
|
+
if (config["api.baseUrl"]) {
|
|
55
|
+
return trimTrailingSlash(config["api.baseUrl"]);
|
|
56
|
+
}
|
|
57
|
+
return resolveBaseUrl(env);
|
|
58
|
+
}
|
|
59
|
+
export function configStoreOptions(deps = {}) {
|
|
60
|
+
const store = deps.configStore ?? {};
|
|
61
|
+
return {
|
|
62
|
+
...store,
|
|
63
|
+
env: store.env ?? deps.env,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function parseInput(schema, value) {
|
|
67
|
+
const result = schema.safeParse(value);
|
|
68
|
+
if (result.success) {
|
|
69
|
+
return result.data;
|
|
70
|
+
}
|
|
71
|
+
throw new ExitError({
|
|
72
|
+
code: "INVALID_USAGE",
|
|
73
|
+
message: result.error.issues.map((issue) => issue.message).join("; "),
|
|
74
|
+
exitCode: ExitCodes.Usage,
|
|
75
|
+
details: result.error.issues,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function readJsonFile(filePath, schema, deps = {}) {
|
|
79
|
+
const reader = deps.readFile ?? readFile;
|
|
80
|
+
const absolutePath = resolve(deps.cwd ?? process.cwd(), filePath);
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(await reader(absolutePath, "utf8"));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new ExitError({
|
|
87
|
+
code: "INVALID_JSON_FILE",
|
|
88
|
+
message: `Could not read valid JSON from ${filePath}.`,
|
|
89
|
+
exitCode: ExitCodes.Validation,
|
|
90
|
+
cause: error,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return parseInput(schema, parsed);
|
|
94
|
+
}
|
|
95
|
+
export function printDryRun(request, globals, deps = {}) {
|
|
96
|
+
print({ dryRun: true, request }, outputOptions(globals, deps));
|
|
97
|
+
}
|
|
98
|
+
function trimTrailingSlash(value) {
|
|
99
|
+
return value.replace(/\/+$/, "");
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parse } from "yaml";
|
|
2
|
+
import { print } from "../output/format.js";
|
|
3
|
+
import { readPackageFile } from "./files.js";
|
|
4
|
+
import { outputOptions } from "./runtime.js";
|
|
5
|
+
export async function runSchema(globals, deps = {}) {
|
|
6
|
+
const source = await readPackageFile("cli.ocs.yaml", import.meta.url);
|
|
7
|
+
if (globals.json || globals.raw) {
|
|
8
|
+
print(parse(source), outputOptions(globals, deps));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
(deps.streams?.stdout ?? process.stdout).write(source.endsWith("\n") ? source : `${source}\n`);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const CONFIG_SERVICE = "eprospera-cli";
|
|
2
|
+
export declare const CONFIG_FILE_NAME = "config.json";
|
|
3
|
+
export declare const SUPPORTED_CONFIG_KEYS: readonly ["api.baseUrl"];
|
|
4
|
+
export type ConfigKey = (typeof SUPPORTED_CONFIG_KEYS)[number];
|
|
5
|
+
export type CliConfig = Partial<Record<ConfigKey, string>>;
|
|
6
|
+
export type ConfigStoreOptions = {
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
homeDir?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function loadConfig(options?: ConfigStoreOptions): Promise<CliConfig>;
|
|
11
|
+
export declare function setConfigValue(key: ConfigKey, value: string, options?: ConfigStoreOptions): Promise<CliConfig>;
|
|
12
|
+
export declare function unsetConfigValue(key: ConfigKey, options?: ConfigStoreOptions): Promise<boolean>;
|
|
13
|
+
export declare function assertConfigKey(value: string): ConfigKey;
|
|
14
|
+
export declare function getConfigFilePath(options?: ConfigStoreOptions): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { ExitCodes, ExitError } from "../errors.js";
|
|
7
|
+
export const CONFIG_SERVICE = "eprospera-cli";
|
|
8
|
+
export const CONFIG_FILE_NAME = "config.json";
|
|
9
|
+
export const SUPPORTED_CONFIG_KEYS = ["api.baseUrl"];
|
|
10
|
+
const configSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
"api.baseUrl": z.string().url().optional(),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
export async function loadConfig(options = {}) {
|
|
16
|
+
const filePath = getConfigFilePath(options);
|
|
17
|
+
let text;
|
|
18
|
+
try {
|
|
19
|
+
text = await readFile(filePath, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
throw configError("CONFIG_UNREADABLE", "Local configuration could not be read.", error);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return configSchema.parse(JSON.parse(text));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw configError("CONFIG_INVALID", "Local configuration is invalid.", error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function setConfigValue(key, value, options = {}) {
|
|
35
|
+
const next = { ...(await loadConfig(options)), [key]: value };
|
|
36
|
+
await saveConfig(configSchema.parse(next), options);
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
39
|
+
export async function unsetConfigValue(key, options = {}) {
|
|
40
|
+
const current = await loadConfig(options);
|
|
41
|
+
const existed = key in current;
|
|
42
|
+
delete current[key];
|
|
43
|
+
if (Object.keys(current).length === 0) {
|
|
44
|
+
await deleteConfig(options);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await saveConfig(current, options);
|
|
48
|
+
}
|
|
49
|
+
return existed;
|
|
50
|
+
}
|
|
51
|
+
export function assertConfigKey(value) {
|
|
52
|
+
if (SUPPORTED_CONFIG_KEYS.includes(value)) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
throw new ExitError({
|
|
56
|
+
code: "INVALID_CONFIG_KEY",
|
|
57
|
+
message: `Unsupported config key ${value}. Supported keys: ${SUPPORTED_CONFIG_KEYS.join(", ")}.`,
|
|
58
|
+
exitCode: ExitCodes.Usage,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function getConfigFilePath(options = {}) {
|
|
62
|
+
return join(getConfigDir(options), CONFIG_FILE_NAME);
|
|
63
|
+
}
|
|
64
|
+
function getConfigDir(options = {}) {
|
|
65
|
+
const xdgConfigHome = options.env?.XDG_CONFIG_HOME?.trim();
|
|
66
|
+
return join(xdgConfigHome || join(options.homeDir ?? homedir(), ".config"), CONFIG_SERVICE);
|
|
67
|
+
}
|
|
68
|
+
async function saveConfig(config, options) {
|
|
69
|
+
const filePath = getConfigFilePath(options);
|
|
70
|
+
await mkdir(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
71
|
+
await chmod(dirname(filePath), 0o700);
|
|
72
|
+
await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
73
|
+
await chmod(filePath, 0o600);
|
|
74
|
+
}
|
|
75
|
+
async function deleteConfig(options) {
|
|
76
|
+
const filePath = getConfigFilePath(options);
|
|
77
|
+
try {
|
|
78
|
+
await access(filePath, constants.F_OK);
|
|
79
|
+
await rm(filePath);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw configError("CONFIG_UNWRITABLE", "Local configuration could not be updated.", error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function configError(code, message, cause) {
|
|
89
|
+
return new ExitError({
|
|
90
|
+
code,
|
|
91
|
+
message,
|
|
92
|
+
exitCode: code === "CONFIG_INVALID" ? ExitCodes.Validation : ExitCodes.Generic,
|
|
93
|
+
cause,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function isNodeError(value) {
|
|
97
|
+
return value instanceof Error && "code" in value;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CredentialStoreOptions } from "./store.js";
|
|
2
|
+
import type { CredentialKind, ResolvedCredential, StoredCredential } from "./types.js";
|
|
3
|
+
type LoadableCredential = StoredCredential & {
|
|
4
|
+
source?: Extract<ResolvedCredential["source"], "keytar" | "file">;
|
|
5
|
+
};
|
|
6
|
+
export type ResolveCredentialOptions = {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
store?: CredentialStoreOptions;
|
|
11
|
+
loadStoredCredential?: () => Promise<LoadableCredential | undefined>;
|
|
12
|
+
};
|
|
13
|
+
export declare function resolveCredential(options?: ResolveCredentialOptions): Promise<ResolvedCredential>;
|
|
14
|
+
export declare function inferCredentialKind(token: string): CredentialKind;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ExitCodes, ExitError } from "../errors.js";
|
|
2
|
+
import { loadCredential } from "./store.js";
|
|
3
|
+
export async function resolveCredential(options = {}) {
|
|
4
|
+
const env = options.env ?? process.env;
|
|
5
|
+
const flagToken = normalizeToken(options.apiKey);
|
|
6
|
+
if (flagToken) {
|
|
7
|
+
return credentialFromToken(flagToken, "flag");
|
|
8
|
+
}
|
|
9
|
+
const envToken = normalizeToken(env.EPROSPERA_API_KEY);
|
|
10
|
+
if (envToken) {
|
|
11
|
+
return credentialFromToken(envToken, "env");
|
|
12
|
+
}
|
|
13
|
+
const stored = await loadStoredCredential(options);
|
|
14
|
+
if (!stored) {
|
|
15
|
+
throw new ExitError({
|
|
16
|
+
code: "NO_CREDENTIAL",
|
|
17
|
+
message: "No API credential configured. Pass --api-key, set EPROSPERA_API_KEY, or run eprospera auth login.",
|
|
18
|
+
exitCode: ExitCodes.Authentication,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (isExpired(stored, options.now ?? Date.now)) {
|
|
22
|
+
throw new ExitError({
|
|
23
|
+
code: "EXPIRED_CREDENTIAL",
|
|
24
|
+
message: "Stored credential has expired. Run eprospera auth login again.",
|
|
25
|
+
exitCode: ExitCodes.Authentication,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...stored,
|
|
30
|
+
source: stored.source ?? "keytar",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function inferCredentialKind(token) {
|
|
34
|
+
if (token.startsWith("ak-")) {
|
|
35
|
+
return "ak";
|
|
36
|
+
}
|
|
37
|
+
if (token.startsWith("sk-")) {
|
|
38
|
+
return "sk";
|
|
39
|
+
}
|
|
40
|
+
return "sk";
|
|
41
|
+
}
|
|
42
|
+
function credentialFromToken(token, source) {
|
|
43
|
+
return {
|
|
44
|
+
kind: inferCredentialKind(token),
|
|
45
|
+
token,
|
|
46
|
+
scopes: [],
|
|
47
|
+
source,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function loadStoredCredential(options) {
|
|
51
|
+
if (options.loadStoredCredential) {
|
|
52
|
+
return options.loadStoredCredential();
|
|
53
|
+
}
|
|
54
|
+
return loadCredential(options.store);
|
|
55
|
+
}
|
|
56
|
+
function normalizeToken(value) {
|
|
57
|
+
const token = value?.trim();
|
|
58
|
+
return token && token.length > 0 ? token : undefined;
|
|
59
|
+
}
|
|
60
|
+
function isExpired(credential, now) {
|
|
61
|
+
return credential.expiresAt !== undefined && credential.expiresAt <= now();
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CredentialSource, type StoredCredential } from "./types.js";
|
|
2
|
+
export declare const CREDENTIAL_SERVICE = "eprospera-cli";
|
|
3
|
+
export declare const CREDENTIAL_ACCOUNT = "default";
|
|
4
|
+
export declare const CREDENTIAL_FILE_NAME = "credentials.json";
|
|
5
|
+
export type KeytarAdapter = {
|
|
6
|
+
getPassword(service: string, account: string): Promise<string | null>;
|
|
7
|
+
setPassword(service: string, account: string, password: string): Promise<void>;
|
|
8
|
+
deletePassword(service: string, account: string): Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
export type CredentialStoreOptions = {
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
homeDir?: string;
|
|
13
|
+
keytar?: KeytarAdapter | null;
|
|
14
|
+
};
|
|
15
|
+
export type LoadedCredential = StoredCredential & {
|
|
16
|
+
source: Extract<CredentialSource, "keytar" | "file">;
|
|
17
|
+
};
|
|
18
|
+
export declare function loadCredential(options?: CredentialStoreOptions): Promise<LoadedCredential | undefined>;
|
|
19
|
+
export declare function saveCredential(credential: StoredCredential, options?: CredentialStoreOptions): Promise<CredentialSource>;
|
|
20
|
+
export declare function deleteCredential(options?: CredentialStoreOptions): Promise<boolean>;
|
|
21
|
+
export declare function getCredentialConfigDir(options?: CredentialStoreOptions): string;
|
|
22
|
+
export declare function getCredentialFilePath(options?: CredentialStoreOptions): string;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { ExitCodes, ExitError } from "../errors.js";
|
|
6
|
+
import { isCredentialKind } from "./types.js";
|
|
7
|
+
export const CREDENTIAL_SERVICE = "eprospera-cli";
|
|
8
|
+
export const CREDENTIAL_ACCOUNT = "default";
|
|
9
|
+
export const CREDENTIAL_FILE_NAME = "credentials.json";
|
|
10
|
+
export async function loadCredential(options = {}) {
|
|
11
|
+
const keytar = await resolveKeytar(options);
|
|
12
|
+
if (keytar) {
|
|
13
|
+
const encoded = await loadKeytarCredential(keytar);
|
|
14
|
+
if (encoded) {
|
|
15
|
+
return { ...parseStoredCredential(encoded, "keytar"), source: "keytar" };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const encoded = await loadFileCredential(options);
|
|
19
|
+
return encoded ? { ...parseStoredCredential(encoded, "file"), source: "file" } : undefined;
|
|
20
|
+
}
|
|
21
|
+
export async function saveCredential(credential, options = {}) {
|
|
22
|
+
const encoded = `${JSON.stringify(normalizeStoredCredential(credential), null, 2)}\n`;
|
|
23
|
+
const keytar = await resolveKeytar(options);
|
|
24
|
+
if (keytar) {
|
|
25
|
+
try {
|
|
26
|
+
await keytar.setPassword(CREDENTIAL_SERVICE, CREDENTIAL_ACCOUNT, encoded);
|
|
27
|
+
await deleteFileCredential(options);
|
|
28
|
+
return "keytar";
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Fall through to the plaintext store when the OS keychain is unavailable.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
await saveFileCredential(encoded, options);
|
|
35
|
+
return "file";
|
|
36
|
+
}
|
|
37
|
+
export async function deleteCredential(options = {}) {
|
|
38
|
+
let deleted = false;
|
|
39
|
+
const keytar = await resolveKeytar(options);
|
|
40
|
+
if (keytar) {
|
|
41
|
+
try {
|
|
42
|
+
deleted = (await keytar.deletePassword(CREDENTIAL_SERVICE, CREDENTIAL_ACCOUNT)) || deleted;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Still attempt to clean up the fallback file.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return (await deleteFileCredential(options)) || deleted;
|
|
49
|
+
}
|
|
50
|
+
export function getCredentialConfigDir(options = {}) {
|
|
51
|
+
const xdgConfigHome = options.env?.XDG_CONFIG_HOME?.trim();
|
|
52
|
+
return join(xdgConfigHome || join(options.homeDir ?? homedir(), ".config"), CREDENTIAL_SERVICE);
|
|
53
|
+
}
|
|
54
|
+
export function getCredentialFilePath(options = {}) {
|
|
55
|
+
return join(getCredentialConfigDir(options), CREDENTIAL_FILE_NAME);
|
|
56
|
+
}
|
|
57
|
+
async function resolveKeytar(options) {
|
|
58
|
+
if (options.keytar !== undefined) {
|
|
59
|
+
return options.keytar ?? undefined;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
// Keep keytar optional in ncc bundles so standalone artifacts can use file fallback.
|
|
63
|
+
const keytarPackageName = ["keytar"].join("");
|
|
64
|
+
const keytarModule = (await import(keytarPackageName));
|
|
65
|
+
return unwrapKeytarModule(keytarModule);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function unwrapKeytarModule(module) {
|
|
72
|
+
if (isKeytarAdapter(module)) {
|
|
73
|
+
return module;
|
|
74
|
+
}
|
|
75
|
+
if (isRecord(module) && isKeytarAdapter(module.default)) {
|
|
76
|
+
return module.default;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
async function loadKeytarCredential(keytar) {
|
|
81
|
+
try {
|
|
82
|
+
return (await keytar.getPassword(CREDENTIAL_SERVICE, CREDENTIAL_ACCOUNT)) ?? undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function loadFileCredential(options) {
|
|
89
|
+
const filePath = getCredentialFilePath(options);
|
|
90
|
+
try {
|
|
91
|
+
return await readFile(filePath, "utf8");
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
throw credentialError("CREDENTIAL_STORE_UNREADABLE", "Stored credentials could not be read.", error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function saveFileCredential(encoded, options) {
|
|
101
|
+
const filePath = getCredentialFilePath(options);
|
|
102
|
+
await ensureCredentialDirectory(dirname(filePath));
|
|
103
|
+
await writeFile(filePath, encoded, { mode: 0o600 });
|
|
104
|
+
await chmod(filePath, 0o600);
|
|
105
|
+
}
|
|
106
|
+
async function deleteFileCredential(options) {
|
|
107
|
+
const filePath = getCredentialFilePath(options);
|
|
108
|
+
try {
|
|
109
|
+
await access(filePath, constants.F_OK);
|
|
110
|
+
await rm(filePath);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
throw credentialError("CREDENTIAL_STORE_UNWRITABLE", "Stored credentials could not be deleted.", error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function ensureCredentialDirectory(dirPath) {
|
|
121
|
+
await mkdir(dirPath, { recursive: true, mode: 0o700 });
|
|
122
|
+
await chmod(dirPath, 0o700);
|
|
123
|
+
}
|
|
124
|
+
function parseStoredCredential(encoded, source) {
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(encoded);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw credentialError("INVALID_CREDENTIAL", `Stored ${source} credential is not valid JSON.`, error);
|
|
131
|
+
}
|
|
132
|
+
return normalizeStoredCredential(parsed);
|
|
133
|
+
}
|
|
134
|
+
function normalizeStoredCredential(value) {
|
|
135
|
+
if (!isRecord(value) || !isCredentialKind(value.kind) || !nonEmptyString(value.token)) {
|
|
136
|
+
throw credentialError("INVALID_CREDENTIAL", "Stored credential is malformed. Run eprospera auth login again.");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
kind: value.kind,
|
|
140
|
+
token: value.token.trim(),
|
|
141
|
+
refreshToken: optionalString(value.refreshToken),
|
|
142
|
+
scopes: stringArray(value.scopes),
|
|
143
|
+
expiresAt: optionalFiniteNumber(value.expiresAt),
|
|
144
|
+
owner: optionalString(value.owner),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function credentialError(code, message, cause) {
|
|
148
|
+
return new ExitError({
|
|
149
|
+
code,
|
|
150
|
+
message,
|
|
151
|
+
exitCode: ExitCodes.Authentication,
|
|
152
|
+
cause,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function nonEmptyString(value) {
|
|
156
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
157
|
+
}
|
|
158
|
+
function optionalString(value) {
|
|
159
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
160
|
+
}
|
|
161
|
+
function optionalFiniteNumber(value) {
|
|
162
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
163
|
+
}
|
|
164
|
+
function stringArray(value) {
|
|
165
|
+
if (!Array.isArray(value)) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
169
|
+
}
|
|
170
|
+
function isKeytarAdapter(value) {
|
|
171
|
+
if (!isRecord(value)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return (typeof value.getPassword === "function" &&
|
|
175
|
+
typeof value.setPassword === "function" &&
|
|
176
|
+
typeof value.deletePassword === "function");
|
|
177
|
+
}
|
|
178
|
+
function isRecord(value) {
|
|
179
|
+
return typeof value === "object" && value !== null;
|
|
180
|
+
}
|
|
181
|
+
function isNodeError(value) {
|
|
182
|
+
return value instanceof Error && "code" in value;
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const CredentialKinds: readonly ["ak", "sk", "oauth"];
|
|
2
|
+
export type CredentialKind = (typeof CredentialKinds)[number];
|
|
3
|
+
export type StoredCredential = {
|
|
4
|
+
kind: CredentialKind;
|
|
5
|
+
token: string;
|
|
6
|
+
refreshToken?: string;
|
|
7
|
+
scopes: string[];
|
|
8
|
+
expiresAt?: number;
|
|
9
|
+
owner?: string;
|
|
10
|
+
};
|
|
11
|
+
export type CredentialSource = "flag" | "env" | "keytar" | "file";
|
|
12
|
+
export type ResolvedCredential = StoredCredential & {
|
|
13
|
+
source: CredentialSource;
|
|
14
|
+
};
|
|
15
|
+
export declare function isCredentialKind(value: unknown): value is CredentialKind;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const ExitCodes: {
|
|
2
|
+
readonly Success: 0;
|
|
3
|
+
readonly Generic: 1;
|
|
4
|
+
readonly Usage: 2;
|
|
5
|
+
readonly Authentication: 3;
|
|
6
|
+
readonly Authorization: 4;
|
|
7
|
+
readonly NotFound: 5;
|
|
8
|
+
readonly Conflict: 6;
|
|
9
|
+
readonly RateLimit: 7;
|
|
10
|
+
readonly Validation: 8;
|
|
11
|
+
readonly Timeout: 9;
|
|
12
|
+
readonly TerminalFailure: 10;
|
|
13
|
+
};
|
|
14
|
+
export type ExitCode = (typeof ExitCodes)[keyof typeof ExitCodes];
|
|
15
|
+
export type ErrorDetails = unknown;
|
|
16
|
+
export type ErrorEnvelope = {
|
|
17
|
+
error: {
|
|
18
|
+
code: string;
|
|
19
|
+
message: string;
|
|
20
|
+
httpStatus?: number;
|
|
21
|
+
details?: ErrorDetails;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export type ExitErrorOptions = {
|
|
25
|
+
code: string;
|
|
26
|
+
message: string;
|
|
27
|
+
exitCode: ExitCode;
|
|
28
|
+
httpStatus?: number;
|
|
29
|
+
details?: ErrorDetails;
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
};
|
|
32
|
+
export declare class ExitError extends Error {
|
|
33
|
+
readonly code: string;
|
|
34
|
+
readonly exitCode: ExitCode;
|
|
35
|
+
readonly httpStatus?: number;
|
|
36
|
+
readonly details?: ErrorDetails;
|
|
37
|
+
constructor(options: ExitErrorOptions);
|
|
38
|
+
toEnvelope(): ErrorEnvelope;
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const ExitCodes = {
|
|
2
|
+
Success: 0,
|
|
3
|
+
Generic: 1,
|
|
4
|
+
Usage: 2,
|
|
5
|
+
Authentication: 3,
|
|
6
|
+
Authorization: 4,
|
|
7
|
+
NotFound: 5,
|
|
8
|
+
Conflict: 6,
|
|
9
|
+
RateLimit: 7,
|
|
10
|
+
Validation: 8,
|
|
11
|
+
Timeout: 9,
|
|
12
|
+
TerminalFailure: 10,
|
|
13
|
+
};
|
|
14
|
+
export class ExitError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
exitCode;
|
|
17
|
+
httpStatus;
|
|
18
|
+
details;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
super(options.message, { cause: options.cause });
|
|
21
|
+
this.name = "ExitError";
|
|
22
|
+
this.code = options.code;
|
|
23
|
+
this.exitCode = options.exitCode;
|
|
24
|
+
this.httpStatus = options.httpStatus;
|
|
25
|
+
this.details = options.details;
|
|
26
|
+
}
|
|
27
|
+
toEnvelope() {
|
|
28
|
+
return {
|
|
29
|
+
error: {
|
|
30
|
+
code: this.code,
|
|
31
|
+
message: this.message,
|
|
32
|
+
httpStatus: this.httpStatus,
|
|
33
|
+
details: this.details,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { RuntimeDependencies } from "./commands/runtime.js";
|
|
3
|
+
export declare function createProgram(deps?: RuntimeDependencies): Command;
|
|
4
|
+
export declare function runCli(argv?: readonly string[], deps?: RuntimeDependencies): Promise<number>;
|