@go-labs-sg/bb 2.20.0 → 2.24.1

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,76 +0,0 @@
1
- import { CliRuntimeError } from "./error.js";
2
- const effectLabels = {
3
- "state-change": "changes Budget Builder state",
4
- email: "sends email",
5
- "external-write": "writes to an external system",
6
- delete: "deletes data",
7
- "financial-write": "creates or changes financial records",
8
- };
9
- const isEffectAllowed = (effect, permissions) => {
10
- switch (effect) {
11
- case "state-change":
12
- return permissions.allowStateChange === true;
13
- case "email":
14
- return permissions.allowEmail === true;
15
- case "external-write":
16
- return permissions.allowExternalWrite === true;
17
- case "delete":
18
- return permissions.allowDelete === true;
19
- case "financial-write":
20
- return permissions.allowFinancialWrite === true;
21
- }
22
- };
23
- const uniqueEffects = (effects) => [...new Set(effects)];
24
- export const getRequiredEffectPermissions = (effects, permissions) => uniqueEffects(effects).filter((effect) => !isEffectAllowed(effect, permissions));
25
- export const formatConfirmationPreview = (request) => [
26
- "Budget Builder workflow preview",
27
- `Action: ${request.action}`,
28
- `Target: ${request.target?.trim() || "not specified"}`,
29
- `Effects: ${uniqueEffects(request.effects)
30
- .map((effect) => effectLabels[effect])
31
- .join("; ") || "none"}`,
32
- ...(request.details === undefined ? [] : [`Details: ${request.details}`]),
33
- ].join("\n");
34
- export const confirmSensitiveCommand = async (runtime, request) => {
35
- const requiredPermissions = getRequiredEffectPermissions(request.effects, request.permissions);
36
- if (request.effects.length === 0) {
37
- return { confirmed: true, requiredPermissions };
38
- }
39
- const target = request.target?.trim();
40
- if (!runtime.isTTY) {
41
- if (target === undefined) {
42
- throw new CliRuntimeError("CONFIRMATION_REQUIRED", "Non-interactive sensitive commands require an explicit target.", {
43
- details: {
44
- action: request.action,
45
- effects: requiredPermissions,
46
- missingFlags: requiredPermissions.map((effect) => `--allow-${effect}`),
47
- target: "required",
48
- },
49
- });
50
- }
51
- if (requiredPermissions.length === 0) {
52
- return { confirmed: true, requiredPermissions };
53
- }
54
- const missing = requiredPermissions.map((effect) => `--allow-${effect}`);
55
- throw new CliRuntimeError("CONFIRMATION_REQUIRED", "Non-interactive sensitive commands require an explicit target and all applicable allow flags.", {
56
- details: {
57
- action: request.action,
58
- effects: requiredPermissions,
59
- missingFlags: missing,
60
- },
61
- });
62
- }
63
- if (requiredPermissions.length === 0) {
64
- return { confirmed: true, requiredPermissions };
65
- }
66
- const preview = formatConfirmationPreview(request);
67
- runtime.stderr.write(`${preview}\n`);
68
- if (runtime.prompt === undefined) {
69
- throw new CliRuntimeError("CONFIRMATION_REQUIRED", "This command needs an interactive confirmation prompt.");
70
- }
71
- const answer = await runtime.prompt("Type CONFIRM to continue: ");
72
- if (answer !== "CONFIRM") {
73
- throw new CliRuntimeError("ABORTED", "Command aborted by user.");
74
- }
75
- return { confirmed: true, preview, requiredPermissions };
76
- };
@@ -1,143 +0,0 @@
1
- const invocationErrorCodes = new Set([
2
- "USAGE",
3
- "VALIDATION",
4
- "CONFIRMATION_REQUIRED",
5
- ]);
6
- const trpcCodeMap = {
7
- BAD_REQUEST: "VALIDATION",
8
- CONFLICT: "CONFLICT",
9
- FORBIDDEN: "AUTHORIZATION",
10
- INTERNAL_SERVER_ERROR: "API",
11
- METHOD_NOT_SUPPORTED: "API",
12
- NOT_FOUND: "NOT_FOUND",
13
- PARSE_ERROR: "VALIDATION",
14
- PAYLOAD_TOO_LARGE: "VALIDATION",
15
- PRECONDITION_FAILED: "CONFLICT",
16
- TIMEOUT: "NETWORK",
17
- TOO_MANY_REQUESTS: "API",
18
- UNAUTHORIZED: "AUTHENTICATION",
19
- UNPROCESSABLE_CONTENT: "VALIDATION",
20
- };
21
- const cliErrorCodes = new Set([
22
- "USAGE",
23
- "VALIDATION",
24
- "CONFIRMATION_REQUIRED",
25
- "AUTHENTICATION",
26
- "AUTHORIZATION",
27
- "NOT_FOUND",
28
- "CONFLICT",
29
- "API",
30
- "NETWORK",
31
- "ABORTED",
32
- "INTERRUPTED",
33
- "INTERNAL",
34
- ]);
35
- export class CliRuntimeError extends Error {
36
- code;
37
- details;
38
- requestId;
39
- constructor(code, message, options = {}) {
40
- super(message);
41
- this.name = "CliRuntimeError";
42
- this.code = code;
43
- this.details = options.details;
44
- this.requestId = options.requestId;
45
- }
46
- }
47
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
48
- const asNonEmptyString = (value) => typeof value === "string" && value.trim() !== "" ? value : undefined;
49
- const asDetails = (value) => {
50
- if (!isRecord(value))
51
- return undefined;
52
- const details = {};
53
- for (const [key, nestedValue] of Object.entries(value)) {
54
- if (isJsonValue(nestedValue))
55
- details[key] = nestedValue;
56
- }
57
- return Object.keys(details).length > 0 ? details : undefined;
58
- };
59
- const isJsonValue = (value) => {
60
- if (value === null ||
61
- typeof value === "string" ||
62
- typeof value === "boolean" ||
63
- typeof value === "number") {
64
- return true;
65
- }
66
- if (Array.isArray(value))
67
- return value.every(isJsonValue);
68
- return isRecord(value) && Object.values(value).every(isJsonValue);
69
- };
70
- const mapErrorCode = (value) => {
71
- if (typeof value !== "string")
72
- return undefined;
73
- if (cliErrorCodes.has(value))
74
- return value;
75
- if (value in trpcCodeMap)
76
- return trpcCodeMap[value];
77
- return undefined;
78
- };
79
- const networkMessagePattern = /\b(fetch failed|network error|unable to connect|econnrefused|econnreset|enotfound|etimedout|socket hang up)\b/i;
80
- const isNetworkError = (error, depth = 0) => {
81
- if (depth > 3 || !isRecord(error))
82
- return false;
83
- const message = asNonEmptyString(error.message);
84
- if (message !== undefined && networkMessagePattern.test(message))
85
- return true;
86
- const code = asNonEmptyString(error.code);
87
- if (code !== undefined && networkMessagePattern.test(code))
88
- return true;
89
- return isNetworkError(error.cause, depth + 1);
90
- };
91
- /**
92
- * Maps local errors and the common tRPC client error shape into the stable
93
- * CLI contract without importing a transport-specific error class.
94
- */
95
- export const normalizeCliError = (error) => {
96
- if (error instanceof CliRuntimeError) {
97
- return {
98
- code: error.code,
99
- message: error.message,
100
- ...(error.details === undefined ? {} : { details: error.details }),
101
- ...(error.requestId === undefined ? {} : { requestId: error.requestId }),
102
- };
103
- }
104
- if (isNetworkError(error)) {
105
- return {
106
- code: "NETWORK",
107
- message: error instanceof Error
108
- ? error.message
109
- : "The Budget Builder API could not be reached.",
110
- };
111
- }
112
- if (isRecord(error)) {
113
- const data = isRecord(error.data) ? error.data : undefined;
114
- const transportCode = mapErrorCode(data?.code ?? error.code);
115
- if (error instanceof Error &&
116
- data === undefined &&
117
- transportCode === undefined) {
118
- return { code: "INTERNAL", message: error.message };
119
- }
120
- const code = transportCode ?? "API";
121
- const message = asNonEmptyString(error.message) ??
122
- "The Budget Builder API request failed.";
123
- const requestId = asNonEmptyString(data?.requestId) ??
124
- asNonEmptyString(data?.requestID) ??
125
- asNonEmptyString(error.requestId);
126
- const details = asDetails(data?.details ?? error.details);
127
- return {
128
- code,
129
- message,
130
- ...(details === undefined ? {} : { details }),
131
- ...(requestId === undefined ? {} : { requestId }),
132
- };
133
- }
134
- if (error instanceof Error) {
135
- return { code: "INTERNAL", message: error.message };
136
- }
137
- return { code: "INTERNAL", message: "An unexpected CLI error occurred." };
138
- };
139
- export const exitCodeForCliError = (error) => {
140
- if (error.code === "INTERRUPTED")
141
- return 130;
142
- return invocationErrorCodes.has(error.code) ? 2 : 1;
143
- };
@@ -1,6 +0,0 @@
1
- export { confirmSensitiveCommand, formatConfirmationPreview, getRequiredEffectPermissions, } from "./confirmation.js";
2
- export { CliRuntimeError, exitCodeForCliError, normalizeCliError, } from "./error.js";
3
- export { createSuccessEnvelope, emitCliDiagnostic, emitCliError, emitCliSuccess, } from "./output.js";
4
- export { createProcessRuntime } from "./process-runtime.js";
5
- export { sanitizeCliDiagnostic, sanitizeCliValue } from "./sanitize.js";
6
- export { clearActiveCliSession, confirmCurrentCommand, emitCommandError, emitCommandResult, getActiveCliSession, setActiveCliSession, } from "./session.js";
@@ -1,36 +0,0 @@
1
- import { exitCodeForCliError, normalizeCliError } from "./error.js";
2
- import { sanitizeCliDiagnostic } from "./sanitize.js";
3
- const serialize = (value) => `${JSON.stringify(value, null, 2)}\n`;
4
- export const createSuccessEnvelope = (data, invocation) => ({
5
- ok: true,
6
- data,
7
- meta: {
8
- command: invocation.command,
9
- cliVersion: invocation.cliVersion,
10
- apiBaseUrl: invocation.apiBaseUrl,
11
- },
12
- });
13
- export const emitCliSuccess = (runtime, invocation, data) => {
14
- runtime.stdout.write(serialize(invocation.legacy ? data : createSuccessEnvelope(data, invocation)));
15
- return 0;
16
- };
17
- export const emitCliError = (runtime, invocation, error) => {
18
- const normalized = normalizeCliError(error);
19
- if (invocation.legacy) {
20
- runtime.stderr.write(serialize({ error: normalized.message }));
21
- }
22
- else {
23
- const envelope = {
24
- ok: false,
25
- error: normalized,
26
- meta: { command: invocation.command },
27
- };
28
- runtime.stderr.write(serialize(envelope));
29
- }
30
- return exitCodeForCliError(normalized);
31
- };
32
- export const emitCliDiagnostic = (runtime, invocation, diagnostic) => {
33
- if (invocation.quiet || !invocation.debug)
34
- return;
35
- runtime.stderr.write(serialize(sanitizeCliDiagnostic(diagnostic)));
36
- };
@@ -1,28 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- export const createProcessRuntime = () => ({
3
- stdout: {
4
- write: (value) => {
5
- process.stdout.write(value);
6
- },
7
- },
8
- stderr: {
9
- write: (value) => {
10
- process.stderr.write(value);
11
- },
12
- },
13
- isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY),
14
- env: process.env,
15
- prompt: async (message) => {
16
- const prompt = createInterface({
17
- input: process.stdin,
18
- output: process.stderr,
19
- });
20
- try {
21
- return await prompt.question(message);
22
- }
23
- finally {
24
- prompt.close();
25
- }
26
- },
27
- now: () => new Date(),
28
- });
@@ -1,50 +0,0 @@
1
- const sensitiveKeys = new Set([
2
- "apikey",
3
- "authorization",
4
- "bb_api_key",
5
- "password",
6
- "paymentproof",
7
- "paymentproofattachments",
8
- "paymentproofpath",
9
- "paymentreference",
10
- "payload",
11
- "proof",
12
- "receipt",
13
- "secret",
14
- "token",
15
- ]);
16
- const isPresignedUrl = (value) => typeof value === "string" &&
17
- (/[?&]X-Amz-(?:Algorithm|Credential|Signature)=/i.test(value) ||
18
- /[?&]X-Goog-(?:Algorithm|Credential|Signature)=/i.test(value));
19
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
20
- export const sanitizeCliValue = (value, seen = new WeakSet()) => {
21
- if (value === null ||
22
- typeof value === "string" ||
23
- typeof value === "boolean" ||
24
- typeof value === "number") {
25
- return isPresignedUrl(value) ? "[redacted]" : value;
26
- }
27
- if (typeof value === "bigint")
28
- return value.toString();
29
- if (value instanceof Date)
30
- return value.toISOString();
31
- if (typeof value !== "object" || value === null)
32
- return String(value);
33
- if (seen.has(value))
34
- return "[circular]";
35
- seen.add(value);
36
- if (Array.isArray(value)) {
37
- return value.map((item) => sanitizeCliValue(item, seen));
38
- }
39
- if (!isRecord(value))
40
- return String(value);
41
- return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
42
- key,
43
- sensitiveKeys.has(key.toLowerCase()) ||
44
- key.toLowerCase().includes("secret") ||
45
- key.toLowerCase().includes("password")
46
- ? "[redacted]"
47
- : sanitizeCliValue(nestedValue, seen),
48
- ]));
49
- };
50
- export const sanitizeCliDiagnostic = (diagnostic) => sanitizeCliValue(diagnostic);
@@ -1,50 +0,0 @@
1
- import { confirmSensitiveCommand } from "./confirmation.js";
2
- import { emitCliError, emitCliSuccess } from "./output.js";
3
- let activeSession;
4
- export const setActiveCliSession = (session) => {
5
- activeSession = {
6
- ...session,
7
- confirmedEffects: new Set(),
8
- };
9
- };
10
- export const clearActiveCliSession = () => {
11
- activeSession = undefined;
12
- };
13
- export const getActiveCliSession = () => activeSession;
14
- const requireSession = () => {
15
- if (activeSession === undefined) {
16
- throw new Error("Budget Builder CLI runtime session is not configured.");
17
- }
18
- return activeSession;
19
- };
20
- const toJsonValue = (value) => {
21
- const serialized = JSON.stringify(value, (_key, nestedValue) => typeof nestedValue === "bigint" ? nestedValue.toString() : nestedValue);
22
- if (serialized === undefined)
23
- return null;
24
- return JSON.parse(serialized);
25
- };
26
- export const emitCommandResult = (data) => {
27
- const session = requireSession();
28
- return emitCliSuccess(session.runtime, session.invocation, toJsonValue(data));
29
- };
30
- export const emitCommandError = (error) => {
31
- const session = requireSession();
32
- return emitCliError(session.runtime, session.invocation, error);
33
- };
34
- export const confirmCurrentCommand = async (input) => {
35
- const session = requireSession();
36
- const effects = input.effects ?? session.effects;
37
- const unconfirmedEffects = effects.filter((effect) => !session.confirmedEffects.has(effect));
38
- if (unconfirmedEffects.length === 0)
39
- return;
40
- await confirmSensitiveCommand(session.runtime, {
41
- action: input.action,
42
- target: input.target,
43
- details: input.details,
44
- effects: unconfirmedEffects,
45
- permissions: session.permissions,
46
- });
47
- for (const effect of unconfirmedEffects) {
48
- session.confirmedEffects.add(effect);
49
- }
50
- };
@@ -1 +0,0 @@
1
- export {};