busa-sdk 0.10.2 → 0.10.3
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/chunk-5NYQX65A.js +6 -0
- package/dist/chunk-J2DZKX7A.js +175 -0
- package/dist/index.d.ts +502 -354
- package/dist/index.js +96 -39
- package/dist/oauth-node.d.ts +33 -0
- package/dist/oauth-node.js +147 -0
- package/dist/oauth.d.ts +55 -0
- package/dist/oauth.js +2 -0
- package/package.json +9 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
import { normalizeBaseUrl } from './chunk-5NYQX65A.js';
|
|
2
|
+
export { normalizeBaseUrl } from './chunk-5NYQX65A.js';
|
|
1
3
|
import { createORPCClient, ORPCError } from '@orpc/client';
|
|
2
4
|
import { OpenAPILink } from '@orpc/openapi-client/fetch';
|
|
3
5
|
import { oc, eventIterator } from '@orpc/contract';
|
|
4
6
|
import { z } from 'zod';
|
|
5
7
|
|
|
6
|
-
// src/
|
|
8
|
+
// src/asset-grep.ts
|
|
9
|
+
var toUnifiedFilesGrepInput = (input) => ({
|
|
10
|
+
pattern: input.pattern,
|
|
11
|
+
flags: input.flags,
|
|
12
|
+
sources: ["files"],
|
|
13
|
+
scope: input.scope ? { files: input.scope } : void 0,
|
|
14
|
+
maxMatches: input.maxMatches,
|
|
15
|
+
contextLines: input.contextLines
|
|
16
|
+
});
|
|
17
|
+
var toFilesOnlyGrepResult = (result) => ({
|
|
18
|
+
matches: result.matches.flatMap((match) => {
|
|
19
|
+
if (match.source !== "files") return [];
|
|
20
|
+
const { source: _source, ...fileMatch } = match;
|
|
21
|
+
return [fileMatch];
|
|
22
|
+
}),
|
|
23
|
+
filesScanned: result.coverage.files.scanned,
|
|
24
|
+
missing: result.coverage.files.missing,
|
|
25
|
+
stale: result.coverage.files.stale,
|
|
26
|
+
unsearchable: result.coverage.files.unsearchable,
|
|
27
|
+
errored: result.coverage.files.errored,
|
|
28
|
+
notReached: result.coverage.files.notReached,
|
|
29
|
+
truncated: result.truncated
|
|
30
|
+
});
|
|
31
|
+
var grepAssets = async (client, input) => toFilesOnlyGrepResult(await client.grep(toUnifiedFilesGrepInput(input)));
|
|
7
32
|
var i18n = {
|
|
8
33
|
locales: ["en", "zh-CN", "zh-TW", "ja", "ko", "de", "fr", "es", "pt"]};
|
|
9
34
|
var LocaleSchema = z.enum(i18n.locales);
|
|
@@ -1277,7 +1302,7 @@ var GREP_DEFAULT_MAX_MATCHES = 100;
|
|
|
1277
1302
|
var GREP_HARD_MAX_MATCHES = 1e3;
|
|
1278
1303
|
var GREP_DEFAULT_CONTEXT_LINES = 0;
|
|
1279
1304
|
var GREP_MAX_CONTEXT_LINES = 10;
|
|
1280
|
-
|
|
1305
|
+
z.object({
|
|
1281
1306
|
pattern: z.string().min(1),
|
|
1282
1307
|
/** JS RegExp flags, e.g. `"i"` for case-insensitive. `g`/`y` are ignored (grep always scans every match per line). */
|
|
1283
1308
|
flags: z.string().optional().default(""),
|
|
@@ -1298,7 +1323,7 @@ var GrepMatchVOSchema = z.object({
|
|
|
1298
1323
|
before: z.array(z.string()),
|
|
1299
1324
|
after: z.array(z.string())
|
|
1300
1325
|
});
|
|
1301
|
-
|
|
1326
|
+
z.object({
|
|
1302
1327
|
matches: z.array(GrepMatchVOSchema),
|
|
1303
1328
|
filesScanned: z.number().int().nonnegative(),
|
|
1304
1329
|
/** Asset ids in scope with no text yet (contentKind text-or-writable-binary, no row). */
|
|
@@ -1432,13 +1457,6 @@ var assetsContract = {
|
|
|
1432
1457
|
summary: "Request a presigned upload URL for large text",
|
|
1433
1458
|
successDescription: "Presigned (or dev) upload URL for a temporary text object; PUT the bytes there, then call putText with the returned storageKey to bind, verify, and content-address it."
|
|
1434
1459
|
}).input(CreateTextUploadUrlInputSchema).output(CreateTextUploadUrlVOSchema),
|
|
1435
|
-
grep: oc.route({
|
|
1436
|
-
method: "POST",
|
|
1437
|
-
path: "/assets/grep",
|
|
1438
|
-
tags: ["Assets"],
|
|
1439
|
-
summary: "Search every text-bearing asset in scope",
|
|
1440
|
-
successDescription: "Streaming regex/literal matches with real file + line + column numbers and context, across every asset with text \u2014 any size, no 256KB cap. Honest coverage: missing/stale/unsearchable/errored name assets that were not (fully or successfully) searched, notReached counts present assets the scan never got to, and truncated flags a capped response."
|
|
1441
|
-
}).input(GrepInputSchema).output(GrepResultVOSchema),
|
|
1442
1460
|
readTextLines: oc.route({
|
|
1443
1461
|
method: "GET",
|
|
1444
1462
|
path: "/assets/{assetId}/text/lines",
|
|
@@ -1616,6 +1634,19 @@ var listRecordsResponseSchema = z.object({
|
|
|
1616
1634
|
records: z.array(recordSchema),
|
|
1617
1635
|
nextCursor: z.string().nullable()
|
|
1618
1636
|
});
|
|
1637
|
+
var listRecordsPageInputSchema = z.object({
|
|
1638
|
+
baseId: z.string().min(1),
|
|
1639
|
+
viewId: z.string().min(1).optional(),
|
|
1640
|
+
page: z.coerce.number().int().min(1).optional().default(1),
|
|
1641
|
+
pageSize: z.coerce.number().int().min(1).max(100).optional().default(50)
|
|
1642
|
+
});
|
|
1643
|
+
var listRecordsPageResponseSchema = z.object({
|
|
1644
|
+
records: z.array(recordSchema),
|
|
1645
|
+
total: z.number().int().nonnegative(),
|
|
1646
|
+
totalPages: z.number().int().nonnegative(),
|
|
1647
|
+
page: z.number().int().min(1),
|
|
1648
|
+
pageSize: z.number().int().min(1).max(100)
|
|
1649
|
+
});
|
|
1619
1650
|
var countRecordsInputSchema = z.object({
|
|
1620
1651
|
baseId: z.string().optional()
|
|
1621
1652
|
}).optional().default({});
|
|
@@ -1662,10 +1693,16 @@ var recordFieldFilterInputSchema = z.object({
|
|
|
1662
1693
|
limit: z.coerce.number().int().min(1).max(100).optional().default(50)
|
|
1663
1694
|
});
|
|
1664
1695
|
var recordFieldGetInputSchema = z.object({
|
|
1665
|
-
baseId: z.string(),
|
|
1666
|
-
fieldSlug: z.string().min(1),
|
|
1667
|
-
valueText: z.string().min(1)
|
|
1696
|
+
baseId: z.string().describe("Field selector: Base id. Requires fieldSlug and valueText."),
|
|
1697
|
+
fieldSlug: z.string().min(1).describe("Field selector: exact field slug. Requires baseId and valueText."),
|
|
1698
|
+
valueText: z.string().min(1).describe("Field selector: exact text value. Requires baseId and fieldSlug.")
|
|
1668
1699
|
});
|
|
1700
|
+
var recordGetInputSchema = z.union([
|
|
1701
|
+
z.object({
|
|
1702
|
+
recordId: z.string().min(1).describe("Record id selector. Use alone; do not combine with field selector fields.")
|
|
1703
|
+
}).strict(),
|
|
1704
|
+
recordFieldGetInputSchema.strict()
|
|
1705
|
+
]);
|
|
1669
1706
|
var restoreRecordInputSchema = z.object({
|
|
1670
1707
|
message: z.string().optional(),
|
|
1671
1708
|
submittedBy: z.string().optional().default("local-editor")
|
|
@@ -1801,6 +1838,13 @@ var recordContract = {
|
|
|
1801
1838
|
summary: "List records",
|
|
1802
1839
|
successDescription: "A page of canonical records plus an opaque nextCursor (null at the end). `status=archived` lists the Base's trash instead of its live rows."
|
|
1803
1840
|
}).input(listRecordsInputSchema).output(listRecordsResponseSchema),
|
|
1841
|
+
listPage: oc.route({
|
|
1842
|
+
method: "GET",
|
|
1843
|
+
path: "/records/page",
|
|
1844
|
+
tags: ["Records"],
|
|
1845
|
+
summary: "List a numbered record page",
|
|
1846
|
+
successDescription: "A random-access page of active records. When viewId is supplied, the saved view is authoritatively filtered and sorted before total and page slicing are calculated."
|
|
1847
|
+
}).input(listRecordsPageInputSchema).output(listRecordsPageResponseSchema),
|
|
1804
1848
|
count: oc.route({
|
|
1805
1849
|
method: "GET",
|
|
1806
1850
|
path: "/records/count",
|
|
@@ -1810,11 +1854,15 @@ var recordContract = {
|
|
|
1810
1854
|
}).input(countRecordsInputSchema).output(countRecordsResponseSchema),
|
|
1811
1855
|
get: oc.route({
|
|
1812
1856
|
method: "GET",
|
|
1813
|
-
path: "/records/
|
|
1857
|
+
path: "/records/get",
|
|
1814
1858
|
tags: ["Records"],
|
|
1815
1859
|
summary: "Get record",
|
|
1816
|
-
|
|
1817
|
-
|
|
1860
|
+
description: "Provide exactly one selector: recordId alone, or the complete baseId + fieldSlug + valueText tuple. Other combinations return 400.",
|
|
1861
|
+
successDescription: "One canonical record selected by id or exact field value."
|
|
1862
|
+
}).errors({
|
|
1863
|
+
BAD_REQUEST: { status: 400, message: "Exactly one record selector is required" },
|
|
1864
|
+
NOT_FOUND: { status: 404, message: "Record not found" }
|
|
1865
|
+
}).input(recordGetInputSchema).output(recordSchema),
|
|
1818
1866
|
search: oc.route({
|
|
1819
1867
|
method: "GET",
|
|
1820
1868
|
path: "/records/search",
|
|
@@ -1822,13 +1870,6 @@ var recordContract = {
|
|
|
1822
1870
|
summary: "Filter records by field text",
|
|
1823
1871
|
successDescription: "Canonical records matching a field text filter."
|
|
1824
1872
|
}).input(recordFieldFilterInputSchema).output(z.array(recordSchema)),
|
|
1825
|
-
getByField: oc.route({
|
|
1826
|
-
method: "GET",
|
|
1827
|
-
path: "/records/by-field",
|
|
1828
|
-
tags: ["Records"],
|
|
1829
|
-
summary: "Get record by field value",
|
|
1830
|
-
successDescription: "Single canonical record whose field value exactly matches, or null when none does \u2014 a scoped point lookup (e.g. by a unique slug or path field), not a list."
|
|
1831
|
-
}).input(recordFieldGetInputSchema).output(recordSchema.nullable()),
|
|
1832
1873
|
changeRequest: oc.route({
|
|
1833
1874
|
method: "POST",
|
|
1834
1875
|
path: "/records/{recordId}/change-requests",
|
|
@@ -2697,7 +2738,7 @@ var UnifiedGrepScopeSchema = z.object({
|
|
|
2697
2738
|
});
|
|
2698
2739
|
var UnifiedGrepInputSchema = z.object({
|
|
2699
2740
|
pattern: z.string().min(1),
|
|
2700
|
-
/** JS RegExp flags, e.g. `"i"` for case-insensitive
|
|
2741
|
+
/** JS RegExp flags, e.g. `"i"` for case-insensitive. */
|
|
2701
2742
|
flags: z.string().optional().default(""),
|
|
2702
2743
|
/** Which sources to scan. Omitted = all three (`files`, `docs`, `records`). */
|
|
2703
2744
|
sources: z.array(GrepSourceSchema).optional(),
|
|
@@ -2806,11 +2847,9 @@ var busabaseContractRoutes = {
|
|
|
2806
2847
|
summary: "Search Busabase",
|
|
2807
2848
|
successDescription: "Paginated search results across records, change requests, Bases, File nodes, and Assets."
|
|
2808
2849
|
}).input(searchInputSchema).output(searchResponseSchema),
|
|
2809
|
-
// Unified Grep (P2a files+docs, P2b records) —
|
|
2810
|
-
//
|
|
2811
|
-
//
|
|
2812
|
-
// unchanged and stays the dedicated endpoint for its fuller
|
|
2813
|
-
// missing/stale/unsearchable reporting.
|
|
2850
|
+
// Unified Grep (P2a files+docs, P2b records) — the single public pattern
|
|
2851
|
+
// search endpoint. Files-only callers use `sources: ["files"]` and retain
|
|
2852
|
+
// the full missing/stale/unsearchable coverage block.
|
|
2814
2853
|
grep: oc.route({
|
|
2815
2854
|
method: "POST",
|
|
2816
2855
|
path: "/grep",
|
|
@@ -3384,9 +3423,6 @@ var env = (key) => {
|
|
|
3384
3423
|
const value = process.env[key];
|
|
3385
3424
|
return value && value.length > 0 ? value : void 0;
|
|
3386
3425
|
};
|
|
3387
|
-
function normalizeBaseUrl(raw) {
|
|
3388
|
-
return raw.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
|
|
3389
|
-
}
|
|
3390
3426
|
function resolveConfig(config = {}) {
|
|
3391
3427
|
return {
|
|
3392
3428
|
baseUrl: normalizeBaseUrl(config.baseUrl ?? env("BUSABASE_BASE_URL") ?? DEFAULT_BASE_URL),
|
|
@@ -3434,6 +3470,17 @@ function createBusabaseClient(config = {}) {
|
|
|
3434
3470
|
return createORPCClient(link);
|
|
3435
3471
|
}
|
|
3436
3472
|
|
|
3473
|
+
// src/record-get.ts
|
|
3474
|
+
var isNotFound = (error) => typeof error === "object" && error !== null && ("status" in error && error.status === 404 || "code" in error && error.code === "NOT_FOUND");
|
|
3475
|
+
var getRecordByField = async (client, input) => {
|
|
3476
|
+
try {
|
|
3477
|
+
return await client.records.get(input);
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
if (isNotFound(error)) return null;
|
|
3480
|
+
throw error;
|
|
3481
|
+
}
|
|
3482
|
+
};
|
|
3483
|
+
|
|
3437
3484
|
// src/index.ts
|
|
3438
3485
|
var Busabase = class {
|
|
3439
3486
|
/** The underlying fully-typed oRPC client. Use it for anything not surfaced here. */
|
|
@@ -3450,7 +3497,13 @@ var Busabase = class {
|
|
|
3450
3497
|
return this.client.bases;
|
|
3451
3498
|
}
|
|
3452
3499
|
get records() {
|
|
3453
|
-
|
|
3500
|
+
const getByField = (input) => getRecordByField(this.client, input);
|
|
3501
|
+
return new Proxy(this.client.records, {
|
|
3502
|
+
get(target, property, receiver) {
|
|
3503
|
+
if (property === "getByField") return getByField;
|
|
3504
|
+
return Reflect.get(target, property, receiver);
|
|
3505
|
+
}
|
|
3506
|
+
});
|
|
3454
3507
|
}
|
|
3455
3508
|
get views() {
|
|
3456
3509
|
return this.client.views;
|
|
@@ -3474,7 +3527,13 @@ var Busabase = class {
|
|
|
3474
3527
|
return this.client.agent;
|
|
3475
3528
|
}
|
|
3476
3529
|
get assets() {
|
|
3477
|
-
|
|
3530
|
+
const filesOnlyGrep = (input) => grepAssets(this.client, input);
|
|
3531
|
+
return new Proxy(this.client.assets, {
|
|
3532
|
+
get(target, property, receiver) {
|
|
3533
|
+
if (property === "grep") return filesOnlyGrep;
|
|
3534
|
+
return Reflect.get(target, property, receiver);
|
|
3535
|
+
}
|
|
3536
|
+
});
|
|
3478
3537
|
}
|
|
3479
3538
|
/** Skills, Drives, and AirApps — one surface, discriminated by `type`. */
|
|
3480
3539
|
get fileTrees() {
|
|
@@ -3507,10 +3566,8 @@ var Busabase = class {
|
|
|
3507
3566
|
* source (Drive/Skill files, Doc bodies, and Base records — records read
|
|
3508
3567
|
* the canonical `headCommit.fields`, never the truncated search
|
|
3509
3568
|
* projection), with a shared `maxMatches`/deadline budget and per-source
|
|
3510
|
-
* honest coverage.
|
|
3511
|
-
*
|
|
3512
|
-
* about files and want its fuller `missing`/`stale`/`unsearchable`
|
|
3513
|
-
* file-only reporting.
|
|
3569
|
+
* honest coverage. `bb.assets.grep` remains available as a files-only SDK
|
|
3570
|
+
* convenience and delegates here with `sources: ["files"]`.
|
|
3514
3571
|
*/
|
|
3515
3572
|
grep(input) {
|
|
3516
3573
|
return this.client.grep(input);
|
|
@@ -3560,4 +3617,4 @@ var Busabase = class {
|
|
|
3560
3617
|
}
|
|
3561
3618
|
};
|
|
3562
3619
|
|
|
3563
|
-
export { Busabase, CREATABLE_NODE_TYPES, DEFAULT_BASE_URL, cloudContract, createBusabaseClient,
|
|
3620
|
+
export { Busabase, CREATABLE_NODE_TYPES, DEFAULT_BASE_URL, cloudContract, createBusabaseClient, getRecordByField, grepAssets, resolveConfig, toFilesOnlyGrepResult, toUnifiedFilesGrepInput };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BusabaseOAuthTokenSet } from './oauth.js';
|
|
2
|
+
|
|
3
|
+
interface BusabaseAirAppOAuthCredential {
|
|
4
|
+
version: 1;
|
|
5
|
+
appId: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
accessToken: string;
|
|
9
|
+
refreshToken: string;
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
scope: string[];
|
|
12
|
+
tokenType: string;
|
|
13
|
+
}
|
|
14
|
+
interface BusabaseAirAppCredentialStoreOptions {
|
|
15
|
+
/** Override only for tests or an explicitly isolated installation. */
|
|
16
|
+
rootDir?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Directory containing one owner-only OAuth registration per local AirApp. */
|
|
19
|
+
declare const busabaseAirAppCredentialsDir: (options?: BusabaseAirAppCredentialStoreOptions) => string;
|
|
20
|
+
declare const busabaseAirAppCredentialPath: (appId: string, options?: BusabaseAirAppCredentialStoreOptions) => string;
|
|
21
|
+
declare function loadBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential | null;
|
|
22
|
+
declare function storeBusabaseAirAppOAuthCredential(input: {
|
|
23
|
+
appId: string;
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
tokenSet: BusabaseOAuthTokenSet;
|
|
26
|
+
clientId?: string;
|
|
27
|
+
}, options?: BusabaseAirAppCredentialStoreOptions): BusabaseAirAppOAuthCredential;
|
|
28
|
+
/** Load a valid access token, rotating and persisting the token set when needed. */
|
|
29
|
+
declare function getBusabaseAirAppAccessToken(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<BusabaseAirAppOAuthCredential | null>;
|
|
30
|
+
declare function clearBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions): void;
|
|
31
|
+
declare function revokeBusabaseAirAppOAuthCredential(appId: string, options?: BusabaseAirAppCredentialStoreOptions, fetchImpl?: typeof fetch): Promise<void>;
|
|
32
|
+
|
|
33
|
+
export { type BusabaseAirAppCredentialStoreOptions, type BusabaseAirAppOAuthCredential, busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { BusabaseOAuthError, BUSABASE_AIRAPP_CLIENT_ID, refreshBusabaseOAuthToken, revokeBusabaseOAuthToken } from './chunk-J2DZKX7A.js';
|
|
2
|
+
import { normalizeBaseUrl } from './chunk-5NYQX65A.js';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { readFileSync, mkdirSync, chmodSync, writeFileSync, renameSync, rmSync } from 'fs';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
var STORE_VERSION = 1;
|
|
9
|
+
var APP_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10
|
+
var REFRESH_WINDOW_MS = 6e4;
|
|
11
|
+
var refreshesByCredentialPath = /* @__PURE__ */ new Map();
|
|
12
|
+
var assertAppId = (appId) => {
|
|
13
|
+
if (!APP_ID_RE.test(appId)) {
|
|
14
|
+
throw new BusabaseOAuthError(
|
|
15
|
+
"invalid_airapp_id",
|
|
16
|
+
"AirApp id must use letters, digits, dot, dash, or underscore"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return appId;
|
|
20
|
+
};
|
|
21
|
+
var storeRoot = (options = {}) => options.rootDir ?? join(homedir(), ".busabase");
|
|
22
|
+
var busabaseAirAppCredentialsDir = (options = {}) => join(storeRoot(options), "airapps");
|
|
23
|
+
var busabaseAirAppCredentialPath = (appId, options = {}) => join(busabaseAirAppCredentialsDir(options), `${assertAppId(appId)}.json`);
|
|
24
|
+
var normalizeOrigin = (raw) => {
|
|
25
|
+
const url = new URL(normalizeBaseUrl(raw));
|
|
26
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
27
|
+
throw new BusabaseOAuthError("invalid_base_url", "Busabase base URL must be an origin");
|
|
28
|
+
}
|
|
29
|
+
return url.origin;
|
|
30
|
+
};
|
|
31
|
+
var parseCredential = (raw, expectedAppId) => {
|
|
32
|
+
let value;
|
|
33
|
+
try {
|
|
34
|
+
value = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
|
|
37
|
+
}
|
|
38
|
+
const item = value;
|
|
39
|
+
if (item.version !== STORE_VERSION || item.appId !== expectedAppId || typeof item.baseUrl !== "string" || typeof item.clientId !== "string" || typeof item.accessToken !== "string" || typeof item.refreshToken !== "string" || typeof item.expiresAt !== "string" || !Array.isArray(item.scope) || item.scope.some((scope) => typeof scope !== "string") || typeof item.tokenType !== "string") {
|
|
40
|
+
throw new BusabaseOAuthError("invalid_local_credential", "AirApp OAuth credential is invalid");
|
|
41
|
+
}
|
|
42
|
+
return item;
|
|
43
|
+
};
|
|
44
|
+
function loadBusabaseAirAppOAuthCredential(appId, options = {}) {
|
|
45
|
+
const path = busabaseAirAppCredentialPath(appId, options);
|
|
46
|
+
try {
|
|
47
|
+
return parseCredential(readFileSync(path, "utf8"), appId);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === "ENOENT") return null;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function storeBusabaseAirAppOAuthCredential(input, options = {}) {
|
|
54
|
+
if (!input.tokenSet.refreshToken) {
|
|
55
|
+
throw new BusabaseOAuthError(
|
|
56
|
+
"missing_refresh_token",
|
|
57
|
+
"A refresh token is required for a persistent local AirApp login"
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const credential = {
|
|
61
|
+
version: STORE_VERSION,
|
|
62
|
+
appId: assertAppId(input.appId),
|
|
63
|
+
baseUrl: normalizeOrigin(input.baseUrl),
|
|
64
|
+
clientId: input.clientId ?? BUSABASE_AIRAPP_CLIENT_ID,
|
|
65
|
+
accessToken: input.tokenSet.accessToken,
|
|
66
|
+
refreshToken: input.tokenSet.refreshToken,
|
|
67
|
+
expiresAt: input.tokenSet.expiresAt,
|
|
68
|
+
scope: input.tokenSet.scope,
|
|
69
|
+
tokenType: input.tokenSet.tokenType
|
|
70
|
+
};
|
|
71
|
+
const path = busabaseAirAppCredentialPath(input.appId, options);
|
|
72
|
+
const directory = dirname(path);
|
|
73
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
74
|
+
try {
|
|
75
|
+
chmodSync(directory, 448);
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
79
|
+
writeFileSync(temporaryPath, `${JSON.stringify(credential, null, 2)}
|
|
80
|
+
`, { mode: 384 });
|
|
81
|
+
try {
|
|
82
|
+
chmodSync(temporaryPath, 384);
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
renameSync(temporaryPath, path);
|
|
86
|
+
return credential;
|
|
87
|
+
}
|
|
88
|
+
async function getBusabaseAirAppAccessToken(appId, options = {}, fetchImpl = fetch) {
|
|
89
|
+
const credential = loadBusabaseAirAppOAuthCredential(appId, options);
|
|
90
|
+
if (!credential) return null;
|
|
91
|
+
const expiresAt = Date.parse(credential.expiresAt);
|
|
92
|
+
if (Number.isFinite(expiresAt) && expiresAt > Date.now() + REFRESH_WINDOW_MS) return credential;
|
|
93
|
+
const credentialPath = busabaseAirAppCredentialPath(appId, options);
|
|
94
|
+
const activeRefresh = refreshesByCredentialPath.get(credentialPath);
|
|
95
|
+
if (activeRefresh) return activeRefresh;
|
|
96
|
+
const refresh = (async () => {
|
|
97
|
+
const tokenSet = await refreshBusabaseOAuthToken(
|
|
98
|
+
{
|
|
99
|
+
baseUrl: credential.baseUrl,
|
|
100
|
+
refreshToken: credential.refreshToken,
|
|
101
|
+
clientId: credential.clientId
|
|
102
|
+
},
|
|
103
|
+
fetchImpl
|
|
104
|
+
);
|
|
105
|
+
return storeBusabaseAirAppOAuthCredential(
|
|
106
|
+
{
|
|
107
|
+
appId,
|
|
108
|
+
baseUrl: credential.baseUrl,
|
|
109
|
+
clientId: credential.clientId,
|
|
110
|
+
tokenSet: {
|
|
111
|
+
...tokenSet,
|
|
112
|
+
refreshToken: tokenSet.refreshToken ?? credential.refreshToken
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
options
|
|
116
|
+
);
|
|
117
|
+
})();
|
|
118
|
+
refreshesByCredentialPath.set(credentialPath, refresh);
|
|
119
|
+
try {
|
|
120
|
+
return await refresh;
|
|
121
|
+
} finally {
|
|
122
|
+
if (refreshesByCredentialPath.get(credentialPath) === refresh) {
|
|
123
|
+
refreshesByCredentialPath.delete(credentialPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function clearBusabaseAirAppOAuthCredential(appId, options = {}) {
|
|
128
|
+
rmSync(busabaseAirAppCredentialPath(appId, options), { force: true });
|
|
129
|
+
}
|
|
130
|
+
async function revokeBusabaseAirAppOAuthCredential(appId, options = {}, fetchImpl = fetch) {
|
|
131
|
+
const credential = loadBusabaseAirAppOAuthCredential(appId, options);
|
|
132
|
+
if (!credential) return;
|
|
133
|
+
try {
|
|
134
|
+
await revokeBusabaseOAuthToken(
|
|
135
|
+
{
|
|
136
|
+
baseUrl: credential.baseUrl,
|
|
137
|
+
token: credential.refreshToken,
|
|
138
|
+
clientId: credential.clientId
|
|
139
|
+
},
|
|
140
|
+
fetchImpl
|
|
141
|
+
);
|
|
142
|
+
} finally {
|
|
143
|
+
clearBusabaseAirAppOAuthCredential(appId, options);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { busabaseAirAppCredentialPath, busabaseAirAppCredentialsDir, clearBusabaseAirAppOAuthCredential, getBusabaseAirAppAccessToken, loadBusabaseAirAppOAuthCredential, revokeBusabaseAirAppOAuthCredential, storeBusabaseAirAppOAuthCredential };
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Public client identifier used by local Hono/AirApp development servers. */
|
|
2
|
+
declare const BUSABASE_AIRAPP_CLIENT_ID = "busabase-airapp";
|
|
3
|
+
|
|
4
|
+
interface BusabaseOAuthRequest {
|
|
5
|
+
authorizeUrl: string;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
codeVerifier: string;
|
|
9
|
+
redirectUri: string;
|
|
10
|
+
resource: string;
|
|
11
|
+
state: string;
|
|
12
|
+
}
|
|
13
|
+
interface CreateBusabaseOAuthRequestInput {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
redirectUri: string;
|
|
16
|
+
clientId?: string;
|
|
17
|
+
state?: string;
|
|
18
|
+
prompt?: "login";
|
|
19
|
+
}
|
|
20
|
+
interface BusabaseOAuthTokenSet {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
refreshToken?: string;
|
|
23
|
+
expiresIn: number;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
scope: string[];
|
|
26
|
+
tokenType: string;
|
|
27
|
+
user?: {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
email: string;
|
|
31
|
+
image: string | null;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
declare class BusabaseOAuthError extends Error {
|
|
35
|
+
readonly code: string;
|
|
36
|
+
readonly status?: number;
|
|
37
|
+
constructor(code: string, message: string, status?: number);
|
|
38
|
+
}
|
|
39
|
+
/** Build a public-client OAuth 2.1 authorization request with PKCE S256. */
|
|
40
|
+
declare function createBusabaseOAuthRequest(input: CreateBusabaseOAuthRequestInput): Promise<BusabaseOAuthRequest>;
|
|
41
|
+
/** Validate state and issuer before accepting the authorization code. */
|
|
42
|
+
declare function parseBusabaseOAuthCallback(callbackUrl: string, request: BusabaseOAuthRequest): string;
|
|
43
|
+
declare function exchangeBusabaseOAuthCode(request: BusabaseOAuthRequest, code: string, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
|
|
44
|
+
declare function refreshBusabaseOAuthToken(input: {
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
refreshToken: string;
|
|
47
|
+
clientId?: string;
|
|
48
|
+
}, fetchImpl?: typeof fetch): Promise<BusabaseOAuthTokenSet>;
|
|
49
|
+
declare function revokeBusabaseOAuthToken(input: {
|
|
50
|
+
baseUrl: string;
|
|
51
|
+
token: string;
|
|
52
|
+
clientId?: string;
|
|
53
|
+
}, fetchImpl?: typeof fetch): Promise<void>;
|
|
54
|
+
|
|
55
|
+
export { BUSABASE_AIRAPP_CLIENT_ID, BusabaseOAuthError, type BusabaseOAuthRequest, type BusabaseOAuthTokenSet, type CreateBusabaseOAuthRequestInput, createBusabaseOAuthRequest, exchangeBusabaseOAuthCode, parseBusabaseOAuthCallback, refreshBusabaseOAuthToken, revokeBusabaseOAuthToken };
|
package/dist/oauth.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "busa-sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Typed TypeScript/JavaScript SDK for the Busabase OpenAPI REST API. Talks to a local or remote `busabase server` (or Busabase Cloud). Short-name alias for busabase-sdk.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/busabase/busabase/tree/main/apps/busabase-sdk",
|
|
@@ -22,6 +22,14 @@
|
|
|
22
22
|
".": {
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
24
24
|
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./oauth": {
|
|
27
|
+
"types": "./dist/oauth.d.ts",
|
|
28
|
+
"default": "./dist/oauth.js"
|
|
29
|
+
},
|
|
30
|
+
"./oauth-node": {
|
|
31
|
+
"types": "./dist/oauth-node.d.ts",
|
|
32
|
+
"default": "./dist/oauth-node.js"
|
|
25
33
|
}
|
|
26
34
|
},
|
|
27
35
|
"files": [
|