@settlemint/dalp-cli 3.1.32-main.42194559254 → 3.1.32-main.42209626311

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.
@@ -0,0 +1,41 @@
1
+ export interface CoverageOperation {
2
+ readonly name: string;
3
+ readonly hintKey: string;
4
+ }
5
+
6
+ export function findSurfaceGaps(
7
+ sdkOperations: ReadonlyArray<string>,
8
+ hasSurface: (name: string) => boolean
9
+ ): Array<string> {
10
+ return sdkOperations.filter((name) => !hasSurface(name));
11
+ }
12
+
13
+ const MANIFEST_HINT_KEY = /\b[A-Z]+ \/\S+/g;
14
+
15
+ export function findMountGaps(operations: ReadonlyArray<CoverageOperation>, manifest: string): Array<string> {
16
+ const mounted = new Set(manifest.match(MANIFEST_HINT_KEY));
17
+ return operations.filter((operation) => !mounted.has(operation.hintKey)).map((operation) => operation.name);
18
+ }
19
+
20
+ export interface ExemptionGaps {
21
+ readonly stale: ReadonlyArray<string>;
22
+ readonly reasonless: ReadonlyArray<string>;
23
+ }
24
+
25
+ export function findExemptionGaps(
26
+ exemptions: ReadonlyMap<string, string>,
27
+ sdkOperations: ReadonlyArray<string>
28
+ ): ExemptionGaps {
29
+ const live = new Set(sdkOperations);
30
+ const stale: Array<string> = [];
31
+ const reasonless: Array<string> = [];
32
+ for (const [name, reason] of exemptions) {
33
+ if (!live.has(name)) {
34
+ stale.push(name);
35
+ }
36
+ if (reason.trim().length === 0) {
37
+ reasonless.push(name);
38
+ }
39
+ }
40
+ return { stale, reasonless };
41
+ }
@@ -0,0 +1,222 @@
1
+ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { getConfigDir } from "./cli-config.ts";
5
+ import { makeCliError, isCliFailure } from "./errors.ts";
6
+
7
+ const SERVICE_NAME = "dalp-cli";
8
+
9
+ const StoredCredentialsSchema = z.object({
10
+ apiKey: z.string().min(1),
11
+ apiKeyId: z.string().min(1),
12
+ userEmail: z.string().min(1),
13
+ userId: z.string().min(1),
14
+ activeOrgId: z.string().min(1).optional(),
15
+ apiUrl: z.url(),
16
+ });
17
+
18
+ export type StoredCredentials = z.infer<typeof StoredCredentialsSchema>;
19
+
20
+ export interface CredentialStoreOptions {
21
+ readonly configDir?: string;
22
+ readonly platform?: NodeJS.Platform;
23
+ readonly runSecurity?: (
24
+ args: ReadonlyArray<string>
25
+ ) => Promise<{ readonly exitCode: number; readonly stdout: string }>;
26
+ }
27
+
28
+ const defaultRunSecurity = async (
29
+ args: ReadonlyArray<string>
30
+ ): Promise<{ readonly exitCode: number; readonly stdout: string }> => {
31
+ const proc = Bun.spawn(["security", ...args], { stdout: "pipe", stderr: "pipe" });
32
+ const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
33
+ return { exitCode, stdout };
34
+ };
35
+
36
+ function credentialsFile(configDir: string): string {
37
+ return path.join(configDir, "credentials.json");
38
+ }
39
+
40
+ async function keychainAccounts(
41
+ runSecurity: NonNullable<CredentialStoreOptions["runSecurity"]>
42
+ ): Promise<Array<string>> {
43
+ const dump = await runSecurity(["dump-keychain"]);
44
+ if (dump.exitCode !== 0) {
45
+ throw makeCliError({
46
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
47
+ message: `Keychain dump failed with exit code ${dump.exitCode}`,
48
+ });
49
+ }
50
+ const accounts = new Set<string>();
51
+ for (const entry of dump.stdout.split(/\n(?=keychain:)/u)) {
52
+ if (!entry.includes(`"svce"<blob>="${SERVICE_NAME}"`)) {
53
+ continue;
54
+ }
55
+ const account = entry.match(/"acct"<blob>="([^"]*)"/u)?.[1];
56
+ if (account !== undefined) {
57
+ accounts.add(account);
58
+ }
59
+ }
60
+ return [...accounts];
61
+ }
62
+
63
+ async function deleteKeychainAccount(
64
+ runSecurity: NonNullable<CredentialStoreOptions["runSecurity"]>,
65
+ account: string
66
+ ): Promise<void> {
67
+ const result = await runSecurity(["delete-generic-password", "-s", SERVICE_NAME, "-a", account]);
68
+ if (result.exitCode !== 0) {
69
+ throw makeCliError({
70
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
71
+ message: `Keychain delete failed for ${account} with exit code ${result.exitCode}`,
72
+ });
73
+ }
74
+ }
75
+
76
+ function resolvePlatform(options: CredentialStoreOptions): NodeJS.Platform {
77
+ if (options.platform !== undefined) {
78
+ return options.platform;
79
+ }
80
+ const override = process.env.DALP_CREDENTIAL_PLATFORM;
81
+ if (override === "darwin" || override === "linux" || override === "win32") {
82
+ return override;
83
+ }
84
+ return process.platform;
85
+ }
86
+
87
+ export async function saveCredentials(
88
+ credentials: StoredCredentials,
89
+ options: CredentialStoreOptions = {}
90
+ ): Promise<void> {
91
+ const parsed = StoredCredentialsSchema.parse(credentials);
92
+ const configDir = options.configDir ?? getConfigDir();
93
+ const platform = resolvePlatform(options);
94
+ const runSecurity = options.runSecurity ?? defaultRunSecurity;
95
+ const encoded = JSON.stringify(parsed);
96
+
97
+ if (platform === "darwin") {
98
+ const existing = await keychainAccounts(runSecurity);
99
+ const result = await runSecurity([
100
+ "add-generic-password",
101
+ "-s",
102
+ SERVICE_NAME,
103
+ "-a",
104
+ parsed.userEmail,
105
+ "-w",
106
+ encoded,
107
+ "-U",
108
+ ]);
109
+ if (result.exitCode !== 0) {
110
+ throw makeCliError({
111
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
112
+ message: `Keychain save failed with exit code ${result.exitCode}`,
113
+ });
114
+ }
115
+ try {
116
+ for (const account of existing.filter((name) => name !== parsed.userEmail)) {
117
+ await deleteKeychainAccount(runSecurity, account);
118
+ }
119
+ } catch (error) {
120
+ if (!existing.includes(parsed.userEmail)) {
121
+ await runSecurity(["delete-generic-password", "-s", SERVICE_NAME, "-a", parsed.userEmail]);
122
+ }
123
+ throw error;
124
+ }
125
+ return;
126
+ }
127
+
128
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
129
+ const file = credentialsFile(configDir);
130
+ await writeFile(file, encoded, { mode: 0o600 });
131
+ await chmod(file, 0o600);
132
+ }
133
+
134
+ export async function loadCredentials(options: CredentialStoreOptions = {}): Promise<StoredCredentials | undefined> {
135
+ const configDir = options.configDir ?? getConfigDir();
136
+ const platform = resolvePlatform(options);
137
+ const runSecurity = options.runSecurity ?? defaultRunSecurity;
138
+
139
+ let encoded: string;
140
+ if (platform === "darwin") {
141
+ const accounts = await keychainAccounts(runSecurity);
142
+ if (accounts.length === 0) {
143
+ return undefined;
144
+ }
145
+ if (accounts.length > 1) {
146
+ throw makeCliError({
147
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
148
+ message: `Multiple ${SERVICE_NAME} keychain accounts found (${accounts.join(", ")}). Remove the stale account or keep exactly one DALP CLI login before retrying.`,
149
+ });
150
+ }
151
+ const result = await runSecurity(["find-generic-password", "-s", SERVICE_NAME, "-a", accounts[0]!, "-w"]);
152
+ if (result.exitCode !== 0) {
153
+ throw makeCliError({
154
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
155
+ message: `The ${SERVICE_NAME} keychain item for ${accounts[0]!} could not be read (exit code ${result.exitCode}). Run 'dalp logout' to remove it, then 'dalp login'.`,
156
+ });
157
+ }
158
+ encoded = result.stdout.trim();
159
+ } else {
160
+ const file = credentialsFile(configDir);
161
+ try {
162
+ const stats = await Bun.file(file).stat();
163
+ if ((stats.mode & 0o077) !== 0) {
164
+ throw makeCliError({
165
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
166
+ message: `Credentials file has insecure permissions: ${(stats.mode & 0o777).toString(8)}. Run 'chmod 600 ${file}' to fix.`,
167
+ });
168
+ }
169
+ encoded = await readFile(file, "utf8");
170
+ } catch (error) {
171
+ if (isCliFailure(error)) {
172
+ throw error;
173
+ }
174
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
175
+ return undefined;
176
+ }
177
+ throw makeCliError({
178
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
179
+ message: error instanceof Error ? error.message : "Failed to load credentials",
180
+ cause: error,
181
+ });
182
+ }
183
+ }
184
+
185
+ try {
186
+ return StoredCredentialsSchema.parse(JSON.parse(encoded));
187
+ } catch (error) {
188
+ throw makeCliError({
189
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
190
+ message: "Stored credentials are invalid or corrupt.",
191
+ why: "The credential payload failed Zod validation.",
192
+ fix: "Run 'dalp logout' then 'dalp login' to refresh credentials.",
193
+ cause: error,
194
+ });
195
+ }
196
+ }
197
+
198
+ export async function deleteCredentials(options: CredentialStoreOptions = {}): Promise<void> {
199
+ const configDir = options.configDir ?? getConfigDir();
200
+ const platform = resolvePlatform(options);
201
+ const runSecurity = options.runSecurity ?? defaultRunSecurity;
202
+
203
+ if (platform === "darwin") {
204
+ for (const account of await keychainAccounts(runSecurity)) {
205
+ await deleteKeychainAccount(runSecurity, account);
206
+ }
207
+ return;
208
+ }
209
+
210
+ try {
211
+ await unlink(credentialsFile(configDir));
212
+ } catch (error) {
213
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
214
+ return;
215
+ }
216
+ throw makeCliError({
217
+ code: "kit.cli.CREDENTIAL_STORE_FAILED",
218
+ message: error instanceof Error ? error.message : "Failed to delete credentials",
219
+ cause: error,
220
+ });
221
+ }
222
+ }
@@ -0,0 +1,188 @@
1
+ import { findDalpSdkError, isDalpApiError } from "@settlemint/dalp-sdk";
2
+ import { buildErrorCta, extractResourceLinks, type CliCtaBlock, type ResourceLinks } from "./cli-cta.ts";
3
+
4
+ export interface CliErrorContext {
5
+ readonly code: string;
6
+ readonly fallbackMessage: string;
7
+ readonly fallbackWhy: string;
8
+ readonly fallbackFix: string;
9
+ }
10
+
11
+ export interface RenderedCliError {
12
+ readonly code: string;
13
+ readonly message: string;
14
+ readonly retryable: boolean;
15
+ readonly cta: CliCtaBlock;
16
+ }
17
+
18
+ export class CliError extends Error {
19
+ override readonly name = "CliError";
20
+ readonly code: string;
21
+ readonly retryable: boolean;
22
+ readonly why: string;
23
+ readonly fix: string;
24
+ readonly links?: ResourceLinks;
25
+ readonly category?: string;
26
+
27
+ constructor(options: {
28
+ readonly message: string;
29
+ readonly code: string;
30
+ readonly retryable?: boolean;
31
+ readonly why?: string;
32
+ readonly fix?: string;
33
+ readonly cause?: unknown;
34
+ readonly links?: ResourceLinks;
35
+ readonly category?: string;
36
+ }) {
37
+ super(options.message, options.cause === undefined ? undefined : { cause: options.cause });
38
+ this.code = options.code;
39
+ this.retryable = options.retryable ?? false;
40
+ this.why = options.why ?? "The CLI operation failed.";
41
+ this.fix = options.fix ?? "Correct the input or environment, then retry.";
42
+ if (options.links !== undefined) {
43
+ this.links = options.links;
44
+ }
45
+ if (options.category !== undefined) {
46
+ this.category = options.category;
47
+ }
48
+ }
49
+ }
50
+
51
+ export function makeCliError(options: ConstructorParameters<typeof CliError>[0]): CliError {
52
+ return new CliError(options);
53
+ }
54
+
55
+ export function isCliFailure(error: unknown): error is CliError {
56
+ return (
57
+ typeof error === "object" &&
58
+ error !== null &&
59
+ Reflect.get(error, "name") === "CliError" &&
60
+ typeof Reflect.get(error, "code") === "string" &&
61
+ typeof Reflect.get(error, "message") === "string"
62
+ );
63
+ }
64
+
65
+ export interface NormalizedFailure {
66
+ readonly code: string;
67
+ readonly message: string;
68
+ readonly why: string;
69
+ readonly fix: string;
70
+ readonly retryable: boolean;
71
+ readonly category?: string;
72
+ readonly links?: ResourceLinks;
73
+ }
74
+
75
+ function isRecord(value: unknown): value is Record<string, unknown> {
76
+ return typeof value === "object" && value !== null && !Array.isArray(value);
77
+ }
78
+
79
+ function extractWireFailure(error: unknown): NormalizedFailure | undefined {
80
+ if (!isRecord(error)) {
81
+ return undefined;
82
+ }
83
+ const message = typeof error.message === "string" ? error.message : undefined;
84
+ const code = typeof error.code === "string" ? error.code : undefined;
85
+ const data = isRecord(error.data) ? error.data : undefined;
86
+
87
+ const whyFromData = data !== undefined && typeof data.why === "string" ? data.why : undefined;
88
+ const fixFromData = data !== undefined && typeof data.fix === "string" ? data.fix : undefined;
89
+ const whyTop = typeof error.why === "string" ? error.why : undefined;
90
+ const fixTop = typeof error.fix === "string" ? error.fix : undefined;
91
+ const why = whyFromData ?? whyTop;
92
+ const fix = fixFromData ?? fixTop;
93
+
94
+ if (message === undefined && why === undefined && code === undefined) {
95
+ return undefined;
96
+ }
97
+ if (code === undefined && why === undefined) {
98
+ return undefined;
99
+ }
100
+
101
+ const retryableFromData = data !== undefined && typeof data.retryable === "boolean" ? data.retryable : undefined;
102
+ const retryableTop = typeof error.retryable === "boolean" ? error.retryable : undefined;
103
+ const categoryFromData = data !== undefined && typeof data.category === "string" ? data.category : undefined;
104
+ const categoryTop = typeof error.category === "string" ? error.category : undefined;
105
+ const idFromData = data !== undefined && typeof data.id === "string" ? data.id : undefined;
106
+
107
+ const links = extractResourceLinks(error) ?? (data !== undefined ? extractResourceLinks(data) : undefined);
108
+
109
+ return {
110
+ code: code ?? idFromData ?? "API_ERROR",
111
+ message: message ?? code ?? "Request failed",
112
+ why: why ?? "The DALP API rejected the request.",
113
+ fix: fix ?? "Correct the input or environment, then retry.",
114
+ retryable: retryableFromData ?? retryableTop ?? false,
115
+ ...(categoryFromData !== undefined || categoryTop !== undefined
116
+ ? { category: categoryFromData ?? categoryTop }
117
+ : {}),
118
+ ...(links === undefined ? {} : { links }),
119
+ };
120
+ }
121
+
122
+ function normalizeFailure(error: unknown, context: CliErrorContext): NormalizedFailure {
123
+ if (isDalpApiError(error)) {
124
+ return {
125
+ code: error.code,
126
+ message: error.message,
127
+ why: error.why,
128
+ fix: error.fix,
129
+ retryable: error.retryable,
130
+ category: error.category,
131
+ ...(extractResourceLinks(error) === undefined ? {} : { links: extractResourceLinks(error) }),
132
+ };
133
+ }
134
+
135
+ const sdkError = findDalpSdkError(error);
136
+ if (sdkError !== undefined) {
137
+ return {
138
+ code: sdkError.code,
139
+ message: sdkError.message,
140
+ why: sdkError.why,
141
+ fix: sdkError.fix,
142
+ retryable: sdkError.retryable,
143
+ category: sdkError.category,
144
+ };
145
+ }
146
+
147
+ if (isCliFailure(error)) {
148
+ return {
149
+ code: error.code,
150
+ message: error.message,
151
+ why: error.why,
152
+ fix: error.fix,
153
+ retryable: error.retryable,
154
+ ...(error.category === undefined ? {} : { category: error.category }),
155
+ ...(error.links === undefined ? {} : { links: error.links }),
156
+ };
157
+ }
158
+
159
+ const wire = extractWireFailure(error);
160
+ if (wire !== undefined) {
161
+ return wire;
162
+ }
163
+
164
+ const message = error instanceof Error ? error.message : context.fallbackMessage;
165
+ return {
166
+ code: context.code,
167
+ message,
168
+ why: context.fallbackWhy,
169
+ fix: context.fallbackFix,
170
+ retryable: false,
171
+ };
172
+ }
173
+
174
+ export function renderSdkErrorForCli(error: unknown, context: CliErrorContext): RenderedCliError {
175
+ const failure = normalizeFailure(error, context);
176
+ return {
177
+ code: failure.code,
178
+ message: failure.message,
179
+ retryable: failure.retryable,
180
+ cta: buildErrorCta({
181
+ why: failure.why,
182
+ fix: failure.fix,
183
+ code: failure.code,
184
+ ...(failure.category === undefined ? {} : { category: failure.category }),
185
+ ...(failure.links === undefined ? {} : { links: failure.links }),
186
+ }),
187
+ };
188
+ }
@@ -0,0 +1,41 @@
1
+ export interface SurfaceParam {
2
+ readonly name: string;
3
+ readonly in: "path" | "query" | "header";
4
+ readonly required: boolean;
5
+ readonly description: string | null;
6
+ readonly type: string | null;
7
+ readonly enum: ReadonlyArray<string> | null;
8
+ readonly pattern: string | null;
9
+ readonly examples: ReadonlyArray<string>;
10
+ readonly default: unknown;
11
+ readonly fields: ReadonlyArray<SurfaceParam>;
12
+ }
13
+
14
+ export interface SurfaceOperation {
15
+ readonly name: string;
16
+ readonly method: string;
17
+ readonly url: string;
18
+ readonly summary: string | null;
19
+ readonly description: string | null;
20
+ readonly help: string;
21
+ readonly tags: ReadonlyArray<string>;
22
+ readonly pathParams: ReadonlyArray<SurfaceParam>;
23
+ readonly queryParams: ReadonlyArray<SurfaceParam>;
24
+ readonly headerParams: ReadonlyArray<SurfaceParam>;
25
+ readonly requestBodyDescription: string | null;
26
+ readonly hasRequestBody: boolean;
27
+ readonly bodyIsComplex: boolean;
28
+ readonly bodyFields: ReadonlyArray<SurfaceParam>;
29
+ readonly examples: ReadonlyArray<{
30
+ readonly description: string;
31
+ readonly path?: Record<string, string>;
32
+ readonly query?: Record<string, string>;
33
+ }>;
34
+ }
35
+
36
+ export interface OpenApiSurface {
37
+ readonly openapiSource: string;
38
+ readonly sdkSource: string;
39
+ readonly operations: Record<string, SurfaceOperation>;
40
+ readonly tags: Record<string, string>;
41
+ }
@@ -0,0 +1,17 @@
1
+ import path from "node:path";
2
+ import surfaceAssetPath from "../generated/openapi-surface.json" with { type: "file" };
3
+ import type { OpenApiSurface, SurfaceOperation } from "./openapi-surface-types.ts";
4
+
5
+ export type { OpenApiSurface, SurfaceOperation, SurfaceParam } from "./openapi-surface-types.ts";
6
+
7
+ const surfaceFile = path.resolve(import.meta.dir, surfaceAssetPath);
8
+ const openApiSurface = (await Bun.file(surfaceFile).json()) as OpenApiSurface;
9
+
10
+ export function surfaceFor(operationName: string): SurfaceOperation | undefined {
11
+ return openApiSurface.operations[operationName];
12
+ }
13
+
14
+ export function tagDescription(tag: string): string | undefined {
15
+ const value = openApiSurface.tags[tag];
16
+ return value !== undefined && value.trim() !== "" ? value : undefined;
17
+ }
@@ -0,0 +1,69 @@
1
+ import type { z } from "zod";
2
+ import { commandPathForUrl, isNonCliOperation, routeIds, staticUrlSegments } from "@settlemint/dalp-sdk/routes";
3
+ import type { DalpRouteId } from "@settlemint/dalp-sdk/types";
4
+ import { zodForRequestBody, zodFromSurfaceParams } from "./sdk-cli-schema.ts";
5
+ import { surfaceFor, type SurfaceOperation } from "./openapi-surface.ts";
6
+ export interface OperationDescriptor {
7
+ readonly name: DalpRouteId;
8
+ readonly url: string;
9
+ readonly method: string;
10
+ readonly group: string;
11
+ readonly commandPath: ReadonlyArray<string>;
12
+ readonly pathSchema?: z.ZodType;
13
+ readonly querySchema?: z.ZodType;
14
+ readonly bodySchema?: z.ZodType;
15
+ readonly surface: SurfaceOperation;
16
+ readonly help: string;
17
+ }
18
+
19
+ function groupForUrl(url: string): string {
20
+ const segments = staticUrlSegments(url);
21
+ return segments[0] ?? "api";
22
+ }
23
+
24
+ export function operationHintKey(method: string, url: string): string {
25
+ return `${method} ${url}`;
26
+ }
27
+
28
+ export function listSdkOperationNames(): Array<DalpRouteId> {
29
+ return [...routeIds];
30
+ }
31
+
32
+ function buildGeneratedOperations(): Array<OperationDescriptor> {
33
+ const descriptors: Array<OperationDescriptor> = [];
34
+ for (const name of listSdkOperationNames()) {
35
+ const surface = surfaceFor(name);
36
+ if (surface === undefined) {
37
+ continue;
38
+ }
39
+ const { url, method } = surface;
40
+ descriptors.push({
41
+ name,
42
+ url,
43
+ method,
44
+ group: groupForUrl(url),
45
+ commandPath: commandPathForUrl(method, url),
46
+ pathSchema: zodFromSurfaceParams(surface.pathParams) as z.ZodType | undefined,
47
+ querySchema: zodFromSurfaceParams(surface.queryParams) as z.ZodType | undefined,
48
+ bodySchema: zodForRequestBody(surface),
49
+ surface,
50
+ help: surface.help,
51
+ });
52
+ }
53
+ return descriptors.sort((left, right) => left.name.localeCompare(right.name));
54
+ }
55
+
56
+ let generatedOperations: ReadonlyArray<OperationDescriptor> | undefined;
57
+
58
+ export function listGeneratedOperations(): Array<OperationDescriptor> {
59
+ generatedOperations ??= buildGeneratedOperations();
60
+ return [...generatedOperations];
61
+ }
62
+
63
+ export function listCliOperations(): Array<OperationDescriptor> {
64
+ return listGeneratedOperations().filter((descriptor) => !isNonCliOperation(descriptor.name));
65
+ }
66
+
67
+ export function listGeneratedOperationNames(): Array<string> {
68
+ return listGeneratedOperations().map((descriptor) => descriptor.name);
69
+ }