@go-labs-sg/bb 2.24.2 → 2.25.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/dist/index.js +242 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -372,6 +372,20 @@ var import_dotenv = __toESM(require_main(), 1);
|
|
|
372
372
|
import path from "path";
|
|
373
373
|
import_dotenv.config({ path: path.resolve(process.cwd(), ".env"), quiet: true });
|
|
374
374
|
|
|
375
|
+
// ../../cli-auth/src/client/credential-store.ts
|
|
376
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
377
|
+
import {
|
|
378
|
+
chmod,
|
|
379
|
+
lstat,
|
|
380
|
+
mkdir,
|
|
381
|
+
open as open2,
|
|
382
|
+
readFile,
|
|
383
|
+
stat,
|
|
384
|
+
unlink
|
|
385
|
+
} from "fs/promises";
|
|
386
|
+
import { userInfo } from "os";
|
|
387
|
+
import { join } from "path";
|
|
388
|
+
|
|
375
389
|
// ../../cli-auth/src/protocol/constants.ts
|
|
376
390
|
var CLI_ACCESS_TOKEN_TTL_MS = 15 * 60 * 1000;
|
|
377
391
|
var CLI_DEVICE_AUTHORIZATION_TTL_MS = 10 * 60 * 1000;
|
|
@@ -19025,13 +19039,15 @@ var deviceTokenErrorSchema = exports_external.object({
|
|
|
19025
19039
|
"authorization_pending",
|
|
19026
19040
|
"slow_down",
|
|
19027
19041
|
"access_denied",
|
|
19028
|
-
"expired_token"
|
|
19042
|
+
"expired_token",
|
|
19043
|
+
"invalid_grant"
|
|
19029
19044
|
])
|
|
19030
19045
|
}).strict();
|
|
19031
19046
|
var refreshRequestSchema = exports_external.object({ refreshToken: exports_external.string().min(40) }).strict();
|
|
19032
19047
|
var storedCliCredentialsSchema = exports_external.object({
|
|
19033
19048
|
accessToken: exports_external.string().min(1),
|
|
19034
19049
|
accessTokenExpiresAt: dateTimeString,
|
|
19050
|
+
issuer: exports_external.string().url().optional(),
|
|
19035
19051
|
refreshToken: exports_external.string().min(1),
|
|
19036
19052
|
refreshTokenExpiresAt: dateTimeString,
|
|
19037
19053
|
session: cliSessionIdentitySchema
|
|
@@ -19083,6 +19099,77 @@ var defaultSecrets = () => {
|
|
|
19083
19099
|
assertBunRuntime();
|
|
19084
19100
|
return Bun.secrets;
|
|
19085
19101
|
};
|
|
19102
|
+
var refreshLockTimeoutMs = 5 * 60 * 1000;
|
|
19103
|
+
var refreshLockRetryMs = 50;
|
|
19104
|
+
var getCredentialLockDirectoryPath = () => {
|
|
19105
|
+
return join(userInfo().homedir, ".cache", "go-labs-cli", "locks");
|
|
19106
|
+
};
|
|
19107
|
+
var ensurePrivateCredentialLockDirectory = async () => {
|
|
19108
|
+
const directory = getCredentialLockDirectoryPath();
|
|
19109
|
+
await mkdir(directory, { mode: 448, recursive: true });
|
|
19110
|
+
const inspectDirectory = async () => {
|
|
19111
|
+
const directoryStat2 = await lstat(directory);
|
|
19112
|
+
if (directoryStat2.isSymbolicLink() || !directoryStat2.isDirectory()) {
|
|
19113
|
+
throw new Error("The CLI credential lock path is not a private directory.");
|
|
19114
|
+
}
|
|
19115
|
+
const uid = process.getuid?.();
|
|
19116
|
+
if (uid !== undefined && directoryStat2.uid !== uid) {
|
|
19117
|
+
throw new Error("The CLI credential lock directory belongs to another user.");
|
|
19118
|
+
}
|
|
19119
|
+
return directoryStat2;
|
|
19120
|
+
};
|
|
19121
|
+
const directoryStat = await inspectDirectory();
|
|
19122
|
+
if ((directoryStat.mode & 63) !== 0) {
|
|
19123
|
+
await chmod(directory, 448);
|
|
19124
|
+
const securedDirectoryStat = await inspectDirectory();
|
|
19125
|
+
if ((securedDirectoryStat.mode & 63) !== 0) {
|
|
19126
|
+
throw new Error("Unable to secure the CLI credential lock directory.");
|
|
19127
|
+
}
|
|
19128
|
+
}
|
|
19129
|
+
return directory;
|
|
19130
|
+
};
|
|
19131
|
+
var getCredentialLockPath = async (product, account) => {
|
|
19132
|
+
const key = createHash2("sha256").update(`${product}:${account}`).digest("hex");
|
|
19133
|
+
return join(await ensurePrivateCredentialLockDirectory(), `${key}.lock`);
|
|
19134
|
+
};
|
|
19135
|
+
var acquireRefreshLock = async (path2) => {
|
|
19136
|
+
const deadline = Date.now() + refreshLockTimeoutMs;
|
|
19137
|
+
while (Date.now() < deadline) {
|
|
19138
|
+
try {
|
|
19139
|
+
const handle = await open2(path2, "wx");
|
|
19140
|
+
const owner = randomUUID();
|
|
19141
|
+
try {
|
|
19142
|
+
await handle.writeFile(owner);
|
|
19143
|
+
} catch (cause) {
|
|
19144
|
+
await handle.close().catch(() => {
|
|
19145
|
+
return;
|
|
19146
|
+
});
|
|
19147
|
+
await unlink(path2).catch(() => {
|
|
19148
|
+
return;
|
|
19149
|
+
});
|
|
19150
|
+
throw cause;
|
|
19151
|
+
}
|
|
19152
|
+
return { handle, owner };
|
|
19153
|
+
} catch (cause) {
|
|
19154
|
+
if (!(cause instanceof Error) || !("code" in cause) || cause.code !== "EEXIST") {
|
|
19155
|
+
throw cause;
|
|
19156
|
+
}
|
|
19157
|
+
try {
|
|
19158
|
+
const lock = await stat(path2);
|
|
19159
|
+
if (Date.now() - lock.mtimeMs > refreshLockTimeoutMs) {
|
|
19160
|
+
throw new Error(`The CLI credential lock appears stale at ${path2}. Ensure no CLI authentication command is running, remove that lock file, and retry.`);
|
|
19161
|
+
}
|
|
19162
|
+
} catch (statCause) {
|
|
19163
|
+
if (statCause instanceof Error && "code" in statCause && statCause.code === "ENOENT") {
|
|
19164
|
+
continue;
|
|
19165
|
+
}
|
|
19166
|
+
throw statCause;
|
|
19167
|
+
}
|
|
19168
|
+
await Bun.sleep(refreshLockRetryMs);
|
|
19169
|
+
}
|
|
19170
|
+
}
|
|
19171
|
+
throw new Error("Timed out waiting for another CLI process to refresh credentials. Retry the command.");
|
|
19172
|
+
};
|
|
19086
19173
|
var createBunCredentialStore = ({
|
|
19087
19174
|
account = "default",
|
|
19088
19175
|
product,
|
|
@@ -19110,16 +19197,46 @@ var createBunCredentialStore = ({
|
|
|
19110
19197
|
service,
|
|
19111
19198
|
value: JSON.stringify(parsed)
|
|
19112
19199
|
}));
|
|
19200
|
+
},
|
|
19201
|
+
withCredentialLock: async (operation) => {
|
|
19202
|
+
const refreshLockPath = await getCredentialLockPath(product, account);
|
|
19203
|
+
const lock = await acquireRefreshLock(refreshLockPath);
|
|
19204
|
+
try {
|
|
19205
|
+
return await operation();
|
|
19206
|
+
} finally {
|
|
19207
|
+
await lock.handle.close();
|
|
19208
|
+
const owner = await readFile(refreshLockPath, "utf8").catch(() => null);
|
|
19209
|
+
if (owner === lock.owner) {
|
|
19210
|
+
await unlink(refreshLockPath).catch(() => {
|
|
19211
|
+
return;
|
|
19212
|
+
});
|
|
19213
|
+
}
|
|
19214
|
+
}
|
|
19113
19215
|
}
|
|
19114
19216
|
};
|
|
19115
19217
|
};
|
|
19116
19218
|
// ../../cli-auth/src/client/login.ts
|
|
19117
19219
|
import { hostname as hostname3 } from "os";
|
|
19118
|
-
var
|
|
19119
|
-
|
|
19220
|
+
var normalizeCliAuthIssuer = (value) => {
|
|
19221
|
+
const url2 = new URL(value);
|
|
19222
|
+
url2.hash = "";
|
|
19223
|
+
url2.search = "";
|
|
19224
|
+
url2.pathname = url2.pathname.replace(/\/+$/, "");
|
|
19225
|
+
return url2.toString().replace(/\/+$/, "");
|
|
19226
|
+
};
|
|
19227
|
+
var isCliCredentialForIssuer = (credentials, apiBaseUrl) => credentials.issuer === normalizeCliAuthIssuer(apiBaseUrl);
|
|
19228
|
+
var credentialIssuerError = (issuer) => issuer ? new Error("These CLI credentials belong to a different API URL. Run auth login for this API URL before retrying.") : new Error("These CLI credentials were created by an older CLI and are not bound to an API URL. Run auth login again.");
|
|
19229
|
+
var assertCredentialIssuer = (credentials, apiBaseUrl) => {
|
|
19230
|
+
const expectedIssuer = normalizeCliAuthIssuer(apiBaseUrl);
|
|
19231
|
+
if (!credentials.issuer || credentials.issuer !== expectedIssuer) {
|
|
19232
|
+
throw credentialIssuerError(credentials.issuer);
|
|
19233
|
+
}
|
|
19234
|
+
};
|
|
19235
|
+
var postJson = async (fetchImplementation, url2, body, options = {}) => fetchImplementation(url2, {
|
|
19120
19236
|
body: JSON.stringify(body),
|
|
19121
19237
|
headers: { "content-type": "application/json" },
|
|
19122
|
-
method: "POST"
|
|
19238
|
+
method: "POST",
|
|
19239
|
+
...options
|
|
19123
19240
|
});
|
|
19124
19241
|
var readJson = async (response) => {
|
|
19125
19242
|
try {
|
|
@@ -19128,9 +19245,29 @@ var readJson = async (response) => {
|
|
|
19128
19245
|
throw new Error(`CLI authentication returned HTTP ${response.status} without valid JSON.`);
|
|
19129
19246
|
}
|
|
19130
19247
|
};
|
|
19131
|
-
var
|
|
19248
|
+
var localCredentialLocks = new WeakMap;
|
|
19249
|
+
var withCredentialLock = (store, operation) => {
|
|
19250
|
+
if (store.withCredentialLock) {
|
|
19251
|
+
return store.withCredentialLock(operation);
|
|
19252
|
+
}
|
|
19253
|
+
const previous = localCredentialLocks.get(store) ?? Promise.resolve();
|
|
19254
|
+
const pending = previous.then(operation, operation);
|
|
19255
|
+
const tail = pending.then(() => {
|
|
19256
|
+
return;
|
|
19257
|
+
}, () => {
|
|
19258
|
+
return;
|
|
19259
|
+
});
|
|
19260
|
+
localCredentialLocks.set(store, tail);
|
|
19261
|
+
return pending.finally(() => {
|
|
19262
|
+
if (localCredentialLocks.get(store) === tail) {
|
|
19263
|
+
localCredentialLocks.delete(store);
|
|
19264
|
+
}
|
|
19265
|
+
});
|
|
19266
|
+
};
|
|
19267
|
+
var toStoredCredentials = (response, issuer, now) => ({
|
|
19132
19268
|
accessToken: response.accessToken,
|
|
19133
19269
|
accessTokenExpiresAt: new Date(now.getTime() + response.expiresIn * 1000).toISOString(),
|
|
19270
|
+
issuer,
|
|
19134
19271
|
refreshToken: response.refreshToken,
|
|
19135
19272
|
refreshTokenExpiresAt: new Date(now.getTime() + response.refreshExpiresIn * 1000).toISOString(),
|
|
19136
19273
|
session: response.session
|
|
@@ -19164,7 +19301,7 @@ var loginWithBrowser = async ({
|
|
|
19164
19301
|
sleep = (milliseconds) => Bun.sleep(milliseconds),
|
|
19165
19302
|
store
|
|
19166
19303
|
}) => {
|
|
19167
|
-
const baseUrl =
|
|
19304
|
+
const baseUrl = normalizeCliAuthIssuer(apiBaseUrl);
|
|
19168
19305
|
const pkce = createPkcePair();
|
|
19169
19306
|
const beginResponse = await postJson(fetchImplementation, `${baseUrl}/api/cli-auth/device`, {
|
|
19170
19307
|
clientName,
|
|
@@ -19191,8 +19328,8 @@ var loginWithBrowser = async ({
|
|
|
19191
19328
|
const payload = await readJson(response);
|
|
19192
19329
|
if (response.ok) {
|
|
19193
19330
|
const issued = tokenResponseSchema.parse(payload);
|
|
19194
|
-
const credentials = toStoredCredentials(issued, now());
|
|
19195
|
-
await store.set(credentials);
|
|
19331
|
+
const credentials = toStoredCredentials(issued, baseUrl, now());
|
|
19332
|
+
await withCredentialLock(store, () => store.set(credentials));
|
|
19196
19333
|
return credentials;
|
|
19197
19334
|
}
|
|
19198
19335
|
const error61 = deviceTokenErrorSchema.safeParse(payload);
|
|
@@ -19213,6 +19350,26 @@ var loginWithBrowser = async ({
|
|
|
19213
19350
|
throw new Error("CLI authentication expired. Run auth login again.");
|
|
19214
19351
|
};
|
|
19215
19352
|
var refreshes = new WeakMap;
|
|
19353
|
+
var refreshRequestTimeoutMs = 30000;
|
|
19354
|
+
var isInvalidGrantResponse = (payload) => {
|
|
19355
|
+
const parsed = deviceTokenErrorSchema.safeParse(payload);
|
|
19356
|
+
return parsed.success && parsed.data.error === "invalid_grant";
|
|
19357
|
+
};
|
|
19358
|
+
var readJsonOrNull = async (response) => {
|
|
19359
|
+
try {
|
|
19360
|
+
return await response.json();
|
|
19361
|
+
} catch {
|
|
19362
|
+
return null;
|
|
19363
|
+
}
|
|
19364
|
+
};
|
|
19365
|
+
var deleteInvalidCredentials = async (store, credentials) => {
|
|
19366
|
+
const current = await store.get();
|
|
19367
|
+
if (!current)
|
|
19368
|
+
return;
|
|
19369
|
+
if (current.issuer === credentials.issuer && current.refreshToken === credentials.refreshToken) {
|
|
19370
|
+
await store.delete();
|
|
19371
|
+
}
|
|
19372
|
+
};
|
|
19216
19373
|
var refreshAccessToken = async ({
|
|
19217
19374
|
apiBaseUrl,
|
|
19218
19375
|
credentials,
|
|
@@ -19220,16 +19377,56 @@ var refreshAccessToken = async ({
|
|
|
19220
19377
|
now,
|
|
19221
19378
|
store
|
|
19222
19379
|
}) => {
|
|
19223
|
-
|
|
19380
|
+
assertCredentialIssuer(credentials, apiBaseUrl);
|
|
19381
|
+
let response;
|
|
19382
|
+
try {
|
|
19383
|
+
response = await postJson(fetchImplementation, `${normalizeCliAuthIssuer(apiBaseUrl)}/api/cli-auth/refresh`, { refreshToken: credentials.refreshToken }, { signal: AbortSignal.timeout(refreshRequestTimeoutMs) });
|
|
19384
|
+
} catch {
|
|
19385
|
+
throw new Error("Unable to refresh the CLI session because the API could not be reached. Credentials were kept; retry the command.");
|
|
19386
|
+
}
|
|
19224
19387
|
if (!response.ok) {
|
|
19225
|
-
await
|
|
19226
|
-
|
|
19388
|
+
const payload2 = await readJsonOrNull(response);
|
|
19389
|
+
if (isInvalidGrantResponse(payload2)) {
|
|
19390
|
+
await deleteInvalidCredentials(store, credentials);
|
|
19391
|
+
throw new Error("The CLI session expired or was revoked. Run auth login again.");
|
|
19392
|
+
}
|
|
19393
|
+
throw new Error(`Unable to refresh the CLI session (HTTP ${response.status}). Credentials were kept; retry the command.`);
|
|
19394
|
+
}
|
|
19395
|
+
const payload = await readJsonOrNull(response);
|
|
19396
|
+
if (payload === null) {
|
|
19397
|
+
throw new Error("Unable to refresh the CLI session because the API returned malformed JSON. Credentials were kept; retry the command.");
|
|
19398
|
+
}
|
|
19399
|
+
let issued;
|
|
19400
|
+
try {
|
|
19401
|
+
issued = tokenResponseSchema.parse(payload);
|
|
19402
|
+
} catch {
|
|
19403
|
+
throw new Error("Unable to refresh the CLI session because the API returned invalid credentials. Credentials were kept; retry the command.");
|
|
19227
19404
|
}
|
|
19228
|
-
const
|
|
19229
|
-
const stored = toStoredCredentials(issued, now());
|
|
19405
|
+
const stored = toStoredCredentials(issued, normalizeCliAuthIssuer(apiBaseUrl), now());
|
|
19230
19406
|
await store.set(stored);
|
|
19231
19407
|
return stored.accessToken;
|
|
19232
19408
|
};
|
|
19409
|
+
var refreshWithCurrentCredentials = async ({
|
|
19410
|
+
apiBaseUrl,
|
|
19411
|
+
fetch: fetchImplementation,
|
|
19412
|
+
now,
|
|
19413
|
+
store
|
|
19414
|
+
}) => {
|
|
19415
|
+
const credentials = await store.get();
|
|
19416
|
+
if (!credentials)
|
|
19417
|
+
throw new Error("Not authenticated. Run auth login first.");
|
|
19418
|
+
assertCredentialIssuer(credentials, apiBaseUrl);
|
|
19419
|
+
if (Date.parse(credentials.accessTokenExpiresAt) - now().getTime() > 60000) {
|
|
19420
|
+
return credentials.accessToken;
|
|
19421
|
+
}
|
|
19422
|
+
return refreshAccessToken({
|
|
19423
|
+
apiBaseUrl,
|
|
19424
|
+
credentials,
|
|
19425
|
+
fetch: fetchImplementation,
|
|
19426
|
+
now,
|
|
19427
|
+
store
|
|
19428
|
+
});
|
|
19429
|
+
};
|
|
19233
19430
|
var getAuthenticatedAccessToken = async ({
|
|
19234
19431
|
apiBaseUrl,
|
|
19235
19432
|
fetch: fetchImplementation = globalThis.fetch,
|
|
@@ -19239,19 +19436,20 @@ var getAuthenticatedAccessToken = async ({
|
|
|
19239
19436
|
const credentials = await store.get();
|
|
19240
19437
|
if (!credentials)
|
|
19241
19438
|
throw new Error("Not authenticated. Run auth login first.");
|
|
19439
|
+
assertCredentialIssuer(credentials, apiBaseUrl);
|
|
19242
19440
|
if (Date.parse(credentials.accessTokenExpiresAt) - now().getTime() > 60000) {
|
|
19243
19441
|
return credentials.accessToken;
|
|
19244
19442
|
}
|
|
19245
19443
|
const existing = refreshes.get(store);
|
|
19246
19444
|
if (existing)
|
|
19247
19445
|
return existing;
|
|
19248
|
-
const
|
|
19446
|
+
const refreshOperation = () => withCredentialLock(store, () => refreshWithCurrentCredentials({
|
|
19249
19447
|
apiBaseUrl,
|
|
19250
|
-
credentials,
|
|
19251
19448
|
fetch: fetchImplementation,
|
|
19252
19449
|
now,
|
|
19253
19450
|
store
|
|
19254
|
-
})
|
|
19451
|
+
}));
|
|
19452
|
+
const refresh = refreshOperation().finally(() => refreshes.delete(store));
|
|
19255
19453
|
refreshes.set(store, refresh);
|
|
19256
19454
|
return refresh;
|
|
19257
19455
|
};
|
|
@@ -19260,18 +19458,22 @@ var logoutCliSession = async ({
|
|
|
19260
19458
|
fetch: fetchImplementation = globalThis.fetch,
|
|
19261
19459
|
store
|
|
19262
19460
|
}) => {
|
|
19263
|
-
const
|
|
19264
|
-
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19461
|
+
const issuer = normalizeCliAuthIssuer(apiBaseUrl);
|
|
19462
|
+
return withCredentialLock(store, async () => {
|
|
19463
|
+
const credentials = await store.get();
|
|
19464
|
+
let remoteRevoked = credentials === null;
|
|
19465
|
+
if (credentials) {
|
|
19466
|
+
try {
|
|
19467
|
+
assertCredentialIssuer(credentials, apiBaseUrl);
|
|
19468
|
+
const response = await postJson(fetchImplementation, `${issuer}/api/cli-auth/revoke`, { refreshToken: credentials.refreshToken }, { signal: AbortSignal.timeout(refreshRequestTimeoutMs) });
|
|
19469
|
+
remoteRevoked = response.ok;
|
|
19470
|
+
} catch {
|
|
19471
|
+
remoteRevoked = false;
|
|
19472
|
+
}
|
|
19271
19473
|
}
|
|
19272
|
-
|
|
19273
|
-
|
|
19274
|
-
|
|
19474
|
+
await store.delete();
|
|
19475
|
+
return { remoteRevoked };
|
|
19476
|
+
});
|
|
19275
19477
|
};
|
|
19276
19478
|
// src/cli.ts
|
|
19277
19479
|
import { readFileSync } from "fs";
|
|
@@ -19426,8 +19628,8 @@ var api2 = new Proxy({}, {
|
|
|
19426
19628
|
});
|
|
19427
19629
|
|
|
19428
19630
|
// src/commands/index.ts
|
|
19429
|
-
import { randomUUID } from "crypto";
|
|
19430
|
-
import { readFile, writeFile } from "fs/promises";
|
|
19631
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
19632
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
19431
19633
|
import { basename } from "path";
|
|
19432
19634
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
19433
19635
|
|
|
@@ -21236,7 +21438,7 @@ var rethrowAfterStagedPaymentProofCleanup = async (error61, cleanup) => {
|
|
|
21236
21438
|
throw error61;
|
|
21237
21439
|
};
|
|
21238
21440
|
var stageBillPaymentProof = async (projectId, filePath) => {
|
|
21239
|
-
const buffer = await
|
|
21441
|
+
const buffer = await readFile2(filePath);
|
|
21240
21442
|
const fileName = basename(filePath);
|
|
21241
21443
|
const size = buffer.byteLength;
|
|
21242
21444
|
const contentType2 = billAttachmentContentTypeForFileName(fileName);
|
|
@@ -21272,7 +21474,7 @@ var stageBillPaymentProof = async (projectId, filePath) => {
|
|
|
21272
21474
|
}
|
|
21273
21475
|
throw uploadError;
|
|
21274
21476
|
}
|
|
21275
|
-
return { id:
|
|
21477
|
+
return { id: randomUUID2(), key, name: fileName, size };
|
|
21276
21478
|
};
|
|
21277
21479
|
async function updateBillStatus(opts) {
|
|
21278
21480
|
validateBillStatusUpdateOptions(opts);
|
|
@@ -21327,7 +21529,7 @@ async function getBillAttachments(id) {
|
|
|
21327
21529
|
async function stageBillAttachmentsFromPaths(projectId, filePaths) {
|
|
21328
21530
|
const attachments = [];
|
|
21329
21531
|
for (const filePath of filePaths) {
|
|
21330
|
-
const buffer = await
|
|
21532
|
+
const buffer = await readFile2(filePath);
|
|
21331
21533
|
const fileName = basename(filePath);
|
|
21332
21534
|
const size = buffer.byteLength;
|
|
21333
21535
|
const contentType2 = billAttachmentContentTypeForFileName(fileName);
|
|
@@ -21345,7 +21547,7 @@ async function stageBillAttachmentsFromPaths(projectId, filePaths) {
|
|
|
21345
21547
|
if (!response.ok) {
|
|
21346
21548
|
throw new Error(`S3 upload failed: HTTP ${response.status} ${(await response.text()).slice(0, 500)}`);
|
|
21347
21549
|
}
|
|
21348
|
-
attachments.push({ id:
|
|
21550
|
+
attachments.push({ id: randomUUID2(), key, name: fileName, size });
|
|
21349
21551
|
}
|
|
21350
21552
|
out({
|
|
21351
21553
|
success: true,
|
|
@@ -21366,7 +21568,7 @@ async function uploadBillAttachmentFromPath(billId, filePath, type = "BILL") {
|
|
|
21366
21568
|
out(confirmed);
|
|
21367
21569
|
}
|
|
21368
21570
|
var uploadSingleBillAttachmentFromPath = async (billId, filePath, type = "BILL") => {
|
|
21369
|
-
const buf = await
|
|
21571
|
+
const buf = await readFile2(filePath);
|
|
21370
21572
|
const fileName = basename(filePath);
|
|
21371
21573
|
const size = buf.byteLength;
|
|
21372
21574
|
const attachmentContentType = billAttachmentContentTypeForFileName(fileName);
|
|
@@ -21435,14 +21637,14 @@ async function uploadBillDocumentsFromPaths({
|
|
|
21435
21637
|
});
|
|
21436
21638
|
}
|
|
21437
21639
|
async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
21438
|
-
const buf = await
|
|
21640
|
+
const buf = await readFile2(filePath);
|
|
21439
21641
|
const fileName = basename(filePath);
|
|
21440
21642
|
if (buf.byteLength > QUOTATION_ATTACHMENT_MAX_SIZE) {
|
|
21441
21643
|
throw new Error("Quotation attachment must be 20MB or smaller.");
|
|
21442
21644
|
}
|
|
21443
21645
|
const attachmentContentType = quotationAttachmentContentTypeForFileName(fileName);
|
|
21444
21646
|
const ext = fileName.split(".").pop()?.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
21445
|
-
const key = `quotations/${projectId}/${
|
|
21647
|
+
const key = `quotations/${projectId}/${randomUUID2()}.${ext || "pdf"}`;
|
|
21446
21648
|
const uploadUrl = await api2.attachment.getPresignedUrlToUpload.mutate({
|
|
21447
21649
|
key,
|
|
21448
21650
|
size: buf.byteLength,
|
|
@@ -21457,7 +21659,7 @@ async function uploadQuotationAttachmentFromPath(projectId, filePath) {
|
|
|
21457
21659
|
throw new Error(`S3 upload failed: HTTP ${res.status} ${(await res.text()).slice(0, 500)}`);
|
|
21458
21660
|
}
|
|
21459
21661
|
out({
|
|
21460
|
-
id:
|
|
21662
|
+
id: randomUUID2(),
|
|
21461
21663
|
name: fileName,
|
|
21462
21664
|
key,
|
|
21463
21665
|
size: buf.byteLength
|
|
@@ -21472,7 +21674,7 @@ var cleanupStagedQuotationAttachments = async (projectId, keys) => {
|
|
|
21472
21674
|
};
|
|
21473
21675
|
async function uploadBudgetAttachmentFromPath(budgetId, filePath) {
|
|
21474
21676
|
await confirmLockedBudgetChangeByBudgetId(budgetId, "Upload budget attachment");
|
|
21475
|
-
const buf = await
|
|
21677
|
+
const buf = await readFile2(filePath);
|
|
21476
21678
|
const fileName = basename(filePath);
|
|
21477
21679
|
const size = buf.byteLength;
|
|
21478
21680
|
const attachmentContentType = attachmentContentTypeForFileName(fileName);
|
|
@@ -21505,7 +21707,7 @@ async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
|
|
|
21505
21707
|
details: "by uploading proof and setting status to ESTIMATE_ACCEPTED"
|
|
21506
21708
|
});
|
|
21507
21709
|
await confirmLockedBudgetChangeByBudgetId(budgetId, "Mark budget won");
|
|
21508
|
-
const buf = await
|
|
21710
|
+
const buf = await readFile2(filePath);
|
|
21509
21711
|
const fileName = basename(filePath);
|
|
21510
21712
|
const size = buf.byteLength;
|
|
21511
21713
|
const attachmentContentType = attachmentContentTypeForFileName(fileName);
|
|
@@ -24065,7 +24267,7 @@ Waiting for approval\u2026
|
|
|
24065
24267
|
authenticated: true,
|
|
24066
24268
|
method: "api-key",
|
|
24067
24269
|
storage: "environment"
|
|
24068
|
-
} : credentials ? {
|
|
24270
|
+
} : credentials && isCliCredentialForIssuer(credentials, BUDGET_BUILDER_API_BASE_URL) ? {
|
|
24069
24271
|
authenticated: true,
|
|
24070
24272
|
method: "browser-session",
|
|
24071
24273
|
accessTokenExpiresAt: credentials.accessTokenExpiresAt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
4
4
|
"description": "Budget Builder CLI for AI agents — manage budgets, bills, claims, quotations, and customer invoices with explicit workflow previews for sensitive changes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|